- {{#if this.channel.isFollowing}}
+ {{#if @channel.isFollowing}}
- {{else if this.channel.isJoinable}}
+ {{else if @channel.isJoinable}}
{{/if}}
- {{#if (gt this.channel.membershipsCount 0)}}
+ {{#if (gt @channel.membershipsCount 0)}}
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js
index 3392fe9f059..36a8f64e98b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js
@@ -1,13 +1,6 @@
-import Component from "@ember/component";
-import { action } from "@ember/object";
+import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class ChatChannelCard extends Component {
@service chat;
- tagName = "";
-
- @action
- afterMembershipToggle() {
- this.chat.forceRefreshChannels();
- }
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
index 3f38523f186..4943ae1e34b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
@@ -3,14 +3,15 @@ import { isEmpty } from "@ember/utils";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { action } from "@ember/object";
-import { ajax } from "discourse/lib/ajax";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseLater from "discourse-common/lib/later";
import { htmlSafe } from "@ember/template";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Component.extend({
+export default Component.extend(ModalFunctionality, {
chat: service(),
+ chatApi: service(),
router: service(),
tagName: "",
chatChannel: null,
@@ -37,16 +38,14 @@ export default Component.extend({
@action
deleteChannel() {
this.set("deleting", true);
- return ajax(`/chat/chat_channels/${this.chatChannel.id}.json`, {
- method: "DELETE",
- data: { channel_name_confirmation: this.channelNameConfirmation },
- })
+
+ return this.chatApi
+ .destroyChannel(this.chatChannel.id, {
+ name_confirmation: this.channelNameConfirmation,
+ })
.then(() => {
this.set("confirmed", true);
- this.appEvents.trigger("modal-body:flash", {
- text: I18n.t("chat.channel_delete.process_started"),
- messageClass: "success",
- });
+ this.flash(I18n.t("chat.channel_delete.process_started"), "success");
discourseLater(() => {
this.closeModal();
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs
index 90834e6051b..5acfc7a5267 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs
@@ -1,6 +1,6 @@
{{#if this.chatProgressBarContainer}}
{{#in-element this.chatProgressBarContainer}}
-
- {{#each this.members as |member|}}
+ {{#each this.members as |membership|}}
-
-
+
+
{{else}}
{{#unless this.isFetchingMembers}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js
index 49907dbd68c..dea4cad847d 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js
@@ -1,24 +1,20 @@
-import { isEmpty } from "@ember/utils";
import { INPUT_DELAY } from "discourse-common/config/environment";
import Component from "@ember/component";
import { action } from "@ember/object";
import { schedule } from "@ember/runloop";
-import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
import discourseDebounce from "discourse-common/lib/debounce";
-
-const LIMIT = 50;
+import { inject as service } from "@ember/service";
export default class ChatChannelMembersView extends Component {
+ @service chatApi;
+
tagName = "";
channel = null;
- members = null;
isSearchFocused = false;
- isFetchingMembers = false;
onlineUsers = null;
- offset = 0;
filter = null;
inputSelector = "channel-members-view__search-input";
- canLoadMore = true;
+ members = null;
didInsertElement() {
this._super(...arguments);
@@ -28,14 +24,15 @@ export default class ChatChannelMembersView extends Component {
}
this._focusSearch();
- this.set("members", []);
- this.fetchMembers();
+ this.set("members", this.chatApi.listChannelMemberships(this.channel.id));
+ this.members.load();
this.appEvents.on("chat:refresh-channel-members", this, "onFilterMembers");
}
willDestroyElement() {
this._super(...arguments);
+
this.appEvents.off("chat:refresh-channel-members", this, "onFilterMembers");
}
@@ -46,59 +43,18 @@ export default class ChatChannelMembersView extends Component {
@action
onFilterMembers(username) {
this.set("filter", username);
- this.set("offset", 0);
- this.set("canLoadMore", true);
discourseDebounce(
this,
- this.fetchMembers,
- this.filter,
- this.offset,
+ this.members.load,
+ { username: this.filter },
INPUT_DELAY
);
}
@action
loadMore() {
- if (!this.canLoadMore) {
- return;
- }
-
- discourseDebounce(
- this,
- this.fetchMembers,
- this.filter,
- this.offset,
- INPUT_DELAY
- );
- }
-
- fetchMembersHandler(id, params = {}) {
- return ChatApi.chatChannelMemberships(id, params);
- }
-
- fetchMembers(filter = null, offset = 0) {
- this.set("isFetchingMembers", true);
-
- return this.fetchMembersHandler(this.channel.id, {
- username: filter,
- offset,
- })
- .then((response) => {
- if (this.offset === 0) {
- this.set("members", []);
- }
-
- if (isEmpty(response)) {
- this.set("canLoadMore", false);
- } else {
- this.set("offset", this.offset + LIMIT);
- this.members.pushObjects(response);
- }
- })
- .finally(() => {
- this.set("isFetchingMembers", false);
- });
+ discourseDebounce(this, this.members.loadMore, INPUT_DELAY);
}
_focusSearch() {
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs
index 2e7f8f58005..eed8d6c84c2 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs
@@ -13,7 +13,6 @@
{{#if this.showJoinButton}}
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js
index 23be8f9a672..954313febe9 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js
@@ -1,6 +1,6 @@
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
-import { action, computed } from "@ember/object";
+import { computed } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import { inject as service } from "@ember/service";
@@ -16,11 +16,4 @@ export default class ChatChannelPreviewCard extends Component {
get hasDescription() {
return !isEmpty(this.channel.description);
}
-
- @action
- afterMembershipToggle() {
- this.chat.forceRefreshChannels().then(() => {
- this.chat.openChannel(this.channel);
- });
- }
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs
index 83776ff9f77..e77ee34205f 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs
@@ -4,7 +4,7 @@
class={{concat-class
"chat-channel-row"
(if @channel.focused "focused")
- (if @channel.current_user_membership.muted "muted")
+ (if @channel.currentUserMembership.muted "muted")
(if @options.leaveButton "can-leave")
(if (eq this.chat.activeChannel.id @channel.id) "active")
(if this.channelHasUnread "has-unread")
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js
index e703be69381..3fe267f6f63 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js
@@ -19,11 +19,7 @@ export default class ChatChannelRow extends Component {
}
get channelHasUnread() {
- return (
- this.currentUser.get(
- `chat_channel_tracking_state.${this.args.channel?.id}.unread_count`
- ) > 0
- );
+ return this.args.channel.currentUserMembership.unread_count > 0;
}
get #firstDirectMessageUser() {
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js
index bfd46d64c0e..1937ff6bdae 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js
@@ -17,24 +17,24 @@ export default Component.extend({
channels: null,
searchIndex: 0,
loading: false,
-
- init() {
- this._super(...arguments);
- this.appEvents.on("chat-channel-selector-modal:close", this.close);
- this.getInitialChannels();
- },
+ chatChannelsManager: service(),
didInsertElement() {
this._super(...arguments);
+
+ this.appEvents.on("chat-channel-selector-modal:close", this.close);
document.addEventListener("keyup", this.onKeyUp);
document
.getElementById("chat-channel-selector-modal-inner")
?.addEventListener("mouseover", this.mouseover);
document.getElementById("chat-channel-selector-input")?.focus();
+
+ this.getInitialChannels();
},
willDestroyElement() {
this._super(...arguments);
+
this.appEvents.off("chat-channel-selector-modal:close", this.close);
document.removeEventListener("keyup", this.onKeyUp);
document
@@ -101,16 +101,17 @@ export default Component.extend({
switchChannel(channel) {
if (channel.user) {
return this.fetchOrCreateChannelForUser(channel).then((response) => {
- this.chat
- .startTrackingChannel(ChatChannel.create(response.chat_channel))
- .then((newlyTracked) => {
- this.chat.openChannel(newlyTracked);
- this.close();
- });
+ const newChannel = this.chatChannelsManager.store(response.channel);
+ return this.chatChannelsManager.follow(newChannel).then((c) => {
+ this.chat.openChannel(c);
+ this.close();
+ });
});
} else {
- this.chat.openChannel(channel);
- this.close();
+ return this.chatChannelsManager.follow(channel).then((c) => {
+ this.chat.openChannel(c);
+ this.close();
+ });
}
},
@@ -135,7 +136,7 @@ export default Component.extend({
searchIndex: this.searchIndex + 1,
});
const thisSearchIndex = this.searchIndex;
- ajax("/chat/chat_channels/search", { data: { filter } })
+ ajax("/chat/api/chatables", { data: { filter } })
.then((searchModel) => {
if (this.searchIndex === thisSearchIndex) {
this.set("searchModel", searchModel);
@@ -149,7 +150,11 @@ export default Component.extend({
}
});
this.setProperties({
- channels: channels.map((channel) => ChatChannel.create(channel)),
+ channels: channels.map((channel) => {
+ return channel.user
+ ? ChatChannel.create(channel)
+ : this.chatChannelsManager.store(channel);
+ }),
loading: false,
});
this.focusFirstChannel(this.channels);
@@ -160,10 +165,9 @@ export default Component.extend({
@action
getInitialChannels() {
- return this.chat.getChannelsWithFilter(this.filter).then((channels) => {
- this.focusFirstChannel(channels);
- this.set("channels", channels);
- });
+ const channels = this.getChannelsWithFilter(this.filter);
+ this.set("channels", channels);
+ this.focusFirstChannel(channels);
},
@action
@@ -178,4 +182,44 @@ export default Component.extend({
channels.forEach((c) => c.set("focused", false));
channels[0]?.set("focused", true);
},
+
+ getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
+ let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => {
+ return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at)
+ ? -1
+ : 1;
+ });
+
+ const trimmedFilter = filter.trim();
+ const lowerCasedFilter = filter.toLowerCase();
+ const { activeChannel } = this;
+
+ return sortedChannels.filter((channel) => {
+ if (
+ opts.excludeActiveChannel &&
+ activeChannel &&
+ activeChannel.id === channel.id
+ ) {
+ return false;
+ }
+ if (!trimmedFilter.length) {
+ return true;
+ }
+
+ if (channel.isDirectMessageChannel) {
+ let userFound = false;
+ channel.chatable.users.forEach((user) => {
+ if (
+ user.username.toLowerCase().includes(lowerCasedFilter) ||
+ user.name?.toLowerCase().includes(lowerCasedFilter)
+ ) {
+ return (userFound = true);
+ }
+ });
+ return userFound;
+ } else {
+ return channel.title.toLowerCase().includes(lowerCasedFilter);
+ }
+ });
+ },
});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
index 5597f74a5c9..9ea17a5e4ed 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
@@ -3,13 +3,13 @@
- {{#unless this.channel.current_user_membership.muted}}
+ {{#unless this.channel.currentUserMembership.muted}}
-
+
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js
index 3045b7f4ed7..7d404060d25 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js
@@ -3,14 +3,15 @@ import I18n from "I18n";
import { reads } from "@ember/object/computed";
import { isBlank } from "@ember/utils";
import { action, computed } from "@ember/object";
-import { ajax } from "discourse/lib/ajax";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { htmlSafe } from "@ember/template";
export default class MoveToChannelModalInner extends Component {
@service chat;
+ @service chatApi;
@service router;
+ @service chatChannelsManager;
tagName = "";
sourceChannel = null;
destinationChannelId = null;
@@ -23,31 +24,25 @@ export default class MoveToChannelModalInner extends Component {
return isBlank(this.destinationChannelId);
}
- @computed("chat.publicChannels.[]")
+ @computed("chatChannelsManager.publicMessageChannels.[]")
get availableChannels() {
- return this.chat.publicChannels.rejectBy("id", this.sourceChannel.id);
+ return this.chatChannelsManager.publicMessageChannels.rejectBy(
+ "id",
+ this.sourceChannel.id
+ );
}
@action
moveMessages() {
- return ajax(
- `/chat/${this.sourceChannel.id}/move_messages_to_channel.json`,
- {
- method: "PUT",
- data: {
- message_ids: this.selectedMessageIds,
- destination_channel_id: this.destinationChannelId,
- },
- }
- )
+ return this.chatApi
+ .moveChannelMessages(this.sourceChannel.id, {
+ message_ids: this.selectedMessageIds,
+ destination_channel_id: this.destinationChannelId,
+ })
.then((response) => {
- this.router.transitionTo(
- "chat.channel",
+ return this.chat.openChannelAtMessage(
response.destination_channel_id,
- response.destination_channel_title,
- {
- queryParams: { messageId: response.first_moved_message_id },
- }
+ response.first_moved_message_id
);
})
.catch(popupAjaxError);
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
index f93d4213caf..db545f463f6 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
@@ -27,6 +27,7 @@
(if this.selectingMessages "selecting-messages")
}}
data-id={{or this.message.id this.message.stagedId}}
+ data-staged-id={{if this.message.staged this.message.stagedId}}
>
{{#if this.show}}
{{#if this.selectingMessages}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
index 867610bdf31..8f82d105015 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
@@ -43,6 +43,7 @@ export default Component.extend({
onHoverMessage: null,
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
+ chatChannelsManager: service("chat-channels-manager"),
adminTools: optionalService(),
_hasSubscribedToAppEvents: false,
tagName: "",
@@ -589,13 +590,11 @@ export default Component.extend({
// so we will fully refresh if we were not members of the channel
// already
if (!this.chatChannel.isFollowing || this.chatChannel.isDraft) {
- this.chat.forceRefreshChannels().then(() => {
- return this.chat
- .getChannelBy("id", this.chatChannel.id)
- .then((reactedChannel) => {
- this.onSwitchChannel(reactedChannel);
- });
- });
+ return this.chatChannelsManager
+ .getChannel(this.chatChannel.id)
+ .then((reactedChannel) => {
+ this.onSwitchChannel(reactedChannel);
+ });
}
});
},
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js
index e7ae0190ffc..eb44133b1f8 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js
@@ -3,9 +3,9 @@ import { htmlSafe } from "@ember/template";
import discourseComputed from "discourse-common/utils/decorators";
import { alias, equal } from "@ember/object/computed";
-export const NEW_TOPIC_SELECTION = "newTopic";
-export const EXISTING_TOPIC_SELECTION = "existingTopic";
-export const NEW_MESSAGE_SELECTION = "newMessage";
+export const NEW_TOPIC_SELECTION = "new_topic";
+export const EXISTING_TOPIC_SELECTION = "existing_topic";
+export const NEW_MESSAGE_SELECTION = "new_message";
export default Component.extend({
newTopicSelection: NEW_TOPIC_SELECTION,
diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
index 76edf0f21ee..a736c4e9011 100644
--- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
+++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
@@ -10,9 +10,6 @@ export default Component.extend({
init() {
this._super(...arguments);
-
- this.appEvents.on("chat:refresh-channels", this, "refreshModel");
- this.appEvents.on("chat:refresh-channel", this, "_refreshChannel");
},
didInsertElement() {
@@ -25,8 +22,6 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
- this.appEvents.off("chat:refresh-channels", this, "refreshModel");
- this.appEvents.off("chat:refresh-channel", this, "_refreshChannel");
document.removeEventListener("keydown", this._autoFocusChatComposer);
},
@@ -77,12 +72,6 @@ export default Component.extend({
}
},
- _refreshChannel(channelId) {
- if (this.chat.activeChannel?.id === channelId) {
- this.refreshModel(true);
- }
- },
-
@action
navigateToIndex() {
this.router.transitionTo("chat.index");
diff --git a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs
index a8b2d2c538c..00751d14915 100644
--- a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs
@@ -1,4 +1,4 @@
-{{#if this.channel.isFollowing}}
+{{#if @channel.currentUserMembership.following}}
{
this.onToggle?.();
})
@@ -69,16 +60,16 @@ export default class ToggleChannelMembershipButton extends Component {
return;
}
- this.set("isLoading", false);
+ this.isLoading = false;
});
}
@action
onLeaveChannel() {
- this.set("isLoading", true);
+ this.isLoading = true;
return this.chat
- .unfollowChannel(this.channel)
+ .unfollowChannel(this.args.channel)
.then(() => {
this.onToggle?.();
})
@@ -88,7 +79,7 @@ export default class ToggleChannelMembershipButton extends Component {
return;
}
- this.set("isLoading", false);
+ this.isLoading = false;
});
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
index 0efdb70bbf1..85e834963ae 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
@@ -1,11 +1,12 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
+import { inject as service } from "@ember/service";
export default class ChatChannelEditDescriptionController extends Controller.extend(
ModalFunctionality
) {
+ @service chatApi;
editedDescription = "";
@computed("model.description", "editedDescription")
@@ -27,11 +28,12 @@ export default class ChatChannelEditDescriptionController extends Controller.ext
@action
onSaveChatChannelDescription() {
- return ChatApi.modifyChatChannel(this.model.id, {
- description: this.editedDescription,
- })
- .then((chatChannel) => {
- this.model.set("description", chatChannel.description);
+ return this.chatApi
+ .updateChannel(this.model.id, {
+ description: this.editedDescription,
+ })
+ .then((result) => {
+ this.model.set("description", result.channel.description);
this.send("closeModal");
})
.catch((event) => {
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js
index 9eb3dbde1f5..d57ad3f6ce6 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js
@@ -1,11 +1,11 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
-
+import { inject as service } from "@ember/service";
export default class ChatChannelEditTitleController extends Controller.extend(
ModalFunctionality
) {
+ @service chatApi;
editedTitle = "";
@computed("model.title", "editedTitle")
@@ -27,11 +27,12 @@ export default class ChatChannelEditTitleController extends Controller.extend(
@action
onSaveChatChannelTitle() {
- return ChatApi.modifyChatChannel(this.model.id, {
- name: this.editedTitle,
- })
- .then((chatChannel) => {
- this.model.set("title", chatChannel.title);
+ return this.chatApi
+ .updateChannel(this.model.id, {
+ name: this.editedTitle,
+ })
+ .then((result) => {
+ this.model.set("title", result.channel.title);
this.send("closeModal");
})
.catch((event) => {
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js
index c7976a24ff7..d33ec8fd222 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js
@@ -8,13 +8,13 @@ export default class ChatChannelInfoAboutController extends Controller.extend(
) {
@action
onEditChatChannelTitle() {
- showModal("chat-channel-edit-title", { model: this.model?.chatChannel });
+ showModal("chat-channel-edit-title", { model: this.model });
}
@action
onEditChatChannelDescription() {
showModal("chat-channel-edit-description", {
- model: this.model?.chatChannel,
+ model: this.model,
});
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js
index 720e6f635f3..65c132080d9 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js
@@ -1,7 +1,7 @@
import Controller from "@ember/controller";
-import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { reads } from "@ember/object/computed";
+import { computed } from "@ember/object";
export default class ChatChannelInfoIndexController extends Controller {
@service router;
@@ -10,28 +10,25 @@ export default class ChatChannelInfoIndexController extends Controller {
@reads("router.currentRoute.localName") tab;
- @computed("model.chatChannel.{membershipsCount,status}")
+ @computed("model.{membershipsCount,status,currentUserMembership.following}")
get tabs() {
const tabs = [];
- if (!this.model.chatChannel.isDirectMessageChannel) {
+ if (!this.model.isDirectMessageChannel) {
tabs.push("about");
}
- if (
- this.model.chatChannel.isOpen &&
- this.model.chatChannel.membershipsCount >= 1
- ) {
+ if (this.model.isOpen && this.model.membershipsCount >= 1) {
tabs.push("members");
}
- tabs.push("settings");
+ if (
+ this.currentUser?.staff ||
+ this.model.currentUserMembership?.following
+ ) {
+ tabs.push("settings");
+ }
return tabs;
}
-
- @action
- switchChannel(channel) {
- return this.chat.openChannel(channel);
- }
}
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
index 00b9b8e0aef..ae2ed12f483 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
@@ -1,10 +1,7 @@
import { escapeExpression } from "discourse/lib/utilities";
import Controller from "@ember/controller";
-import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
-import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import { ajax } from "discourse/lib/ajax";
import { action, computed } from "@ember/object";
import { gt, notEmpty } from "@ember/object/computed";
import { inject as service } from "@ember/service";
@@ -23,6 +20,8 @@ export default class CreateChannelController extends Controller.extend(
) {
@service chat;
@service dialog;
+ @service chatChannelsManager;
+ @service chatApi;
category = null;
categoryId = null;
@@ -57,20 +56,18 @@ export default class CreateChannelController extends Controller.extend(
_createChannel() {
const data = {
- id: this.categoryId,
+ chatable_id: this.categoryId,
name: this.name,
description: this.description,
auto_join_users: this.autoJoinUsers,
};
- return ajax("/chat/chat_channels", { method: "PUT", data })
- .then((response) => {
- const chatChannel = ChatChannel.create(response.chat_channel);
-
- return this.chat.startTrackingChannel(chatChannel).then(() => {
- this.send("closeModal");
- this.chat.openChannel(chatChannel);
- });
+ return this.chatApi
+ .createChannel(data)
+ .then((channel) => {
+ this.send("closeModal");
+ this.chatChannelsManager.follow(channel);
+ this.chat.openChannel(channel);
})
.catch((e) => {
this.flash(e.jqXHR.responseJSON.errors[0], "error");
@@ -117,24 +114,26 @@ export default class CreateChannelController extends Controller.extend(
if (category) {
const fullSlug = this._buildCategorySlug(category);
- return ChatApi.categoryPermissions(category.id).then((catPermissions) => {
- this._updateAutoJoinConfirmWarning(category, catPermissions);
- const allowedGroups = catPermissions.allowed_groups;
- const translationKey =
- allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups";
+ return this.chatApi
+ .categoryPermissions(category.id)
+ .then((catPermissions) => {
+ this._updateAutoJoinConfirmWarning(category, catPermissions);
+ const allowedGroups = catPermissions.allowed_groups;
+ const translationKey =
+ allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups";
- this.set(
- "categoryPermissionsHint",
- htmlSafe(
- I18n.t(`chat.create_channel.choose_category.${translationKey}`, {
- link: `/c/${escapeExpression(fullSlug)}/edit/security`,
- hint: escapeExpression(allowedGroups[0]),
- hint_2: escapeExpression(allowedGroups[1]),
- count: allowedGroups.length,
- })
- )
- );
- });
+ this.set(
+ "categoryPermissionsHint",
+ htmlSafe(
+ I18n.t(`chat.create_channel.choose_category.${translationKey}`, {
+ link: `/c/${escapeExpression(fullSlug)}/edit/security`,
+ hint: escapeExpression(allowedGroups[0]),
+ hint_2: escapeExpression(allowedGroups[1]),
+ count: allowedGroups.length,
+ })
+ )
+ );
+ });
} else {
this.set("categoryPermissionsHint", DEFAULT_HINT);
this.set("autoJoinWarning", "");
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
index ee754a1e75e..c892853691c 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
@@ -12,15 +12,13 @@ export default {
name: "chat-setup",
initialize(container) {
this.chatService = container.lookup("service:chat");
-
- if (!this.chatService.userCanChat) {
- return;
- }
-
this.siteSettings = container.lookup("service:site-settings");
this.appEvents = container.lookup("service:appEvents");
this.appEvents.on("discourse:focus-changed", this, "_handleFocusChanged");
+ if (!this.chatService.userCanChat) {
+ return;
+ }
withPluginApi("0.12.1", (api) => {
api.registerChatComposerButton({
id: "chat-upload-btn",
@@ -99,8 +97,6 @@ export default {
const currentUser = api.getCurrentUser();
if (currentUser?.chat_channels) {
this.chatService.setupWithPreloadedChannels(currentUser.chat_channels);
- } else {
- this.chatService.setupWithoutPreloadedChannels();
}
const chatNotificationManager = container.lookup(
@@ -115,19 +111,7 @@ export default {
api.addCardClickListenerSelector(".chat-drawer-outlet");
- api.dispatchWidgetAppEvent(
- "site-header",
- "header-chat-link",
- "chat:rerender-header"
- );
-
- api.dispatchWidgetAppEvent(
- "sidebar-header",
- "header-chat-link",
- "chat:rerender-header"
- );
-
- api.addToHeaderIcons("header-chat-link");
+ api.addToHeaderIcons("chat-header-icon");
api.decorateChatMessage(function (chatMessage, chatChannel) {
if (!this.currentUser) {
@@ -155,17 +139,22 @@ export default {
},
teardown() {
+ this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged");
+
if (!this.chatService.userCanChat) {
return;
}
- this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged");
_lastForcedRefreshAt = null;
clearChatComposerButtons();
},
@bind
_handleFocusChanged(hasFocus) {
+ if (!this.chatService.userCanChat) {
+ return;
+ }
+
if (!hasFocus) {
_lastForcedRefreshAt = Date.now();
return;
@@ -179,6 +168,5 @@ export default {
}
_lastForcedRefreshAt = Date.now();
- this.chatService.refreshTrackingState();
},
};
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
index e7c417e8e1e..547632c6c77 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
@@ -25,41 +25,12 @@ export default {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
const SidebarChatChannelsSectionLink = class extends BaseCustomSidebarSectionLink {
- @tracked chatChannelTrackingState =
- this.chatService.currentUser.chat_channel_tracking_state[
- this.channel.id
- ];
-
constructor({ channel, chatService }) {
super(...arguments);
this.channel = channel;
this.chatService = chatService;
}
- @bind
- willDestroy() {
- this.chatService.appEvents.off(
- "chat:user-tracking-state-changed",
- this._refreshTrackingState
- );
- }
-
- @bind
- didInsert() {
- this.chatService.appEvents.on(
- "chat:user-tracking-state-changed",
- this._refreshTrackingState
- );
- }
-
- @bind
- _refreshTrackingState() {
- this.chatChannelTrackingState =
- this.chatService.currentUser.chat_channel_tracking_state[
- this.channel.id
- ];
- }
-
get name() {
return dasherize(slugifyChannel(this.channel));
}
@@ -68,7 +39,7 @@ export default {
get classNames() {
const classes = [];
- if (this.channel.current_user_membership.muted) {
+ if (this.channel.currentUserMembership.muted) {
classes.push("sidebar-section-link--muted");
}
@@ -76,6 +47,8 @@ export default {
classes.push("sidebar-section-link--active");
}
+ classes.push(`channel-${this.channel.id}`);
+
return classes.join(" ");
}
@@ -118,26 +91,19 @@ export default {
}
get suffixValue() {
- return this.chatChannelTrackingState?.unread_count > 0
+ return this.channel.currentUserMembership.unread_count > 0
? "circle"
: "";
}
get suffixCSSClass() {
- return this.chatChannelTrackingState?.unread_mentions > 0
+ return this.channel.currentUserMembership.unread_mentions > 0
? "urgent"
: "unread";
}
};
const SidebarChatChannelsSection = class extends BaseCustomSidebarSection {
- @tracked sectionLinks = [];
-
- @tracked sectionIndicator =
- this.chatService.publicChannels &&
- this.chatService.publicChannels[0].current_user_membership
- .unread_count;
-
@tracked currentUserCanJoinPublicChannels =
this.sidebar.currentUser &&
(this.sidebar.currentUser.staff ||
@@ -150,37 +116,20 @@ export default {
return;
}
this.chatService = container.lookup("service:chat");
- this.router = container.lookup("service:router");
- this.appEvents = container.lookup("service:app-events");
- this.appEvents.on("chat:refresh-channels", this._refreshChannels);
- this._refreshChannels();
- }
-
- @bind
- willDestroy() {
- if (!this.appEvents) {
- return;
- }
- this.appEvents.off(
- "chat:refresh-channels",
- this._refreshChannels
+ this.chatChannelsManager = container.lookup(
+ "service:chat-channels-manager"
);
+ this.router = container.lookup("service:router");
}
- @bind
- _refreshChannels() {
- const newSectionLinks = [];
- this.chatService.getChannels().then((channels) => {
- channels.publicChannels.forEach((channel) => {
- newSectionLinks.push(
- new SidebarChatChannelsSectionLink({
- channel,
- chatService: this.chatService,
- })
- );
- });
- this.sectionLinks = newSectionLinks;
- });
+ get sectionLinks() {
+ return this.chatChannelsManager.publicMessageChannels.map(
+ (channel) =>
+ new SidebarChatChannelsSectionLink({
+ channel,
+ chatService: this.chatService,
+ })
+ );
}
get name() {
@@ -228,11 +177,6 @@ export default {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink {
- @tracked chatChannelTrackingState =
- this.chatService.currentUser.chat_channel_tracking_state[
- this.channel.id
- ];
-
constructor({ channel, chatService }) {
super(...arguments);
this.channel = channel;
@@ -258,7 +202,7 @@ export default {
get classNames() {
const classes = [];
- if (this.channel.current_user_membership.muted) {
+ if (this.channel.currentUserMembership.muted) {
classes.push("sidebar-section-link--muted");
}
@@ -266,6 +210,8 @@ export default {
classes.push("sidebar-section-link--active");
}
+ classes.push(`channel-${this.channel.id}`);
+
return classes.join(" ");
}
@@ -340,7 +286,7 @@ export default {
}
get suffixValue() {
- return this.chatChannelTrackingState?.unread_count > 0
+ return this.channel.currentUserMembership.unread_count > 0
? "circle"
: "";
}
@@ -396,7 +342,6 @@ export default {
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
@service site;
@service router;
- @tracked sectionLinks = [];
@tracked userCanDirectMessage =
this.chatService.userCanDirectMessage;
@@ -407,40 +352,19 @@ export default {
return;
}
this.chatService = container.lookup("service:chat");
- this.chatService.appEvents.on(
- "chat:user-tracking-state-changed",
- this._refreshDirectMessageChannels
- );
- this._refreshDirectMessageChannels();
- }
-
- @bind
- willDestroy() {
- if (container.isDestroyed) {
- return;
- }
- this.chatService.appEvents.off(
- "chat:user-tracking-state-changed",
- this._refreshDirectMessageChannels
+ this.chatChannelsManager = container.lookup(
+ "service:chat-channels-manager"
);
}
- @bind
- _refreshDirectMessageChannels() {
- const newSectionLinks = [];
- this.chatService.getChannels().then((channels) => {
- this.chatService
- .truncateDirectMessageChannels(channels.directMessageChannels)
- .forEach((channel) => {
- newSectionLinks.push(
- new SidebarChatDirectMessagesSectionLink({
- channel,
- chatService: this.chatService,
- })
- );
- });
- this.sectionLinks = newSectionLinks;
- });
+ get sectionLinks() {
+ return this.chatChannelsManager.truncatedDirectMessageChannels.map(
+ (channel) =>
+ new SidebarChatDirectMessagesSectionLink({
+ channel,
+ chatService: this.chatService,
+ })
+ );
}
get name() {
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js b/plugins/chat/assets/javascripts/discourse/lib/chat-api.js
deleted file mode 100644
index 3b373570a72..00000000000
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { ajax } from "discourse/lib/ajax";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
-export default class ChatApi {
- static async chatChannelMemberships(channelId, data) {
- return await ajax(`/chat/api/chat_channels/${channelId}/memberships.json`, {
- data,
- }).catch(popupAjaxError);
- }
-
- static async updateChatChannelNotificationsSettings(channelId, data = {}) {
- return await ajax(
- `/chat/api/chat_channels/${channelId}/notifications_settings.json`,
- {
- method: "PUT",
- data,
- }
- ).catch(popupAjaxError);
- }
-
- static async sendMessage(channelId, data = {}) {
- return ajax(`/chat/${channelId}.json`, {
- ignoreUnsent: false,
- method: "POST",
- data,
- });
- }
-
- static async chatChannels(data = {}) {
- if (data?.status === "all") {
- delete data.status;
- }
-
- return await ajax(`/chat/api/chat_channels.json`, {
- method: "GET",
- data,
- })
- .then((channels) =>
- channels.map((channel) => ChatChannel.create(channel))
- )
- .catch(popupAjaxError);
- }
-
- static async modifyChatChannel(channelId, data) {
- return await this._performRequest(
- `/chat/api/chat_channels/${channelId}.json`,
- {
- method: "PUT",
- data,
- }
- );
- }
-
- static async unfollowChatChannel(channel) {
- return await this._performRequest(
- `/chat/chat_channels/${channel.id}/unfollow.json`,
- {
- method: "POST",
- }
- ).then((updatedChannel) => {
- channel.updateMembership(updatedChannel.current_user_membership);
-
- // doesn't matter if this is inaccurate, it will be eventually consistent
- // via the channel-metadata MessageBus channel
- channel.set("memberships_count", channel.memberships_count - 1);
- return channel;
- });
- }
-
- static async followChatChannel(channel) {
- return await this._performRequest(
- `/chat/chat_channels/${channel.id}/follow.json`,
- {
- method: "POST",
- }
- ).then((updatedChannel) => {
- channel.updateMembership(updatedChannel.current_user_membership);
-
- // doesn't matter if this is inaccurate, it will be eventually consistent
- // via the channel-metadata MessageBus channel
- channel.set("memberships_count", channel.memberships_count + 1);
- return channel;
- });
- }
-
- static async categoryPermissions(categoryId) {
- return await this._performRequest(
- `/chat/api/category-chatables/${categoryId}/permissions.json`
- );
- }
-
- static async _performRequest(...args) {
- return await ajax(...args).catch(popupAjaxError);
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
index 7dca63e974b..182b01a1ec3 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
@@ -1,15 +1,16 @@
import RestModel from "discourse/models/rest";
import I18n from "I18n";
-import { computed } from "@ember/object";
import User from "discourse/models/user";
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
import { ajax } from "discourse/lib/ajax";
import { escapeExpression } from "discourse/lib/utilities";
+import { tracked } from "@glimmer/tracking";
export const CHATABLE_TYPES = {
directMessageChannel: "DirectMessage",
categoryChannel: "Category",
};
+
export const CHANNEL_STATUSES = {
open: "open",
readOnly: "read_only",
@@ -38,13 +39,10 @@ export function channelStatusIcon(channelStatus) {
switch (channelStatus) {
case CHANNEL_STATUSES.closed:
return "lock";
- break;
case CHANNEL_STATUSES.readOnly:
return "comment-slash";
- break;
case CHANNEL_STATUSES.archived:
return "archive";
- break;
}
}
@@ -60,62 +58,51 @@ const READONLY_STATUSES = [
];
export default class ChatChannel extends RestModel {
- isDraft = false;
- lastSendReadMessageId = null;
+ @tracked currentUserMembership = null;
+ @tracked isDraft = false;
+ @tracked title;
+ @tracked description;
+ @tracked chatableType;
+ @tracked status;
- @computed("title")
get escapedTitle() {
return escapeExpression(this.title);
}
- @computed("description")
get escapedDescription() {
return escapeExpression(this.description);
}
- @computed("chatable_type")
get isDirectMessageChannel() {
return this.chatable_type === CHATABLE_TYPES.directMessageChannel;
}
- @computed("chatable_type")
get isCategoryChannel() {
return this.chatable_type === CHATABLE_TYPES.categoryChannel;
}
- @computed("status")
get isOpen() {
return !this.status || this.status === CHANNEL_STATUSES.open;
}
- @computed("status")
get isReadOnly() {
return this.status === CHANNEL_STATUSES.readOnly;
}
- @computed("status")
get isClosed() {
return this.status === CHANNEL_STATUSES.closed;
}
- @computed("status")
get isArchived() {
return this.status === CHANNEL_STATUSES.archived;
}
- @computed("isArchived", "isOpen")
get isJoinable() {
return this.isOpen && !this.isArchived;
}
- @computed("memberships_count")
- get membershipsCount() {
- return this.memberships_count;
- }
-
- @computed("current_user_membership.following")
get isFollowing() {
- return this.current_user_membership.following;
+ return this.currentUserMembership.following;
}
canModifyMessages(user) {
@@ -127,12 +114,12 @@ export default class ChatChannel extends RestModel {
}
updateMembership(membership) {
- this.current_user_membership.setProperties({
- following: membership.following,
- muted: membership.muted,
- desktop_notification_level: membership.desktop_notification_level,
- mobile_notification_level: membership.mobile_notification_level,
- });
+ this.currentUserMembership.following = membership.following;
+ this.currentUserMembership.muted = membership.muted;
+ this.currentUserMembership.desktop_notification_level =
+ membership.desktop_notification_level;
+ this.currentUserMembership.mobile_notification_level =
+ membership.mobile_notification_level;
}
updateLastReadMessage(messageId) {
@@ -143,7 +130,7 @@ export default class ChatChannel extends RestModel {
return ajax(`/chat/${this.id}/read/${messageId}.json`, {
method: "PUT",
}).then(() => {
- this.set("lastSendReadMessageId", messageId);
+ this.currentUserMembership.last_read_message_id = messageId;
});
}
}
@@ -151,11 +138,12 @@ export default class ChatChannel extends RestModel {
ChatChannel.reopenClass({
create(args) {
args = args || {};
+
this._initUserModels(args);
this._initUserMembership(args);
- args.lastSendReadMessageId =
- args.current_user_membership?.last_read_message_id;
+ args.chatableType = args.chatable_type;
+ args.membershipsCount = args.memberships_count;
return this._super(args);
},
@@ -170,11 +158,11 @@ ChatChannel.reopenClass({
},
_initUserMembership(args) {
- if (args.current_user_membership instanceof UserChatChannelMembership) {
+ if (args.currentUserMembership instanceof UserChatChannelMembership) {
return;
}
- args.current_user_membership = UserChatChannelMembership.create(
+ args.currentUserMembership = UserChatChannelMembership.create(
args.current_user_membership || {
following: false,
muted: false,
@@ -182,6 +170,8 @@ ChatChannel.reopenClass({
unread_mentions: 0,
}
);
+
+ delete args.current_user_membership;
},
});
diff --git a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js
index 8e97e26bfe2..9d732e82fc4 100644
--- a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js
+++ b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js
@@ -1,3 +1,30 @@
import RestModel from "discourse/models/rest";
+import { tracked } from "@glimmer/tracking";
+import User from "discourse/models/user";
+export default class UserChatChannelMembership extends RestModel {
+ @tracked following = false;
+ @tracked muted = false;
+ @tracked unread_count = 0;
+ @tracked unread_mentions = 0;
+ @tracked chat_message_id = null;
+ @tracked chat_channel_id = null;
+ @tracked desktop_notification_level = null;
+ @tracked mobile_notification_level = null;
+ @tracked last_read_message_id = null;
+}
-export default class UserChatChannelMembership extends RestModel {}
+UserChatChannelMembership.reopenClass({
+ create(args) {
+ args = args || {};
+ this._initUser(args);
+ return this._super(args);
+ },
+
+ _initUser(args) {
+ if (args.user instanceof User) {
+ return;
+ }
+
+ args.user = User.create(args.user);
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js
new file mode 100644
index 00000000000..7fc075e2cee
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js
@@ -0,0 +1,9 @@
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default class ChatBrowseIndexRoute extends DiscourseRoute {
+ afterModel() {
+ if (!this.siteSettings.chat_allow_archiving_channels) {
+ this.replaceWith("chat.browse");
+ }
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js
index 7516fe58bb1..ba6c91c245e 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js
@@ -13,8 +13,8 @@ export default class ChatChannelByNameRoute extends DiscourseRoute {
.then((response) => {
this.transitionTo(
"chat.channel",
- response.chat_channel.id,
- response.chat_channel.title
+ response.channel.id,
+ response.channel.title
);
})
.catch(() => this.replaceWith("/404"));
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js
index 181c9ffb690..a92d5e882f0 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js
@@ -2,7 +2,7 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class ChatChannelInfoAboutRoute extends DiscourseRoute {
afterModel(model) {
- if (model.chatChannel.isDirectMessageChannel) {
+ if (model.isDirectMessageChannel) {
this.replaceWith("chat.channel.info.index");
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js
index ffc3bc589b4..c7bfd1c0905 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js
@@ -2,8 +2,8 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class ChatChannelInfoIndexRoute extends DiscourseRoute {
afterModel(model) {
- if (model.chatChannel.isDirectMessageChannel) {
- if (model.chatChannel.isOpen && model.chatChannel.membershipsCount >= 1) {
+ if (model.isDirectMessageChannel) {
+ if (model.isOpen && model.membershipsCount >= 1) {
this.replaceWith("chat.channel.info.members");
} else {
this.replaceWith("chat.channel.info.settings");
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js
index 25d2e4eb93a..d3fba6f97de 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js
@@ -2,8 +2,12 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class ChatChannelInfoMembersRoute extends DiscourseRoute {
afterModel(model) {
- if (!model.chatChannel.isOpen) {
- this.replaceWith("chat.channel.info.settings");
+ if (!model.isOpen) {
+ return this.replaceWith("chat.channel.info.settings");
+ }
+
+ if (model.membershipsCount < 1) {
+ return this.replaceWith("chat.channel.info");
}
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js
new file mode 100644
index 00000000000..61635238534
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js
@@ -0,0 +1,9 @@
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default class ChatChannelInfoSettingsRoute extends DiscourseRoute {
+ afterModel(model) {
+ if (!this.currentUser?.staff && !model.currentUserMembership?.following) {
+ this.replaceWith("chat.channel.info");
+ }
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
index 17e6a2f7456..7e49d1f764e 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
@@ -1,48 +1,23 @@
import DiscourseRoute from "discourse/routes/discourse";
-import Promise from "rsvp";
-import EmberObject, { action } from "@ember/object";
-import { ajax } from "discourse/lib/ajax";
import { inject as service } from "@ember/service";
-import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
export default class ChatChannelRoute extends DiscourseRoute {
@service chat;
@service router;
+ @service chatChannelsManager;
async model(params) {
- let [chatChannel, channels] = await Promise.all([
- this.getChannel(params.channelId),
- this.chat.getChannels(),
- ]);
-
- return EmberObject.create({
- chatChannel,
- channels,
- });
- }
-
- async getChannel(id) {
- let channel = await this.chat.getChannelBy("id", id);
- if (!channel || this.forceRefetchChannel) {
- channel = await this.getChannelFromServer(id);
- }
- return channel;
- }
-
- async getChannelFromServer(id) {
- return ajax(`/chat/chat_channels/${id}`)
- .then((response) => ChatChannel.create(response))
- .catch(() => this.replaceWith("/404"));
+ return this.chatChannelsManager.find(params.channelId);
}
afterModel(model) {
- this.chat.setActiveChannel(model?.chatChannel);
+ this.chat.setActiveChannel(model);
const queryParams = this.paramsFor(this.routeName);
- const slug = slugifyChannel(model.chatChannel);
+ const slug = slugifyChannel(model);
if (queryParams?.channelTitle !== slug) {
- this.router.replaceWith("chat.channel.index", model.chatChannel.id, slug);
+ this.router.replaceWith("chat.channel.index", model.id, slug);
}
}
@@ -54,10 +29,4 @@ export default class ChatChannelRoute extends DiscourseRoute {
this.controller.set("messageId", null);
}
}
-
- @action
- refreshModel(forceRefetchChannel = false) {
- this.forceRefetchChannel = forceRefetchChannel;
- this.refresh();
- }
}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js
index fbf8cd0a3cb..027f21b4672 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js
@@ -3,36 +3,29 @@ import { inject as service } from "@ember/service";
export default class ChatIndexRoute extends DiscourseRoute {
@service chat;
+ @service chatChannelsManager;
@service router;
redirect() {
+ // Always want the channel index on mobile.
if (this.site.mobileView) {
- return; // Always want the channel index on mobile.
+ return;
}
- // We are on desktop. Check for a channel to enter and transition if so.
- // Otherwise, `setupController` will fetch all available
- return this.chat.getIdealFirstChannelIdAndTitle().then((channelInfo) => {
- if (channelInfo) {
- return this.chat.getChannelBy("id", channelInfo.id).then((c) => {
- return this.chat.openChannel(c);
- });
- } else {
- return this.router.transitionTo("chat.browse");
- }
- });
+ // We are on desktop. Check for a channel to enter and transition if so
+ const id = this.chat.getIdealFirstChannelId();
+ if (id) {
+ return this.chatChannelsManager.find(id).then((c) => {
+ return this.chat.openChannel(c);
+ });
+ } else {
+ return this.router.transitionTo("chat.browse");
+ }
}
model() {
if (this.site.mobileView) {
- return this.chat.getChannels().then((channels) => {
- if (
- channels.publicChannels.length ||
- channels.directMessageChannels.length
- ) {
- return channels;
- }
- });
+ return this.chatChannelsManager.channels;
}
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
new file mode 100644
index 00000000000..4a24d8a9b6e
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -0,0 +1,242 @@
+import Service, { inject as service } from "@ember/service";
+import { ajax } from "discourse/lib/ajax";
+import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
+import { tracked } from "@glimmer/tracking";
+import { bind } from "discourse-common/utils/decorators";
+import { Promise } from "rsvp";
+
+class Collection {
+ @tracked items = [];
+ @tracked meta = {};
+ @tracked loading = false;
+
+ constructor(resourceURL, handler) {
+ this._resourceURL = resourceURL;
+ this._handler = handler;
+ this._fetchedAll = false;
+ }
+
+ get loadMoreURL() {
+ return this.meta.load_more_url;
+ }
+
+ get totalRows() {
+ return this.meta.total_rows;
+ }
+
+ get length() {
+ return this.items.length;
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
+ [Symbol.iterator]() {
+ let index = 0;
+
+ return {
+ next: () => {
+ if (index < this.items.length) {
+ return { value: this.items[index++], done: false };
+ } else {
+ return { done: true };
+ }
+ },
+ };
+ }
+
+ @bind
+ load(params = {}) {
+ this._fetchedAll = false;
+
+ if (this.loading) {
+ return;
+ }
+
+ this.loading = true;
+
+ const filteredQueryParams = Object.entries(params).filter(
+ ([, v]) => v !== undefined
+ );
+ const queryString = new URLSearchParams(filteredQueryParams).toString();
+
+ const endpoint = this._resourceURL + (queryString ? `?${queryString}` : "");
+ return this.#fetch(endpoint)
+ .then((result) => {
+ this.items = this._handler(result);
+ this.meta = result.meta;
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ }
+
+ @bind
+ loadMore() {
+ if (this.loading) {
+ return;
+ }
+
+ if (
+ this._fetchedAll ||
+ (this.totalRows && this.items.length >= this.totalRows)
+ ) {
+ return;
+ }
+
+ let promise;
+
+ this.loading = true;
+
+ if (this.loadMoreURL) {
+ promise = this.#fetch(this.loadMoreURL).then((result) => {
+ const newItems = this._handler(result);
+
+ if (newItems.length) {
+ this.items = this.items.concat(newItems);
+ } else {
+ this._fetchedAll = true;
+ }
+ this.meta = result.meta;
+ });
+ } else {
+ promise = Promise.resolve();
+ }
+
+ return promise.finally(() => {
+ this.loading = false;
+ });
+ }
+
+ #fetch(url) {
+ return ajax(url, { type: "GET" });
+ }
+}
+
+export default class ChatApi extends Service {
+ @service chatChannelsManager;
+
+ getChannel(channelId) {
+ return this.#getRequest(`/channels/${channelId}`).then((result) =>
+ this.chatChannelsManager.store(result.channel)
+ );
+ }
+
+ channels() {
+ return new Collection(`${this.#basePath}/channels`, (response) => {
+ return response.channels.map((channel) =>
+ this.chatChannelsManager.store(channel)
+ );
+ });
+ }
+
+ moveChannelMessages(channelId, data = {}) {
+ return this.#postRequest(`/channels/${channelId}/messages/moves`, {
+ move: data,
+ });
+ }
+
+ destroyChannel(channelId, data = {}) {
+ return this.#deleteRequest(`/channels/${channelId}`, { channel: data });
+ }
+
+ createChannel(data = {}) {
+ return this.#postRequest("/channels", { channel: data }).then((response) =>
+ this.chatChannelsManager.store(response.channel)
+ );
+ }
+
+ categoryPermissions(categoryId) {
+ return ajax(`/chat/api/category-chatables/${categoryId}/permissions`);
+ }
+
+ sendMessage(channelId, data = {}) {
+ return ajax(`/chat/${channelId}`, {
+ ignoreUnsent: false,
+ type: "POST",
+ data,
+ });
+ }
+
+ createChannelArchive(channelId, data = {}) {
+ return this.#postRequest(`/channels/${channelId}/archives`, {
+ archive: data,
+ });
+ }
+
+ updateChannel(channelId, data = {}) {
+ return this.#putRequest(`/channels/${channelId}`, { channel: data });
+ }
+
+ updateChannelStatus(channelId, status) {
+ return this.#putRequest(`/channels/${channelId}/status`, { status });
+ }
+
+ listChannelMemberships(channelId) {
+ return new Collection(
+ `${this.#basePath}/channels/${channelId}/memberships`,
+ (response) => {
+ return response.memberships.map((membership) =>
+ UserChatChannelMembership.create(membership)
+ );
+ }
+ );
+ }
+
+ listCurrentUserChannels() {
+ return this.#getRequest(`/channels/me`).then((result) => {
+ return (result?.channels || []).map((channel) =>
+ this.chatChannelsManager.store(channel)
+ );
+ });
+ }
+
+ followChannel(channelId) {
+ return this.#postRequest(`/channels/${channelId}/memberships/me`).then(
+ (result) => UserChatChannelMembership.create(result.membership)
+ );
+ }
+
+ unfollowChannel(channelId) {
+ return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then(
+ (result) => UserChatChannelMembership.create(result.membership)
+ );
+ }
+
+ updateCurrentUserChatChannelNotificationsSettings(channelId, data = {}) {
+ return this.#putRequest(
+ `/channels/${channelId}/notifications-settings/me`,
+ { notifications_settings: data }
+ );
+ }
+
+ get #basePath() {
+ return "/chat/api";
+ }
+
+ #getRequest(endpoint, data = {}) {
+ return ajax(`${this.#basePath}/${endpoint}`, {
+ type: "GET",
+ data,
+ });
+ }
+
+ #putRequest(endpoint, data = {}) {
+ return ajax(`${this.#basePath}/${endpoint}`, {
+ type: "PUT",
+ data,
+ });
+ }
+
+ #postRequest(endpoint, data = {}) {
+ return ajax(`${this.#basePath}/${endpoint}`, {
+ type: "POST",
+ data,
+ });
+ }
+
+ #deleteRequest(endpoint, data = {}) {
+ return ajax(`${this.#basePath}/${endpoint}`, {
+ type: "DELETE",
+ data,
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
new file mode 100644
index 00000000000..4ba6ab2365b
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
@@ -0,0 +1,136 @@
+import Service, { inject as service } from "@ember/service";
+import Promise from "rsvp";
+import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import { tracked } from "@glimmer/tracking";
+import { TrackedObject } from "@ember-compat/tracked-built-ins";
+
+const DIRECT_MESSAGE_CHANNELS_LIMIT = 20;
+
+export default class ChatChannelsManager extends Service {
+ @service chatSubscriptionsManager;
+ @service chatApi;
+ @service currentUser;
+ @tracked _cached = new TrackedObject();
+
+ get channels() {
+ return Object.values(this._cached);
+ }
+
+ async find(id) {
+ const existingChannel = this.#findStale(id);
+ if (existingChannel) {
+ return Promise.resolve(existingChannel);
+ } else {
+ return this.#find(id);
+ }
+ }
+
+ store(channelObject) {
+ let model = this.#findStale(channelObject.id);
+
+ if (!model) {
+ model = ChatChannel.create(channelObject);
+ this.#cache(model);
+ }
+
+ return model;
+ }
+
+ async follow(model) {
+ this.chatSubscriptionsManager.startChannelSubscription(model);
+
+ if (!model.currentUserMembership.following) {
+ return this.chatApi.followChannel(model.id).then((membership) => {
+ model.currentUserMembership.following = membership.following;
+ model.currentUserMembership.muted = membership.muted;
+ model.currentUserMembership.desktop_notification_level =
+ membership.desktop_notification_level;
+ model.currentUserMembership.mobile_notification_level =
+ membership.mobile_notification_level;
+
+ return model;
+ });
+ } else {
+ return Promise.resolve(model);
+ }
+ }
+
+ async unfollow(model) {
+ this.chatSubscriptionsManager.stopChannelSubscription(model);
+
+ return this.chatApi.unfollowChannel(model.id).then((membership) => {
+ model.currentUserMembership = membership;
+
+ return model;
+ });
+ }
+
+ get unreadCount() {
+ let count = 0;
+ this.publicMessageChannels.forEach((channel) => {
+ count += channel.currentUserMembership.unread_count || 0;
+ });
+ return count;
+ }
+
+ get unreadUrgentCount() {
+ let count = 0;
+ this.channels.forEach((channel) => {
+ if (channel.isDirectMessageChannel) {
+ count += channel.currentUserMembership.unread_count || 0;
+ }
+ count += channel.currentUserMembership.unread_mentions || 0;
+ });
+ return count;
+ }
+
+ get publicMessageChannels() {
+ return this.channels.filter(
+ (channel) =>
+ channel.isCategoryChannel && channel.currentUserMembership.following
+ );
+ }
+
+ get directMessageChannels() {
+ return this.#sortDirectMessageChannels(
+ this.channels.filter((channel) => {
+ const membership = channel.currentUserMembership;
+ return channel.isDirectMessageChannel && membership.following;
+ })
+ );
+ }
+
+ get truncatedDirectMessageChannels() {
+ return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT);
+ }
+
+ async #find(id) {
+ return this.chatApi.getChannel(id).then((channel) => {
+ this.#cache(channel);
+ return channel;
+ });
+ }
+
+ #cache(channel) {
+ this._cached[channel.id] = channel;
+ }
+
+ #findStale(id) {
+ return this._cached[id];
+ }
+
+ #sortDirectMessageChannels(channels) {
+ return channels.sort((a, b) => {
+ const unreadCountA = a.currentUserMembership.unread_count || 0;
+ const unreadCountB = b.currentUserMembership.unread_count || 0;
+ if (unreadCountA === unreadCountB) {
+ return new Date(a.get("last_message_sent_at")) >
+ new Date(b.get("last_message_sent_at"))
+ ? -1
+ : 1;
+ } else {
+ return unreadCountA > unreadCountB ? -1 : 1;
+ }
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js
index b76f8420e44..bc51a26394e 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js
@@ -19,7 +19,11 @@ export default class ChatMessageVisibilityObserver extends Service {
entries.forEach((entry) => {
entry.target.dataset.visible = entry.isIntersecting;
- if (entry.isIntersecting && !isTesting()) {
+ if (
+ !entry.target.dataset.stagedId &&
+ entry.isIntersecting &&
+ !isTesting()
+ ) {
this.chat.updateLastReadMessage();
}
});
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js
index 12df7f6382e..ee79bb20bb3 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js
@@ -86,6 +86,10 @@ export default class ChatStateManager extends Service {
return this.router.currentRouteName?.startsWith("chat");
}
+ get isActive() {
+ return this.isFullPageActive || this.isDrawerActive;
+ }
+
storeAppURL(URL = null) {
this._appURL = URL || this.router.currentURL;
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js
new file mode 100644
index 00000000000..cf627de4d57
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js
@@ -0,0 +1,265 @@
+import Service, { inject as service } from "@ember/service";
+import { bind } from "discourse-common/utils/decorators";
+import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
+
+export default class ChatSubscriptionsManager extends Service {
+ @service store;
+ @service chatChannelsManager;
+ @service currentUser;
+ @service appEvents;
+
+ _channelSubscriptions = new Set();
+
+ startChannelSubscription(channel) {
+ if (
+ channel.currentUserMembership.muted ||
+ this._channelSubscriptions.has(channel.id)
+ ) {
+ return;
+ }
+
+ this._channelSubscriptions.add(channel.id);
+
+ if (!channel.isDirectMessageChannel) {
+ this._startChannelMentionsSubscription(channel);
+ }
+
+ this._startChannelNewMessagesSubscription(channel);
+ }
+
+ stopChannelSubscription(channel) {
+ this.messageBus.unsubscribe(
+ `/chat/${channel.id}/new-messages`,
+ this._onNewMessages
+ );
+ if (!channel.isDirectMessageChannel) {
+ this.messageBus.unsubscribe(
+ `/chat/${channel.id}/new-mentions`,
+ this._onNewMentions
+ );
+ }
+
+ this._channelSubscriptions.delete(channel.id);
+ }
+
+ startChannelsSubscriptions(messageBusIds) {
+ this._startNewChannelSubscription(messageBusIds.new_channel);
+ this._startUserTrackingStateSubscription(messageBusIds.user_tracking_state);
+ this._startChannelsEditsSubscription(messageBusIds.channel_edits);
+ this._startChannelsStatusChangesSubscription(messageBusIds.channel_status);
+ this._startChannelsMetadataChangesSubscription(
+ messageBusIds.channel_metadata
+ );
+ }
+
+ stopChannelsSubscriptions() {
+ this._stopNewChannelSubscription();
+ this._stopUserTrackingStateSubscription();
+ this._stopChannelsEditsSubscription();
+ this._stopChannelsStatusChangesSubscription();
+ this._stopChannelsMetadataChangesSubscription();
+
+ (this.chatChannelsManager.channels || []).forEach((channel) => {
+ this.stopChannelSubscription(channel);
+ });
+ }
+
+ _startChannelMentionsSubscription(channel) {
+ this.messageBus.subscribe(
+ `/chat/${channel.id}/new-mentions`,
+ this._onNewMentions,
+ channel.meta.message_bus_last_ids.new_mentions
+ );
+ }
+
+ @bind
+ _onNewMentions(busData) {
+ this.chatChannelsManager.find(busData.channel_id).then((channel) => {
+ const membership = channel.currentUserMembership;
+ if (membership) {
+ membership.unread_mentions = (membership.unread_mentions || 0) + 1;
+ }
+ });
+ }
+
+ _startChannelNewMessagesSubscription(channel) {
+ this.messageBus.subscribe(
+ `/chat/${channel.id}/new-messages`,
+ this._onNewMessages,
+ channel.meta.message_bus_last_ids.new_messages
+ );
+ }
+
+ @bind
+ _onNewMessages(busData) {
+ this.chatChannelsManager.find(busData.channel_id).then((channel) => {
+ if (busData.user_id === this.currentUser.id) {
+ // User sent message, update tracking state to no unread
+ channel.currentUserMembership.chat_message_id = busData.message_id;
+ } else {
+ // Ignored user sent message, update tracking state to no unread
+ if (this.currentUser.ignored_users.includes(busData.username)) {
+ channel.currentUserMembership.chat_message_id = busData.message_id;
+ } else {
+ // Message from other user. Increment trackings state
+ if (
+ busData.message_id >
+ (channel.currentUserMembership.chat_message_id || 0)
+ ) {
+ channel.currentUserMembership.unread_count =
+ channel.currentUserMembership.unread_count + 1;
+ }
+ }
+ }
+
+ channel.set("last_message_sent_at", new Date());
+ });
+ }
+
+ _startUserTrackingStateSubscription(lastId) {
+ if (!this.currentUser) {
+ return;
+ }
+
+ this.messageBus.subscribe(
+ `/chat/user-tracking-state/${this.currentUser.id}`,
+ this._onUserTrackingStateUpdate,
+ lastId
+ );
+ }
+
+ _stopUserTrackingStateSubscription() {
+ if (!this.currentUser) {
+ return;
+ }
+
+ this.messageBus.unsubscribe(
+ `/chat/user-tracking-state/${this.currentUser.id}`,
+ this._onUserTrackingStateUpdate
+ );
+ }
+
+ @bind
+ _onUserTrackingStateUpdate(data) {
+ this.chatChannelsManager.find(data.chat_channel_id).then((channel) => {
+ if (
+ channel?.currentUserMembership?.chat_message_id < data.chat_message_id
+ ) {
+ channel.currentUserMembership.chat_message_id = data.chat_message_id;
+ channel.currentUserMembership.unread_count = 0;
+ channel.currentUserMembership.unread_mentions = 0;
+ }
+ });
+ }
+
+ _startNewChannelSubscription(lastId) {
+ this.messageBus.subscribe(
+ "/chat/new-channel",
+ this._onNewChannelSubscription,
+ lastId
+ );
+ }
+
+ _stopNewChannelSubscription() {
+ this.messageBus.unsubscribe(
+ "/chat/new-channel",
+ this._onNewChannelSubscription
+ );
+ }
+
+ @bind
+ _onNewChannelSubscription(data) {
+ this.chatChannelsManager.find(data.channel.id).then((channel) => {
+ // we need to refrehs here to have correct last message ids
+ channel.meta = data.channel.meta;
+
+ if (
+ channel.isDirectMessageChannel &&
+ !channel.currentUserMembership.following
+ ) {
+ channel.currentUserMembership.unread_count = 1;
+ }
+
+ this.chatChannelsManager.follow(channel);
+ });
+ }
+
+ _startChannelsMetadataChangesSubscription(lastId) {
+ this.messageBus.subscribe(
+ "/chat/channel-metadata",
+ this._onChannelMetadata,
+ lastId
+ );
+ }
+
+ _startChannelsEditsSubscription(lastId) {
+ this.messageBus.subscribe(
+ "/chat/channel-edits",
+ this._onChannelEdits,
+ lastId
+ );
+ }
+
+ _startChannelsStatusChangesSubscription(lastId) {
+ this.messageBus.subscribe(
+ "/chat/channel-status",
+ this._onChannelStatus,
+ lastId
+ );
+ }
+
+ _stopChannelsStatusChangesSubscription() {
+ this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus);
+ }
+
+ _stopChannelsEditsSubscription() {
+ this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits);
+ }
+
+ _stopChannelsMetadataChangesSubscription() {
+ this.messageBus.unsubscribe(
+ "/chat/channel-metadata",
+ this._onChannelMetadata
+ );
+ }
+
+ @bind
+ _onChannelMetadata(busData) {
+ this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
+ if (channel) {
+ channel.setProperties({
+ memberships_count: busData.memberships_count,
+ });
+ this.appEvents.trigger("chat:refresh-channel-members");
+ }
+ });
+ }
+
+ @bind
+ _onChannelEdits(busData) {
+ this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
+ if (channel) {
+ channel.setProperties({
+ title: busData.name,
+ description: busData.description,
+ });
+ }
+ });
+ }
+
+ @bind
+ _onChannelStatus(busData) {
+ this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
+ channel.set("status", busData.status);
+
+ // it is not possible for the user to set their last read message id
+ // if the channel has been archived, because all the messages have
+ // been deleted. we don't want them seeing the blue dot anymore so
+ // just completely reset the unreads
+ if (busData.status === CHANNEL_STATUSES.archived) {
+ channel.currentUserMembership.unread_count = 0;
+ channel.currentUserMembership.unread_mentions = 0;
+ }
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js
index 26feb212750..03b9164a058 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat.js
@@ -5,22 +5,15 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import Service, { inject as service } from "@ember/service";
import Site from "discourse/models/site";
import { ajax } from "discourse/lib/ajax";
-import { A } from "@ember/array";
import { generateCookFunction } from "discourse/lib/text";
import { cancel, next } from "@ember/runloop";
import { and } from "@ember/object/computed";
+import { computed } from "@ember/object";
import { Promise } from "rsvp";
-import ChatChannel, {
- CHANNEL_STATUSES,
- CHATABLE_TYPES,
-} from "discourse/plugins/chat/discourse/models/chat-channel";
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
import discourseDebounce from "discourse-common/lib/debounce";
-import EmberObject, { computed } from "@ember/object";
-import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
import discourseLater from "discourse-common/lib/later";
import userPresent from "discourse/lib/user-presence";
-import { bind } from "discourse-common/utils/decorators";
export const LIST_VIEW = "list_view";
export const CHAT_VIEW = "chat_view";
@@ -36,30 +29,21 @@ const READ_INTERVAL = 1000;
export default class Chat extends Service {
@service appEvents;
@service chatNotificationManager;
+ @service chatSubscriptionsManager;
@service chatStateManager;
@service presence;
@service router;
@service site;
+ @service chatChannelsManager;
activeChannel = null;
- allChannels = null;
cook = null;
- directMessageChannels = null;
- hasFetchedChannels = false;
- hasUnreadMessages = false;
- idToTitleMap = null;
- lastUserTrackingMessageId = null;
+
messageId = null;
presenceChannel = null;
- publicChannels = null;
sidebarActive = false;
- unreadUrgentCount = null;
- directMessagesLimit = 20;
isNetworkUnreliable = false;
@and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat;
- _fetchingChannels = null;
- _onNewMentionsCallbacks = new Map();
- _onNewMessagesCallbacks = new Map();
@computed("currentUser.staff", "currentUser.groups.[]")
get userCanDirectMessage() {
@@ -81,7 +65,6 @@ export default class Chat extends Service {
super.init(...arguments);
if (this.userCanChat) {
- this.set("allChannels", []);
this.presenceChannel = this.presence.getChannel("/chat/online");
this.draftStore = {};
@@ -114,38 +97,24 @@ export default class Chat extends Service {
}
setupWithPreloadedChannels(channels) {
- this.currentUser.set("chat_channel_tracking_state", {});
- this._processChannels(channels || {});
- this.subscribeToChannelMessageBus();
- this.userChatChannelTrackingStateChanged();
- this.appEvents.trigger("chat:refresh-channels");
- }
+ this.chatSubscriptionsManager.startChannelsSubscriptions(
+ channels.meta.message_bus_last_ids
+ );
+ this.presenceChannel.subscribe(channels.global_presence_channel_state);
- setupWithoutPreloadedChannels() {
- this.getChannels().then(() => {
- this.subscribeToChannelMessageBus();
- });
- }
-
- subscribeToChannelMessageBus() {
- this._subscribeToNewChannelUpdates();
- this._subscribeToUserTrackingChannel();
- this._subscribeToChannelEdits();
- this._subscribeToChannelMetadata();
- this._subscribeToChannelStatusChange();
+ [...channels.public_channels, ...channels.direct_message_channels].forEach(
+ (channelObject) => {
+ const channel = this.chatChannelsManager.store(channelObject);
+ return this.chatChannelsManager.follow(channel);
+ }
+ );
}
willDestroy() {
super.willDestroy(...arguments);
if (this.userCanChat) {
- this.set("allChannels", null);
- this._unsubscribeFromNewDmChannelUpdates();
- this._unsubscribeFromUserTrackingChannel();
- this._unsubscribeFromChannelEdits();
- this._unsubscribeFromChannelMetadata();
- this._unsubscribeFromChannelStatusChange();
- this._unsubscribeFromAllChatChannels();
+ this.chatSubscriptionsManager.stopChannelsSubscriptions();
}
}
@@ -186,10 +155,7 @@ export default class Chat extends Service {
return;
}
- if (
- this.chatStateManager.isFullPageActive ||
- this.chatStateManager.isDrawerActive
- ) {
+ if (this.chatStateManager.isActive) {
this.presenceChannel.enter({ activeOptions: CHAT_ONLINE_OPTIONS });
} else {
this.presenceChannel.leave();
@@ -199,61 +165,10 @@ export default class Chat extends Service {
getDocumentTitleCount() {
return this.chatNotificationManager.shouldCountChatInDocTitle()
- ? this.unreadUrgentCount
+ ? this.chatChannelsManager.unreadUrgentCount
: 0;
}
- _channelObject() {
- return {
- publicChannels: this.publicChannels,
- directMessageChannels: this.directMessageChannels,
- };
- }
-
- truncateDirectMessageChannels(channels) {
- return channels.slice(0, this.directMessagesLimit);
- }
-
- async getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
- let sortedChannels = this.allChannels.sort((a, b) => {
- return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at)
- ? -1
- : 1;
- });
-
- const trimmedFilter = filter.trim();
- const lowerCasedFilter = filter.toLowerCase();
- const { activeChannel } = this;
-
- return sortedChannels.filter((channel) => {
- if (
- opts.excludeActiveChannel &&
- activeChannel &&
- activeChannel.id === channel.id
- ) {
- return false;
- }
- if (!trimmedFilter.length) {
- return true;
- }
-
- if (channel.isDirectMessageChannel) {
- let userFound = false;
- channel.chatable.users.forEach((user) => {
- if (
- user.username.toLowerCase().includes(lowerCasedFilter) ||
- user.name?.toLowerCase().includes(lowerCasedFilter)
- ) {
- return (userFound = true);
- }
- });
- return userFound;
- } else {
- return channel.title.toLowerCase().includes(lowerCasedFilter);
- }
- });
- }
-
switchChannelUpOrDown(direction) {
const { activeChannel } = this;
if (!activeChannel) {
@@ -262,15 +177,11 @@ export default class Chat extends Service {
let currentList, otherList;
if (activeChannel.isDirectMessageChannel) {
- currentList = this.truncateDirectMessageChannels(
- this.directMessageChannels
- );
- otherList = this.publicChannels;
+ currentList = this.chatChannelsManager.truncatedDirectMessageChannels;
+ otherList = this.chatChannelsManager.publicMessageChannels;
} else {
- currentList = this.publicChannels;
- otherList = this.truncateDirectMessageChannels(
- this.directMessageChannels
- );
+ currentList = this.chatChannelsManager.publicMessageChannels;
+ otherList = this.chatChannelsManager.truncatedDirectMessageChannels;
}
const directionUp = direction === "up";
@@ -296,109 +207,6 @@ export default class Chat extends Service {
}
}
- getChannels() {
- return new Promise((resolve) => {
- if (this.hasFetchedChannels) {
- return resolve(this._channelObject());
- }
-
- if (!this._fetchingChannels) {
- this._fetchingChannels = this._refreshChannels();
- }
-
- this._fetchingChannels
- .then(() => resolve(this._channelObject()))
- .finally(() => (this._fetchingChannels = null));
- });
- }
-
- forceRefreshChannels() {
- this.set("hasFetchedChannels", false);
- this._unsubscribeFromAllChatChannels();
- return this.getChannels();
- }
-
- refreshTrackingState() {
- if (!this.currentUser) {
- return;
- }
-
- return ajax("/chat/chat_channels.json")
- .then((response) => {
- this.currentUser.set("chat_channel_tracking_state", {});
- (response.direct_message_channels || []).forEach((channel) => {
- this._updateUserTrackingState(channel);
- });
- (response.public_channels || []).forEach((channel) => {
- this._updateUserTrackingState(channel);
- });
- })
- .finally(() => {
- this.userChatChannelTrackingStateChanged();
- });
- }
-
- _refreshChannels() {
- return new Promise((resolve) => {
- this.setProperties({
- loading: true,
- allChannels: [],
- });
- this.currentUser.set("chat_channel_tracking_state", {});
- ajax("/chat/chat_channels.json").then((channels) => {
- this._processChannels(channels);
- this.userChatChannelTrackingStateChanged();
- this.appEvents.trigger("chat:refresh-channels");
- resolve(this._channelObject());
- });
- });
- }
-
- _processChannels(channels) {
- // Must be set first because `processChannels` relies on this data.
- this.set("messageBusLastIds", channels.message_bus_last_ids);
- this.setProperties({
- publicChannels: A(
- this.sortPublicChannels(
- (channels.public_channels || []).map((channel) =>
- this.processChannel(channel)
- )
- )
- ),
- directMessageChannels: A(
- this.sortDirectMessageChannels(
- (channels.direct_message_channels || []).map((channel) =>
- this.processChannel(channel)
- )
- )
- ),
- hasFetchedChannels: true,
- loading: false,
- });
- const idToTitleMap = {};
- this.allChannels.forEach((c) => {
- idToTitleMap[c.id] = c.title;
- });
- this.set("idToTitleMap", idToTitleMap);
- this.presenceChannel.subscribe(channels.global_presence_channel_state);
- }
-
- reSortDirectMessageChannels() {
- this.set(
- "directMessageChannels",
- this.sortDirectMessageChannels(this.directMessageChannels)
- );
- }
-
- async getChannelBy(key, value) {
- return this.getChannels().then(() => {
- if (!isNaN(value)) {
- value = parseInt(value, 10);
- }
- return (this.allChannels || []).findBy(key, value);
- });
- }
-
searchPossibleDirectMessageUsers(options) {
// TODO: implement a chat specific user search function
return userSearch(options);
@@ -414,99 +222,54 @@ export default class Chat extends Service {
// if that is present and in the list of channels the user can access.
// If none of these options exist, then we get the first public channel,
// or failing that the first DM channel.
- return this.getChannels().then(() => {
- // Defined in order of significance.
- let publicChannelWithMention,
- dmChannelWithUnread,
- publicChannelWithUnread,
- publicChannel,
- dmChannel,
- defaultChannel;
+ // Defined in order of significance.
+ let publicChannelWithMention,
+ dmChannelWithUnread,
+ publicChannelWithUnread,
+ publicChannel,
+ dmChannel,
+ defaultChannel;
- for (const [channel, state] of Object.entries(
- this.currentUser.chat_channel_tracking_state
- )) {
- if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) {
- if (!dmChannelWithUnread && state.unread_count > 0) {
- dmChannelWithUnread = channel;
- } else if (!dmChannel) {
- dmChannel = channel;
- }
- } else {
- if (state.unread_mentions > 0) {
- publicChannelWithMention = channel;
- break; // <- We have a public channel with a mention. Break and return this.
- } else if (!publicChannelWithUnread && state.unread_count > 0) {
- publicChannelWithUnread = channel;
- } else if (
- !defaultChannel &&
- parseInt(this.siteSettings.chat_default_channel_id || 0, 10) ===
- parseInt(channel, 10)
- ) {
- defaultChannel = channel;
- } else if (!publicChannel) {
- publicChannel = channel;
- }
+ this.chatChannelsManager.channels.forEach((channel) => {
+ const membership = channel.currentUserMembership;
+
+ if (channel.isDirectMessageChannel) {
+ if (!dmChannelWithUnread && membership.unread_count > 0) {
+ dmChannelWithUnread = channel.id;
+ } else if (!dmChannel) {
+ dmChannel = channel.id;
+ }
+ } else {
+ if (membership.unread_mentions > 0) {
+ publicChannelWithMention = channel.id;
+ return; // <- We have a public channel with a mention. Break and return this.
+ } else if (!publicChannelWithUnread && membership.unread_count > 0) {
+ publicChannelWithUnread = channel.id;
+ } else if (
+ !defaultChannel &&
+ parseInt(this.siteSettings.chat_default_channel_id || 0, 10) ===
+ channel.id
+ ) {
+ defaultChannel = channel.id;
+ } else if (!publicChannel) {
+ publicChannel = channel.id;
}
}
- return (
- publicChannelWithMention ||
- dmChannelWithUnread ||
- publicChannelWithUnread ||
- defaultChannel ||
- publicChannel ||
- dmChannel
- );
});
- }
- sortPublicChannels(channels) {
- return channels.sort((a, b) => a.title.localeCompare(b.title));
- }
-
- sortDirectMessageChannels(channels) {
- return channels.sort((a, b) => {
- const unreadCountA =
- this.currentUser.chat_channel_tracking_state[a.id]?.unread_count || 0;
- const unreadCountB =
- this.currentUser.chat_channel_tracking_state[b.id]?.unread_count || 0;
- if (unreadCountA === unreadCountB) {
- return new Date(a.last_message_sent_at) >
- new Date(b.last_message_sent_at)
- ? -1
- : 1;
- } else {
- return unreadCountA > unreadCountB ? -1 : 1;
- }
- });
- }
-
- getIdealFirstChannelIdAndTitle() {
- return this.getIdealFirstChannelId().then((channelId) => {
- if (!channelId) {
- return;
- }
- return {
- id: channelId,
- title: this.idToTitleMap[channelId],
- };
- });
+ return (
+ publicChannelWithMention ||
+ dmChannelWithUnread ||
+ publicChannelWithUnread ||
+ defaultChannel ||
+ publicChannel ||
+ dmChannel
+ );
}
async openChannelAtMessage(channelId, messageId = null) {
- let channel = await this.getChannelBy("id", channelId);
- if (channel) {
+ return this.chatChannelsManager.find(channelId).then((channel) => {
return this._openFoundChannelAtMessage(channel, messageId);
- }
-
- return ajax(`/chat/chat_channels/${channelId}`).then((response) => {
- const queryParams = messageId ? { messageId } : {};
- return this.router.transitionTo(
- "chat.channel",
- response.id,
- slugifyChannel(response),
- { queryParams }
- );
});
}
@@ -559,380 +322,18 @@ export default class Chat extends Service {
this.appEvents.trigger("chat-live-pane:highlight-message", messageId);
}
- async startTrackingChannel(channel) {
- if (!channel.current_user_membership.following) {
- return;
- }
-
- let existingChannel = await this.getChannelBy("id", channel.id);
- if (existingChannel) {
- return existingChannel; // User is already tracking this channel. return!
- }
-
- const existingChannels = channel.isDirectMessageChannel
- ? this.directMessageChannels
- : this.publicChannels;
-
- // this check shouldn't be needed given the previous check to existingChannel
- // this is a safety net, to ensure we never track duplicated channels
- existingChannel = existingChannels.findBy("id", channel.id);
- if (existingChannel) {
- return existingChannel;
- }
-
- const newChannel = this.processChannel(channel);
- existingChannels.pushObject(newChannel);
- this.currentUser.chat_channel_tracking_state[channel.id] =
- EmberObject.create({
- unread_count: 1,
- unread_mentions: 0,
- chatable_type: channel.chatable_type,
- });
- this.userChatChannelTrackingStateChanged();
- if (channel.isDirectMessageChannel) {
- this.reSortDirectMessageChannels();
- }
- if (channel.isPublicChannel) {
- this.set("publicChannels", this.sortPublicChannels(this.publicChannels));
- }
- this.appEvents.trigger("chat:refresh-channels");
- return newChannel;
- }
-
- async stopTrackingChannel(channel) {
- return this.getChannelBy("id", channel.id).then((existingChannel) => {
- if (existingChannel) {
- return this.forceRefreshChannels();
- }
- });
- }
-
- _subscribeToChannelMetadata() {
- this.messageBus.subscribe(
- "/chat/channel-metadata",
- this._onChannelMetadata,
- this.messageBusLastIds.channel_metadata
- );
- }
-
- _subscribeToChannelEdits() {
- this.messageBus.subscribe(
- "/chat/channel-edits",
- this._onChannelEdits,
- this.messageBusLastIds.channel_edits
- );
- }
-
- _subscribeToChannelStatusChange() {
- this.messageBus.subscribe("/chat/channel-status", this._onChannelStatus);
- }
-
- _unsubscribeFromChannelStatusChange() {
- this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus);
- }
-
- _unsubscribeFromChannelEdits() {
- this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits);
- }
-
- _unsubscribeFromChannelMetadata() {
- this.messageBus.unsubscribe(
- "/chat/channel-metadata",
- this._onChannelMetadata
- );
- }
-
- _subscribeToNewChannelUpdates() {
- this.messageBus.subscribe(
- "/chat/new-channel",
- this._onNewChannel,
- this.messageBusLastIds.new_channel
- );
- }
-
- _unsubscribeFromNewDmChannelUpdates() {
- this.messageBus.unsubscribe("/chat/new-channel", this._onNewChannel);
- }
-
- _subscribeToSingleUpdateChannel(channel) {
- if (channel.current_user_membership.muted) {
- return;
- }
-
- // We do this first so we don't multi-subscribe to mention + messages
- // messageBus channels for this chat channel, since _subscribeToSingleUpdateChannel
- // is called from multiple places.
- this._unsubscribeFromChatChannel(channel);
-
- if (!channel.isDirectMessageChannel) {
- this._subscribeToMentionChannel(channel);
- }
-
- this._subscribeToNewMessagesChannel(channel);
- }
-
- _subscribeToMentionChannel(channel) {
- const onNewMentions = () => {
- const trackingState =
- this.currentUser.chat_channel_tracking_state[channel.id];
-
- if (trackingState) {
- const count = (trackingState.unread_mentions || 0) + 1;
- trackingState.set("unread_mentions", count);
- this.userChatChannelTrackingStateChanged();
- }
- };
-
- this._onNewMentionsCallbacks.set(channel.id, onNewMentions);
-
- this.messageBus.subscribe(
- `/chat/${channel.id}/new-mentions`,
- onNewMentions,
- channel.message_bus_last_ids.new_mentions
- );
- }
-
- _subscribeToNewMessagesChannel(channel) {
- const onNewMessages = (busData) => {
- const trackingState =
- this.currentUser.chat_channel_tracking_state[channel.id];
-
- if (busData.user_id === this.currentUser.id) {
- // User sent message, update tracking state to no unread
- trackingState.set("chat_message_id", busData.message_id);
- } else {
- // Ignored user sent message, update tracking state to no unread
- if (this.currentUser.ignored_users.includes(busData.username)) {
- trackingState.set("chat_message_id", busData.message_id);
- } else {
- // Message from other user. Increment trackings state
- if (busData.message_id > (trackingState.chat_message_id || 0)) {
- trackingState.set("unread_count", trackingState.unread_count + 1);
- }
- }
- }
-
- this.userChatChannelTrackingStateChanged();
- channel.set("last_message_sent_at", new Date());
-
- const directMessageChannel = (this.directMessageChannels || []).findBy(
- "id",
- parseInt(channel.id, 10)
- );
-
- if (directMessageChannel) {
- this.reSortDirectMessageChannels();
- }
- };
-
- this._onNewMessagesCallbacks.set(channel.id, onNewMessages);
-
- this.messageBus.subscribe(
- `/chat/${channel.id}/new-messages`,
- onNewMessages,
- channel.message_bus_last_ids.new_messages
- );
- }
-
- @bind
- _onChannelMetadata(busData) {
- this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
- if (channel) {
- channel.setProperties({
- memberships_count: busData.memberships_count,
- });
- this.appEvents.trigger("chat:refresh-channel-members");
- }
- });
- }
-
- @bind
- _onChannelEdits(busData) {
- this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
- if (channel) {
- channel.setProperties({
- title: busData.name,
- description: busData.description,
- });
- }
- });
- }
-
- @bind
- _onChannelStatus(busData) {
- this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
- if (!channel) {
- return;
- }
-
- channel.set("status", busData.status);
-
- // it is not possible for the user to set their last read message id
- // if the channel has been archived, because all the messages have
- // been deleted. we don't want them seeing the blue dot anymore so
- // just completely reset the unreads
- if (busData.status === CHANNEL_STATUSES.archived) {
- this.currentUser.chat_channel_tracking_state[channel.id] = {
- unread_count: 0,
- unread_mentions: 0,
- chatable_type: channel.chatable_type,
- };
- this.userChatChannelTrackingStateChanged();
- }
-
- this.appEvents.trigger("chat:refresh-channel", channel.id);
- }, this.messageBusLastIds.channel_status);
- }
-
- @bind
- _onNewChannel(busData) {
- this.startTrackingChannel(ChatChannel.create(busData.chat_channel));
- }
-
async followChannel(channel) {
- return ChatApi.followChatChannel(channel).then(() => {
- this.startTrackingChannel(channel);
- this._subscribeToSingleUpdateChannel(channel);
- });
+ return this.chatChannelsManager.follow(channel);
}
async unfollowChannel(channel) {
- return ChatApi.unfollowChatChannel(channel).then(() => {
- this._unsubscribeFromChatChannel(channel);
- this.stopTrackingChannel(channel);
-
+ return this.chatChannelsManager.unfollow(channel).then(() => {
if (channel === this.activeChannel && channel.isDirectMessageChannel) {
this.router.transitionTo("chat");
}
});
}
- _unsubscribeFromAllChatChannels() {
- (this.allChannels || []).forEach((channel) => {
- this._unsubscribeFromChatChannel(channel);
- });
- }
-
- _unsubscribeFromChatChannel(channel) {
- this.messageBus.unsubscribe("/chat/*", this._onNewMessagesCallbacks);
- if (!channel.isDirectMessageChannel) {
- this.messageBus.unsubscribe("/chat/*", this._onNewMentionsCallbacks);
- }
- }
-
- _subscribeToUserTrackingChannel() {
- this.messageBus.subscribe(
- `/chat/user-tracking-state/${this.currentUser.id}`,
- this._onUserTrackingState,
- this.messageBusLastIds.user_tracking_state
- );
- }
-
- _unsubscribeFromUserTrackingChannel() {
- this.messageBus.unsubscribe(
- `/chat/user-tracking-state/${this.currentUser.id}`,
- this._onUserTrackingState
- );
- }
-
- @bind
- _onUserTrackingState(busData, _, messageId) {
- const lastId = this.lastUserTrackingMessageId;
-
- // we don't want this state to go backwards, only catch
- // up if messages from messagebus were missed
- if (!lastId || messageId > lastId) {
- this.lastUserTrackingMessageId = messageId;
- }
-
- // we are too far out of sync, we should resync everything.
- // this will trigger a route transition and blur the chat input
- if (lastId && messageId > lastId + 1) {
- return this.forceRefreshChannels();
- }
-
- const trackingState =
- this.currentUser.chat_channel_tracking_state[busData.chat_channel_id];
-
- if (trackingState) {
- trackingState.set("chat_message_id", busData.chat_message_id);
- trackingState.set("unread_count", 0);
- trackingState.set("unread_mentions", 0);
- this.userChatChannelTrackingStateChanged();
- }
- }
-
- resetTrackingStateForChannel(channelId) {
- const trackingState =
- this.currentUser.chat_channel_tracking_state[channelId];
- if (trackingState) {
- trackingState.set("unread_count", 0);
- this.userChatChannelTrackingStateChanged();
- }
- }
-
- userChatChannelTrackingStateChanged() {
- this._recalculateUnreadMessages();
- this.appEvents.trigger("chat:user-tracking-state-changed");
- }
-
- _recalculateUnreadMessages() {
- let unreadPublicCount = 0;
- let unreadUrgentCount = 0;
- let headerNeedsRerender = false;
-
- Object.values(this.currentUser.chat_channel_tracking_state).forEach(
- (state) => {
- if (state.muted) {
- return;
- }
-
- if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) {
- unreadUrgentCount += state.unread_count || 0;
- } else {
- unreadUrgentCount += state.unread_mentions || 0;
- unreadPublicCount += state.unread_count || 0;
- }
- }
- );
-
- let hasUnreadPublic = unreadPublicCount > 0;
- if (hasUnreadPublic !== this.hasUnreadMessages) {
- headerNeedsRerender = true;
- this.set("hasUnreadMessages", hasUnreadPublic);
- }
-
- if (unreadUrgentCount !== this.unreadUrgentCount) {
- headerNeedsRerender = true;
- this.set("unreadUrgentCount", unreadUrgentCount);
- }
-
- this.currentUser.notifyPropertyChange("chat_channel_tracking_state");
- if (headerNeedsRerender) {
- this.appEvents.trigger("chat:rerender-header");
- this.appEvents.trigger("notifications:changed");
- }
- }
-
- processChannel(channel) {
- channel = ChatChannel.create(channel);
- this._subscribeToSingleUpdateChannel(channel);
- this._updateUserTrackingState(channel);
- this.allChannels.push(channel);
- return channel;
- }
-
- _updateUserTrackingState(channel) {
- this.currentUser.chat_channel_tracking_state[channel.id] =
- EmberObject.create({
- chatable_type: channel.chatable_type,
- muted: channel.current_user_membership.muted,
- unread_count: channel.current_user_membership.unread_count,
- unread_mentions: channel.current_user_membership.unread_mentions,
- chat_message_id: channel.current_user_membership.last_read_message_id,
- });
- }
-
upsertDmChannelForUser(channel, user) {
const usernames = (channel.chatable.users || [])
.mapBy("username")
@@ -951,9 +352,9 @@ export default class Chat extends Service {
data: { usernames: usernames.uniq() },
})
.then((response) => {
- const chatChannel = ChatChannel.create(response.chat_channel);
- this.startTrackingChannel(chatChannel);
- return chatChannel;
+ const channel = this.chatChannelsManager.store(response.channel);
+ this.chatChannelsManager.follow(channel);
+ return channel;
})
.catch(popupAjaxError);
}
@@ -1031,17 +432,8 @@ export default class Chat extends Service {
10
);
- const hasUnreadMessages = latestUnreadMsgId > channel.lastSendReadMessageId;
-
- if (
- !hasUnreadMessages &&
- this.currentUser.chat_channel_tracking_state[this.activeChannel.id]
- ?.unread_count > 0
- ) {
- // Weird state here where the chat_channel_tracking_state is wrong. Need to reset it.
- this.resetTrackingStateForChannel(this.activeChannel.id);
- }
-
+ const hasUnreadMessages =
+ latestUnreadMsgId > channel.currentUserMembership.last_read_message_id;
if (hasUnreadMessages) {
channel.updateLastReadMessage(latestUnreadMsgId);
}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs
index dc261f00413..89a38629ad6 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs
@@ -1 +1 @@
-
+
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs
index 5211ae3ff72..a9cb4f67d41 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs
@@ -1 +1,5 @@
-
+
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs
index b6e654e73d6..e80afabd3ba 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs
@@ -1 +1 @@
-
+
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs
index d267701835a..a30950bc79e 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs
@@ -1 +1 @@
-
+
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs
index e7e0ba3aacb..fdb757c23c3 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs
@@ -3,16 +3,17 @@
@@ -35,16 +36,13 @@
>
{{i18n (concat "chat.channel_info.tabs." tab)}}
{{#if (eq tab "members")}}
- ({{this.model.chatChannel.membershipsCount}})
+ ({{this.model.membershipsCount}})
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
index e39cc9a9d59..14199193dec 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
@@ -17,15 +17,15 @@
{{/if}}
-