DEV: converts models to native classes (#21418)

- `ChatChannel`
- `UserChatChannelMembership`

Also creates a new `chat-direct-message` model used as the object for the`chatable` property of the `ChatChannel` when the `ChatChannel` is a direct message channel. When the chatable is a category a real `Category` object will now be returned.

Archive state of a `ChatChannel` is now hold in a `ChatChannelArchive` object.
This commit is contained in:
Joffrey JAFFEUX 2023-05-08 18:24:41 +02:00 committed by GitHub
parent afbeeea09f
commit 22521d3428
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 638 additions and 624 deletions

View File

@ -42,7 +42,7 @@ class Chat::Api::ChatablesController < Chat::ApiController
direct_message_channels = direct_message_channels =
if users.count > 0 if users.count > 0
# FIXME: investigate the cost of this query # FIXME: investigate the cost of this query
Chat::Channel Chat::DirectMessageChannel
.includes(chatable: :users) .includes(chatable: :users)
.joins(direct_message: :direct_message_users) .joins(direct_message: :direct_message_users)
.group(1) .group(1)

View File

@ -2,6 +2,8 @@
module Chat module Chat
class DirectMessageSerializer < ApplicationSerializer class DirectMessageSerializer < ApplicationSerializer
attributes :id
has_many :users, serializer: Chat::UserWithCustomFieldsAndStatusSerializer, embed: :objects has_many :users, serializer: Chat::UserWithCustomFieldsAndStatusSerializer, embed: :objects
def users def users

View File

@ -40,7 +40,7 @@ export default Component.extend(ModalFunctionality, {
.createChannelArchive(this.chatChannel.id, this._data()) .createChannelArchive(this.chatChannel.id, this._data())
.then(() => { .then(() => {
this.flash(I18n.t("chat.channel_archive.process_started"), "success"); this.flash(I18n.t("chat.channel_archive.process_started"), "success");
this.chatChannel.set("status", CHANNEL_STATUSES.archived); this.chatChannel.status = CHANNEL_STATUSES.archived;
discourseLater(() => { discourseLater(() => {
this.closeModal(); this.closeModal();

View File

@ -1,18 +1,25 @@
{{#if (and this.channel.archive_failed this.currentUser.admin)}} {{#if this.shouldRender}}
<div class="alert alert-warn chat-channel-retry-archive"> {{#if @channel.archive.failed}}
<div class="chat-channel-archive-failed-message"> <div
{{this.channelArchiveFailedMessage}} class={{concat-class
</div> "alert alert-warn chat-channel-retry-archive"
@channel.status
}}
>
<div class="chat-channel-archive-failed-message">
{{this.channelArchiveFailedMessage}}
</div>
<div class="chat-channel-archive-failed-retry"> <div class="chat-channel-archive-failed-retry">
<DButton <DButton
@action={{action "retryArchive"}} @action={{this.retryArchive}}
@label="chat.channel_archive.retry" @label="chat.channel_archive.retry"
/> />
</div>
</div> </div>
</div> {{else if @channel.archive.completed}}
{{else if (and this.channel.archive_completed this.currentUser.admin)}} <div class={{concat-class "chat-channel-archive-status" @channel.status}}>
<div class="chat-channel-archive-status"> {{this.channelArchiveCompletedMessage}}
{{this.channelArchiveCompletedMessage}} </div>
</div> {{/if}}
{{/if}} {{/if}}

View File

@ -1,61 +1,53 @@
import Component from "@ember/component"; import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import I18n from "I18n"; import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { isPresent } from "@ember/utils";
export default Component.extend({ export default class ChatChannelArchiveStatus extends Component {
channel: null, @service chatApi;
tagName: "", @service currentUser;
chatApi: service(),
@discourseComputed( get shouldRender() {
"channel.status", return this.currentUser.admin && isPresent(this.args.channel.archive);
"channel.archived_messages", }
"channel.total_messages",
"channel.archive_failed" get channelArchiveFailedMessage() {
) const archive = this.args.channel.archive;
channelArchiveFailedMessage() { const translationKey = !archive.topicId
const translationKey = !this.channel.archive_topic_id
? "chat.channel_status.archive_failed_no_topic" ? "chat.channel_status.archive_failed_no_topic"
: "chat.channel_status.archive_failed"; : "chat.channel_status.archive_failed";
return htmlSafe( return htmlSafe(
I18n.t(translationKey, { I18n.t(translationKey, {
completed: this.channel.archived_messages, completed: archive.messages,
total: this.channel.total_messages, total: archive.totalMessages,
topic_url: this._getTopicUrl(), topic_url: this.#getTopicUrl(),
}) })
); );
}, }
@discourseComputed( get channelArchiveCompletedMessage() {
"channel.status",
"channel.archived_messages",
"channel.total_messages",
"channel.archive_completed"
)
channelArchiveCompletedMessage() {
return htmlSafe( return htmlSafe(
I18n.t("chat.channel_status.archive_completed", { I18n.t("chat.channel_status.archive_completed", {
topic_url: this._getTopicUrl(), topic_url: this.#getTopicUrl(),
}) })
); );
}, }
@action @action
retryArchive() { retryArchive() {
return this.chatApi return this.chatApi
.createChannelArchive(this.channel.id) .createChannelArchive(this.args.channel.id)
.catch(popupAjaxError); .catch(popupAjaxError);
}, }
_getTopicUrl() { get #getTopicUrl() {
if (!this.channel.archive_topic_id) { if (!this.args.channel.archive.topicId) {
return ""; return "";
} }
return getURL(`/t/-/${this.channel.archive_topic_id}`); return getURL(`/t/-/${this.args.channel.archive.topicId}`);
}, }
}); }

View File

@ -1,8 +1,8 @@
{{#unless this.site.mobileView}} {{#if this.shouldRender}}
<DButton <DButton
@icon="times" @icon="times"
@action={{this.onLeaveChannel}} @action={{@onLeaveChannel}}
@class="btn-flat chat-channel-leave-btn" @class="btn-flat chat-channel-leave-btn"
@title={{this.leaveChatTitleKey}} @title={{this.leaveChatTitleKey}}
/> />
{{/unless}} {{/if}}

View File

@ -1,25 +1,19 @@
import discourseComputed from "discourse-common/utils/decorators"; import Component from "@glimmer/component";
import Component from "@ember/component";
import { equal } from "@ember/object/computed";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel"; import { isPresent } from "@ember/utils";
export default class ChatChannelLeaveBtn extends Component {
@service chat;
@service site;
export default Component.extend({ get shouldRender() {
tagName: "", return !this.site.mobileView && isPresent(this.args.channel);
channel: null, }
chat: service(),
isDirectMessageRow: equal( get leaveChatTitleKey() {
"channel.chatable_type", if (this.args.channel.isDirectMessageChannel) {
CHATABLE_TYPES.directMessageChannel
),
@discourseComputed("isDirectMessageRow")
leaveChatTitleKey(isDirectMessageRow) {
if (isDirectMessageRow) {
return "chat.direct_messages.leave"; return "chat.direct_messages.leave";
} else { } else {
return "chat.leave"; return "chat.leave";
} }
}, }
}); }

View File

@ -19,7 +19,7 @@ export default class ChatChannelRow extends Component {
} }
get channelHasUnread() { get channelHasUnread() {
return this.args.channel.currentUserMembership.unread_count > 0; return this.args.channel.currentUserMembership.unreadCount > 0;
} }
get #firstDirectMessageUser() { get #firstDirectMessageUser() {

View File

@ -5,9 +5,11 @@ import { action } from "@ember/object";
export default Component.extend({ export default Component.extend({
tagName: "", tagName: "",
@discourseComputed("model", "model.focused") isFocused: false,
rowClassNames(model, focused) {
return `chat-channel-selection-row ${focused ? "focused" : ""} ${ @discourseComputed("model", "isFocused")
rowClassNames(model, isFocused) {
return `chat-channel-selection-row ${isFocused ? "focused" : ""} ${
this.model.user ? "user-row" : "channel-row" this.model.user ? "user-row" : "channel-row"
}`; }`;
}, },

View File

@ -18,6 +18,7 @@
<ConditionalLoadingSpinner @condition={{this.loading}}> <ConditionalLoadingSpinner @condition={{this.loading}}>
{{#each this.channels as |channel|}} {{#each this.channels as |channel|}}
<ChatChannelSelectionRow <ChatChannelSelectionRow
@isFocused={{eq channel this.focusedRow}}
@model={{channel}} @model={{channel}}
@onClick={{this.switchChannel}} @onClick={{this.switchChannel}}
/> />

View File

@ -1,6 +1,5 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
@ -9,6 +8,8 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { INPUT_DELAY } from "discourse-common/config/environment"; import { INPUT_DELAY } from "discourse-common/config/environment";
import { isPresent } from "@ember/utils"; import { isPresent } from "@ember/utils";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import User from "discourse/models/user";
export default Component.extend({ export default Component.extend({
chat: service(), chat: service(),
@ -19,6 +20,7 @@ export default Component.extend({
loading: false, loading: false,
chatChannelsManager: service(), chatChannelsManager: service(),
router: service(), router: service(),
focusedRow: null,
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
@ -53,19 +55,16 @@ export default Component.extend({
} else { } else {
channel = this.channels.find((c) => c.user && c.id === id); channel = this.channels.find((c) => c.user && c.id === id);
} }
channel?.set("focused", true); if (channel) {
this.channels.forEach((c) => { this.set("focusedRow", channel);
if (c !== channel) { }
c.set("focused", false);
}
});
} }
}, },
@bind @bind
onKeyUp(e) { onKeyUp(e) {
if (e.key === "Enter") { if (e.key === "Enter") {
let focusedChannel = this.channels.find((c) => c.focused); let focusedChannel = this.channels.find((c) => c === this.focusedRow);
this.switchChannel(focusedChannel); this.switchChannel(focusedChannel);
e.preventDefault(); e.preventDefault();
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
@ -78,16 +77,17 @@ export default Component.extend({
}, },
arrowNavigateChannels(direction) { arrowNavigateChannels(direction) {
const indexOfFocused = this.channels.findIndex((c) => c.focused); const indexOfFocused = this.channels.findIndex(
(c) => c === this.focusedRow
);
if (indexOfFocused > -1) { if (indexOfFocused > -1) {
const nextIndex = direction === "down" ? 1 : -1; const nextIndex = direction === "down" ? 1 : -1;
const nextChannel = this.channels[indexOfFocused + nextIndex]; const nextChannel = this.channels[indexOfFocused + nextIndex];
if (nextChannel) { if (nextChannel) {
this.channels[indexOfFocused].set("focused", false); this.set("focusedRow", nextChannel);
nextChannel.set("focused", true);
} }
} else { } else {
this.channels[0].set("focused", true); this.set("focusedRow", this.channels[0]);
} }
schedule("afterRender", () => { schedule("afterRender", () => {
@ -100,7 +100,7 @@ export default Component.extend({
@action @action
switchChannel(channel) { switchChannel(channel) {
if (channel.user) { if (channel instanceof User) {
return this.fetchOrCreateChannelForUser(channel).then((response) => { return this.fetchOrCreateChannelForUser(channel).then((response) => {
const newChannel = this.chatChannelsManager.store(response.channel); const newChannel = this.chatChannelsManager.store(response.channel);
return this.chatChannelsManager.follow(newChannel).then((c) => { return this.chatChannelsManager.follow(newChannel).then((c) => {
@ -145,21 +145,21 @@ export default Component.extend({
.then((searchModel) => { .then((searchModel) => {
if (this.searchIndex === thisSearchIndex) { if (this.searchIndex === thisSearchIndex) {
this.set("searchModel", searchModel); this.set("searchModel", searchModel);
const channels = searchModel.public_channels.concat( let channels = searchModel.public_channels
searchModel.direct_message_channels, .concat(searchModel.direct_message_channels, searchModel.users)
searchModel.users .map((c) => {
); if (
channels.forEach((c) => { c.chatable_type === "DirectMessage" ||
if (c.username) { c.chatable_type === "Category"
c.user = true; // This is used by the `chat-channel-selection-row` component ) {
} return ChatChannel.create(c);
}); }
return User.create(c);
});
this.setProperties({ this.setProperties({
channels: channels.map((channel) => { channels,
return channel.user
? ChatChannel.create(channel)
: this.chatChannelsManager.store(channel);
}),
loading: false, loading: false,
}); });
this.focusFirstChannel(this.channels); this.focusFirstChannel(this.channels);
@ -188,8 +188,11 @@ export default Component.extend({
}, },
focusFirstChannel(channels) { focusFirstChannel(channels) {
channels.forEach((c) => c.set("focused", false)); if (channels[0]) {
channels[0]?.set("focused", true); this.set("focusedRow", channels[0]);
} else {
this.set("focusedRow", null);
}
}, },
getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) { getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {

View File

@ -12,7 +12,7 @@
@value={{this.channel.currentUserMembership.muted}} @value={{this.channel.currentUserMembership.muted}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__muted-selector" @class="channel-settings-view__muted-selector"
@onChange={{action (fn this.saveNotificationSettings "muted")}} @onChange={{fn this.saveNotificationSettings "muted" "muted"}}
/> />
</div> </div>
</div> </div>
@ -22,17 +22,19 @@
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.desktop_notification_level"}}</span> <span>{{i18n "chat.settings.desktop_notification_level"}}</span>
<ChatChannelSettingsSavedIndicator <ChatChannelSettingsSavedIndicator
@property={{this.channel.currentUserMembership.desktop_notification_level}} @property={{this.channel.currentUserMembership.desktopNotificationLevel}}
/> />
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox <ComboBox
@content={{this.notificationLevels}} @content={{this.notificationLevels}}
@value={{this.channel.currentUserMembership.desktop_notification_level}} @value={{this.channel.currentUserMembership.desktopNotificationLevel}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__desktop-notification-level-selector" @class="channel-settings-view__desktop-notification-level-selector"
@onChange={{action @onChange={{fn
(fn this.saveNotificationSettings "desktop_notification_level") this.saveNotificationSettings
"desktopNotificationLevel"
"desktop_notification_level"
}} }}
/> />
</div> </div>
@ -42,17 +44,19 @@
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.mobile_notification_level"}}</span> <span>{{i18n "chat.settings.mobile_notification_level"}}</span>
<ChatChannelSettingsSavedIndicator <ChatChannelSettingsSavedIndicator
@property={{this.channel.currentUserMembership.mobile_notification_level}} @property={{this.channel.currentUserMembership.mobileNotificationLevel}}
/> />
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox <ComboBox
@content={{this.notificationLevels}} @content={{this.notificationLevels}}
@value={{this.channel.currentUserMembership.mobile_notification_level}} @value={{this.channel.currentUserMembership.mobileNotificationLevel}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__mobile-notification-level-selector" @class="channel-settings-view__mobile-notification-level-selector"
@onChange={{action @onChange={{fn
(fn this.saveNotificationSettings "mobile_notification_level") this.saveNotificationSettings
"mobileNotificationLevel"
"mobile_notification_level"
}} }}
/> />
</div> </div>
@ -74,16 +78,16 @@
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.auto_join_users_label"}}</span> <span>{{i18n "chat.settings.auto_join_users_label"}}</span>
<ChatChannelSettingsSavedIndicator <ChatChannelSettingsSavedIndicator
@property={{this.channel.auto_join_users}} @property={{this.channel.autoJoinUsers}}
/> />
</label> </label>
<ComboBox <ComboBox
@content={{this.autoAddUsersOptions}} @content={{this.autoAddUsersOptions}}
@value={{this.channel.auto_join_users}} @value={{this.channel.autoJoinUsers}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__auto-join-selector" @class="channel-settings-view__auto-join-selector"
@onChange={{action @onChange={{action
(fn this.onToggleAutoJoinUsers this.channel.auto_join_users) (fn this.onToggleAutoJoinUsers this.channel.autoJoinUsers)
}} }}
/> />
<p class="chat-form__description -autojoin"> <p class="chat-form__description -autojoin">
@ -102,12 +106,12 @@
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.channel_wide_mentions_label"}}</span> <span>{{i18n "chat.settings.channel_wide_mentions_label"}}</span>
<ChatChannelSettingsSavedIndicator <ChatChannelSettingsSavedIndicator
@property={{this.channel.allow_channel_wide_mentions}} @property={{this.channel.allowChannelWideMentions}}
/> />
</label> </label>
<ComboBox <ComboBox
@content={{this.channelWideMentionsOptions}} @content={{this.channelWideMentionsOptions}}
@value={{this.channel.allow_channel_wide_mentions}} @value={{this.channel.allowChannelWideMentions}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__channel-wide-mentions-selector" @class="channel-settings-view__channel-wide-mentions-selector"
@onChange={{this.onToggleChannelWideMentions}} @onChange={{this.onToggleChannelWideMentions}}

View File

@ -3,7 +3,6 @@ import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import I18n from "I18n"; import I18n from "I18n";
import { Promise } from "rsvp";
import { reads } from "@ember/object/computed"; import { reads } from "@ember/object/computed";
const NOTIFICATION_LEVELS = [ const NOTIFICATION_LEVELS = [
@ -79,29 +78,18 @@ export default class ChatChannelSettingsView extends Component {
} }
@action @action
saveNotificationSettings(key, value) { saveNotificationSettings(frontendKey, backendKey, newValue) {
if (this.channel[key] === value) { if (this.channel.currentUserMembership[frontendKey] === newValue) {
return; return;
} }
const settings = {}; const settings = {};
settings[key] = value; settings[backendKey] = newValue;
return this.chatApi return this.chatApi
.updateCurrentUserChannelNotificationsSettings(this.channel.id, settings) .updateCurrentUserChannelNotificationsSettings(this.channel.id, settings)
.then((result) => { .then((result) => {
[ this.channel.currentUserMembership[frontendKey] =
"muted", result.membership[backendKey];
"desktop_notification_level",
"mobile_notification_level",
].forEach((property) => {
if (
result.membership[property] !==
this.channel.currentUserMembership[property]
) {
this.channel.currentUserMembership[property] =
result.membership[property];
}
});
}); });
} }
@ -125,7 +113,7 @@ export default class ChatChannelSettingsView extends Component {
@action @action
onToggleAutoJoinUsers() { onToggleAutoJoinUsers() {
if (!this.channel.auto_join_users) { if (!this.channel.autoJoinUsers) {
this.onEnableAutoJoinUsers(); this.onEnableAutoJoinUsers();
} else { } else {
this.onDisableAutoJoinUsers(); this.onDisableAutoJoinUsers();
@ -134,43 +122,61 @@ export default class ChatChannelSettingsView extends Component {
@action @action
onToggleChannelWideMentions() { onToggleChannelWideMentions() {
const newValue = !this.channel.allowChannelWideMentions;
if (this.channel.allowChannelWideMentions === newValue) {
return;
}
return this._updateChannelProperty( return this._updateChannelProperty(
this.channel, this.channel,
"allow_channel_wide_mentions", "allow_channel_wide_mentions",
!this.channel.allow_channel_wide_mentions newValue
); ).then((result) => {
this.channel.allowChannelWideMentions =
result.channel.allow_channel_wide_mentions;
});
} }
onDisableAutoJoinUsers() { onDisableAutoJoinUsers() {
return this._updateChannelProperty(this.channel, "auto_join_users", false); if (this.channel.autoJoinUsers === false) {
return;
}
return this._updateChannelProperty(
this.channel,
"auto_join_users",
false
).then((result) => {
this.channel.autoJoinUsers = result.channel.auto_join_users;
});
} }
onEnableAutoJoinUsers() { onEnableAutoJoinUsers() {
if (this.channel.autoJoinUsers === true) {
return;
}
this.dialog.confirm({ this.dialog.confirm({
message: I18n.t("chat.settings.auto_join_users_warning", { message: I18n.t("chat.settings.auto_join_users_warning", {
category: this.channel.chatable.name, category: this.channel.chatable.name,
}), }),
didConfirm: () => didConfirm: () =>
this._updateChannelProperty(this.channel, "auto_join_users", true), this._updateChannelProperty(this.channel, "auto_join_users", true).then(
(result) => {
this.channel.autoJoinUsers = result.channel.auto_join_users;
}
),
}); });
} }
_updateChannelProperty(channel, property, value) { _updateChannelProperty(channel, property, value) {
if (channel[property] === value) {
return Promise.resolve();
}
const payload = {}; const payload = {};
payload[property] = value; payload[property] = value;
return this.chatApi
.updateChannel(channel.id, payload) return this.chatApi.updateChannel(channel.id, payload).catch((event) => {
.then((result) => { if (event.jqXHR?.responseJSON?.errors) {
channel.set(property, result.channel[property]); this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}) }
.catch((event) => { });
if (event.jqXHR?.responseJSON?.errors) {
this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}
});
} }
} }

View File

@ -1,21 +1,21 @@
{{#if this.channel.isDraft}} {{#if @channel.isDraft}}
<div class="chat-channel-title is-draft"> <div class="chat-channel-title is-draft">
<span class="chat-channel-title__name">{{this.channel.title}}</span> <span class="chat-channel-title__name">{{@channel.title}}</span>
{{#if (has-block)}} {{#if (has-block)}}
{{yield}} {{yield}}
{{/if}} {{/if}}
</div> </div>
{{else}} {{else}}
{{#if this.channel.isDirectMessageChannel}} {{#if @channel.isDirectMessageChannel}}
<div class="chat-channel-title is-dm"> <div class="chat-channel-title is-dm">
<div class="chat-channel-title__avatar"> <div class="chat-channel-title__avatar">
{{#if this.multiDm}} {{#if this.multiDm}}
<span class="chat-channel-title__users-count"> <span class="chat-channel-title__users-count">
{{this.channel.chatable.users.length}} {{@channel.chatable.users.length}}
</span> </span>
{{else}} {{else}}
<ChatUserAvatar @user={{this.channel.chatable.users.firstObject}} /> <ChatUserAvatar @user={{@channel.chatable.users.firstObject}} />
{{/if}} {{/if}}
</div> </div>
@ -24,12 +24,12 @@
{{#if this.multiDm}} {{#if this.multiDm}}
<span class="chat-channel-title__name">{{this.usernames}}</span> <span class="chat-channel-title__name">{{this.usernames}}</span>
{{else}} {{else}}
{{#let this.channel.chatable.users.firstObject as |user|}} {{#let @channel.chatable.users.firstObject as |user|}}
<span class="chat-channel-title__name">{{user.username}}</span> <span class="chat-channel-title__name">{{user.username}}</span>
{{#if this.showUserStatus}} {{#if this.showUserStatus}}
<UserStatusMessage <UserStatusMessage
@class="chat-channel-title__user-status-message" @class="chat-channel-title__user-status-message"
@status={{this.channel.chatable.users.firstObject.status}} @status={{@channel.chatable.users.firstObject.status}}
@showDescription={{if this.site.mobileView "true"}} @showDescription={{if this.site.mobileView "true"}}
/> />
{{/if}} {{/if}}
@ -48,19 +48,19 @@
{{yield}} {{yield}}
{{/if}} {{/if}}
</div> </div>
{{else if this.channel.isCategoryChannel}} {{else if @channel.isCategoryChannel}}
<div class="chat-channel-title is-category"> <div class="chat-channel-title is-category">
<span <span
class="chat-channel-title__category-badge" class="chat-channel-title__category-badge"
style={{this.channelColorStyle}} style={{this.channelColorStyle}}
> >
{{d-icon "d-chat"}} {{d-icon "d-chat"}}
{{#if this.channel.chatable.read_restricted}} {{#if @channel.chatable.read_restricted}}
{{d-icon "lock" class="chat-channel-title__restricted-category-icon"}} {{d-icon "lock" class="chat-channel-title__restricted-category-icon"}}
{{/if}} {{/if}}
</span> </span>
<span class="chat-channel-title__name"> <span class="chat-channel-title__name">
{{replace-emoji this.channel.title}} {{replace-emoji @channel.title}}
</span> </span>
{{#if (has-block)}} {{#if (has-block)}}

View File

@ -1,33 +1,24 @@
import Component from "@ember/component"; import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { computed } from "@ember/object";
import { gt, reads } from "@ember/object/computed";
export default class ChatChannelTitle extends Component { export default class ChatChannelTitle extends Component {
tagName = ""; get users() {
channel = null; return this.args.channel.chatable.users;
}
@reads("channel.chatable.users.[]") users; get multiDm() {
@gt("users.length", 1) multiDm; return this.users.length > 1;
}
@computed("users")
get usernames() { get usernames() {
return this.users.mapBy("username").join(", "); return this.users.mapBy("username").join(", ");
} }
@computed("channel.chatable.color")
get channelColorStyle() { get channelColorStyle() {
return htmlSafe(`color: #${this.channel.chatable.color}`); return htmlSafe(`color: #${this.args.channel.chatable.color}`);
} }
@computed(
"channel.chatable.users.length",
"channel.chatable.users.@each.status"
)
get showUserStatus() { get showUserStatus() {
return !!( return !!(this.users.length === 1 && this.users[0].status);
this.channel.chatable.users.length === 1 &&
this.channel.chatable.users[0].status
);
} }
} }

View File

@ -1,16 +1,16 @@
{{#if (gt @channel.currentUserMembership.unread_count 0)}} {{#if (gt @channel.currentUserMembership.unreadCount 0)}}
<div <div
class={{concat-class class={{concat-class
"chat-channel-unread-indicator" "chat-channel-unread-indicator"
(if (if
(or (or
@channel.isDirectMessageChannel @channel.isDirectMessageChannel
(gt @channel.currentUserMembership.unread_mentions 0) (gt @channel.currentUserMembership.unreadMentions 0)
) )
"urgent" "urgent"
) )
}} }}
> >
<div class="number">{{@channel.currentUserMembership.unread_count}}</div> <div class="number">{{@channel.currentUserMembership.unreadCount}}</div>
</div> </div>
{{/if}} {{/if}}

View File

@ -165,7 +165,7 @@ export default class ChatLivePane extends Component {
findArgs["targetMessageId"] = this.requestedTargetMessageId; findArgs["targetMessageId"] = this.requestedTargetMessageId;
} else if (fetchingFromLastRead) { } else if (fetchingFromLastRead) {
findArgs["targetMessageId"] = findArgs["targetMessageId"] =
this.args.channel.currentUserMembership.last_read_message_id; this.args.channel.currentUserMembership.lastReadMessageId;
} }
return this.chatApi return this.chatApi
@ -346,7 +346,7 @@ export default class ChatLivePane extends Component {
if ( if (
!foundFirstNew && !foundFirstNew &&
messageData.id > messageData.id >
this.args.channel.currentUserMembership.last_read_message_id && this.args.channel.currentUserMembership.lastReadMessageId &&
!channel.messages.some((m) => m.newest) !channel.messages.some((m) => m.newest)
) { ) {
foundFirstNew = true; foundFirstNew = true;
@ -444,7 +444,7 @@ export default class ChatLivePane extends Component {
} }
const lastReadId = const lastReadId =
this.args.channel.currentUserMembership?.last_read_message_id; this.args.channel.currentUserMembership?.lastReadMessageId;
let lastUnreadVisibleMessage = this.args.channel.visibleMessages.findLast( let lastUnreadVisibleMessage = this.args.channel.visibleMessages.findLast(
(message) => !lastReadId || message.id > lastReadId (message) => !lastReadId || message.id > lastReadId
); );

View File

@ -93,7 +93,7 @@
{{#if this.shouldRenderReplyingIndicator}} {{#if this.shouldRenderReplyingIndicator}}
<div class="chat-replying-indicator-container"> <div class="chat-replying-indicator-container">
<ChatReplyingIndicator @chatChannel={{@channel}} /> <ChatReplyingIndicator @channel={{@channel}} />
</div> </div>
{{/if}} {{/if}}

View File

@ -3,6 +3,7 @@ import { inject as service } from "@ember/service";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";
export default class ChatDraftChannelScreen extends Component { export default class ChatDraftChannelScreen extends Component {
@service chat; @service chat;
@service router; @service router;
@ -20,7 +21,7 @@ export default class ChatDraftChannelScreen extends Component {
@action @action
onSwitchFromDraftChannel(channel) { onSwitchFromDraftChannel(channel) {
channel.set("isDraft", false); channel.isDraft = false;
} }
_fetchPreviewedChannel(users) { _fetchPreviewedChannel(users) {
@ -29,20 +30,16 @@ export default class ChatDraftChannelScreen extends Component {
return this.chat return this.chat
.getDmChannelForUsernames(users.mapBy("username")) .getDmChannelForUsernames(users.mapBy("username"))
.then((response) => { .then((response) => {
this.set( const channel = ChatChannel.create(response.channel);
"previewedChannel", channel.isDraft = true;
ChatChannel.create( this.set("previewedChannel", channel);
Object.assign({}, response.channel, { isDraft: true })
)
);
}) })
.catch((error) => { .catch((error) => {
if (error?.jqXHR?.status === 404) { if (error?.jqXHR?.status === 404) {
this.set( this.set(
"previewedChannel", "previewedChannel",
ChatChannel.create({ ChatChannel.createDirectMessageChannelDraft({
chatable: { users: cloneJSON(users) }, users: cloneJSON(users),
isDraft: true,
}) })
); );
} }

View File

@ -1,10 +1,21 @@
<div class="chat-replying-indicator"> {{#if @channel}}
{{#if this.shouldDisplay}} <div
<span class="chat-replying-indicator__text">{{this.text}}</span> class={{concat-class
<span class="chat-replying-indicator__wave"> "chat-replying-indicator"
<span class="chat-replying-indicator__dot">.</span> (if this.presenceChannel.subscribed "is-subscribed")
<span class="chat-replying-indicator__dot">.</span> }}
<span class="chat-replying-indicator__dot">.</span> {{did-insert this.subscribe}}
</span> {{did-update this.handleDraft @channel.isDraft}}
{{/if}} {{did-update this.subscribe this.channelName}}
</div> {{will-destroy this.teardown}}
>
{{#if this.shouldRender}}
<span class="chat-replying-indicator__text">{{this.text}}</span>
<span class="chat-replying-indicator__wave">
<span class="chat-replying-indicator__dot">.</span>
<span class="chat-replying-indicator__dot">.</span>
<span class="chat-replying-indicator__dot">.</span>
</span>
{{/if}}
</div>
{{/if}}

View File

@ -1,89 +1,93 @@
import { isBlank, isPresent } from "@ember/utils"; import { isBlank, isPresent } from "@ember/utils";
import Component from "@ember/component"; import Component from "@glimmer/component";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
import { fmt } from "discourse/lib/computed"; import { action } from "@ember/object";
import { next } from "@ember/runloop"; import { tracked } from "@glimmer/tracking";
export default Component.extend({ export default class ChatReplyingIndicator extends Component {
tagName: "", @service currentUser;
presence: service(), @service presence;
presenceChannel: null,
chatChannel: null,
@discourseComputed("presenceChannel.users.[]") @tracked presenceChannel = null;
usernames(users) { @tracked users = [];
return users
?.filter((u) => u.id !== this.currentUser.id)
?.mapBy("username");
},
@discourseComputed("usernames.[]") @action
text(usernames) { async subscribe() {
if (isBlank(usernames)) { this.presenceChannel = this.presence.getChannel(this.channelName);
this.presenceChannel.on("change", this.handlePresenceChange);
await this.presenceChannel.subscribe();
}
@action
async resubscribe() {
await this.teardown();
await this.subscribe();
}
@action
handlePresenceChange(presenceChannel) {
this.users = presenceChannel.users || [];
}
@action
async handleDraft() {
if (this.args.channel.isDraft) {
await this.teardown();
} else {
await this.resubscribe();
}
}
@action
async teardown() {
if (this.presenceChannel) {
await this.presenceChannel.unsubscribe();
}
}
get usernames() {
return this.users
.filter((u) => u.id !== this.currentUser.id)
.mapBy("username");
}
get text() {
if (isBlank(this.usernames)) {
return; return;
} }
if (usernames.length === 1) { if (this.usernames.length === 1) {
return I18n.t("chat.replying_indicator.single_user", { return I18n.t("chat.replying_indicator.single_user", {
username: usernames[0], username: this.usernames[0],
}); });
} }
if (usernames.length < 4) { if (this.usernames.length < 4) {
const lastUsername = usernames.pop(); const lastUsername = this.usernames[this.usernames.length - 1];
const commaSeparatedUsernames = usernames.join( const commaSeparatedUsernames = this.usernames
I18n.t("word_connector.comma") .slice(0, this.usernames.length - 1)
); .join(I18n.t("word_connector.comma"));
return I18n.t("chat.replying_indicator.multiple_users", { return I18n.t("chat.replying_indicator.multiple_users", {
commaSeparatedUsernames, commaSeparatedUsernames,
lastUsername, lastUsername,
}); });
} }
const commaSeparatedUsernames = usernames const commaSeparatedUsernames = this.usernames
.slice(0, 2) .slice(0, 2)
.join(I18n.t("word_connector.comma")); .join(I18n.t("word_connector.comma"));
return I18n.t("chat.replying_indicator.many_users", { return I18n.t("chat.replying_indicator.many_users", {
commaSeparatedUsernames, commaSeparatedUsernames,
count: usernames.length - 2, count: this.usernames.length - 2,
}); });
}, }
@discourseComputed("usernames.[]") get shouldRender() {
shouldDisplay(usernames) { return isPresent(this.usernames);
return isPresent(usernames); }
},
channelName: fmt("chatChannel.id", "/chat-reply/%@"), get channelName() {
return `/chat-reply/${this.args.channel.id}`;
didReceiveAttrs() { }
this._super(...arguments); }
if (!this.chatChannel || this.chatChannel.isDraft) {
this.presenceChannel?.unsubscribe();
return;
}
if (this.presenceChannel?.name !== this.channelName) {
this.presenceChannel?.unsubscribe();
next(() => {
if (this.isDestroyed || this.isDestroying) {
return;
}
const presenceChannel = this.presence.getChannel(this.channelName);
this.set("presenceChannel", presenceChannel);
presenceChannel.subscribe();
});
}
},
willDestroyElement() {
this._super(...arguments);
this.presenceChannel?.unsubscribe();
},
});

View File

@ -75,14 +75,14 @@ export default class ChatSelectionManager extends Component {
const openOpts = {}; const openOpts = {};
if (this.chatChannel.isCategoryChannel) { if (this.chatChannel.isCategoryChannel) {
openOpts.categoryId = this.chatChannel.chatable_id; openOpts.categoryId = this.chatChannel.chatableId;
} }
if (this.site.mobileView) { if (this.site.mobileView) {
// go to the relevant chatable (e.g. category) and open the // go to the relevant chatable (e.g. category) and open the
// composer to insert text // composer to insert text
if (this.chatChannel.chatable_url) { if (this.chatChannel.chatableUrl) {
this.router.transitionTo(this.chatChannel.chatable_url); this.router.transitionTo(this.chatChannel.chatableUrl);
} }
await composer.focusComposer({ await composer.focusComposer({

View File

@ -8,7 +8,7 @@ import { INPUT_DELAY } from "discourse-common/config/environment";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { gt, not } from "@ember/object/computed"; import { gt, not } from "@ember/object/computed";
import { createDirectMessageChannelDraft } from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
export default Component.extend({ export default Component.extend({
tagName: "", tagName: "",
@ -29,7 +29,7 @@ export default Component.extend({
this.set("users", []); this.set("users", []);
this.set("selectedUsers", []); this.set("selectedUsers", []);
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
}, },
didInsertElement() { didInsertElement() {

View File

@ -154,7 +154,7 @@ export default {
} }
const highlightable = [`@${this.currentUser.username}`]; const highlightable = [`@${this.currentUser.username}`];
if (chatChannel.allow_channel_wide_mentions) { if (chatChannel.allowChannelWideMentions) {
highlightable.push(...MENTION_KEYWORDS.map((k) => `@${k}`)); highlightable.push(...MENTION_KEYWORDS.map((k) => `@${k}`));
} }

View File

@ -88,13 +88,13 @@ export default {
} }
get suffixValue() { get suffixValue() {
return this.channel.currentUserMembership.unread_count > 0 return this.channel.currentUserMembership.unreadCount > 0
? "circle" ? "circle"
: ""; : "";
} }
get suffixCSSClass() { get suffixCSSClass() {
return this.channel.currentUserMembership.unread_mentions > 0 return this.channel.currentUserMembership.unreadMentions > 0
? "urgent" ? "urgent"
: "unread"; : "unread";
} }
@ -282,7 +282,7 @@ export default {
} }
get suffixValue() { get suffixValue() {
return this.channel.currentUserMembership.unread_count > 0 return this.channel.currentUserMembership.unreadCount > 0
? "circle" ? "circle"
: ""; : "";
} }

View File

@ -0,0 +1,21 @@
import { tracked } from "@glimmer/tracking";
export default class ChatChannelArchive {
static create(args = {}) {
return new ChatChannelArchive(args);
}
@tracked failed;
@tracked completed;
@tracked messages;
@tracked topicId;
@tracked totalMessages;
constructor(args = {}) {
this.failed = args.archive_failed;
this.completed = args.archive_completed;
this.messages = args.archived_messages;
this.topicId = args.archive_topic_id;
this.totalMessages = args.total_messages;
}
}

View File

@ -1,5 +1,3 @@
import RestModel from "discourse/models/rest";
import User from "discourse/models/user";
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
@ -10,6 +8,9 @@ import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messa
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
import guid from "pretty-text/guid"; import guid from "pretty-text/guid";
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread"; import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
import ChatDirectMessage from "discourse/plugins/chat/discourse/models/chat-direct-message";
import ChatChannelArchive from "discourse/plugins/chat/discourse/models/chat-channel-archive";
import Category from "discourse/models/category";
export const CHATABLE_TYPES = { export const CHATABLE_TYPES = {
directMessageChannel: "DirectMessage", directMessageChannel: "DirectMessage",
@ -49,25 +50,79 @@ const READONLY_STATUSES = [
CHANNEL_STATUSES.archived, CHANNEL_STATUSES.archived,
]; ];
export default class ChatChannel extends RestModel { export default class ChatChannel {
static create(args = {}) {
return new ChatChannel(args);
}
static createDirectMessageChannelDraft(args = {}) {
const channel = ChatChannel.create({
chatable_type: CHATABLE_TYPES.directMessageChannel,
chatable: {
users: args.users || [],
},
});
channel.isDraft = true;
return channel;
}
@tracked currentUserMembership = null; @tracked currentUserMembership = null;
@tracked isDraft = false; @tracked isDraft = false;
@tracked title; @tracked title;
@tracked slug;
@tracked description; @tracked description;
@tracked chatableType;
@tracked status; @tracked status;
@tracked activeThread; @tracked activeThread = null;
@tracked lastMessageSentAt; @tracked lastMessageSentAt;
@tracked canDeleteOthers; @tracked canDeleteOthers;
@tracked canDeleteSelf; @tracked canDeleteSelf;
@tracked canFlag; @tracked canFlag;
@tracked canModerate; @tracked canModerate;
@tracked userSilenced; @tracked userSilenced;
@tracked draft; @tracked draft = null;
@tracked meta;
@tracked chatableType;
@tracked chatableUrl;
@tracked autoJoinUsers = false;
@tracked allowChannelWideMentions = true;
@tracked membershipsCount = 0;
@tracked archive;
threadsManager = new ChatThreadsManager(getOwner(this)); threadsManager = new ChatThreadsManager(getOwner(this));
messagesManager = new ChatMessagesManager(getOwner(this)); messagesManager = new ChatMessagesManager(getOwner(this));
constructor(args = {}) {
this.id = args.id;
this.chatableId = args.chatable_id;
this.chatableUrl = args.chatable_url;
this.chatableType = args.chatable_type;
this.membershipsCount = args.memberships_count;
this.meta = args.meta;
this.slug = args.slug;
this.title = args.title;
this.status = args.status;
this.canDeleteSelf = args.can_delete_self;
this.canDeleteOthers = args.can_delete_others;
this.canFlag = args.can_flag;
this.userSilenced = args.user_silenced;
this.canModerate = args.can_moderate;
this.description = args.description;
this.lastMessageSentAt = args.last_message_sent_at;
this.threadingEnabled = args.threading_enabled;
this.autoJoinUsers = args.auto_join_users;
this.allowChannelWideMentions = args.allow_channel_wide_mentions;
this.chatable = this.isDirectMessageChannel
? ChatDirectMessage.create(args)
: Category.create(args.chatable);
this.currentUserMembership = UserChatChannelMembership.create(
args.current_user_membership
);
if (args.archive_completed || args.archive_failed) {
this.archive = ChatChannelArchive.create(args);
}
}
findIndexOfMessage(id) { findIndexOfMessage(id) {
return this.messagesManager.findIndexOfMessage(id); return this.messagesManager.findIndexOfMessage(id);
} }
@ -223,14 +278,14 @@ export default class ChatChannel extends RestModel {
updateMembership(membership) { updateMembership(membership) {
this.currentUserMembership.following = membership.following; this.currentUserMembership.following = membership.following;
this.currentUserMembership.last_read_message_id = this.currentUserMembership.lastReadMessage_id =
membership.last_read_message_id; membership.last_read_message_id;
this.currentUserMembership.desktop_notification_level = this.currentUserMembership.desktopNotificationLevel =
membership.desktop_notification_level; membership.desktop_notification_level;
this.currentUserMembership.mobile_notification_level = this.currentUserMembership.mobileNotificationLevel =
membership.mobile_notification_level; membership.mobile_notification_level;
this.currentUserMembership.unread_count = membership.unread_count; this.currentUserMembership.unreadCount = membership.unread_count;
this.currentUserMembership.unread_mentions = membership.unread_mentions; this.currentUserMembership.unreadMentions = membership.unread_mentions;
this.currentUserMembership.muted = membership.muted; this.currentUserMembership.muted = membership.muted;
} }
@ -239,7 +294,7 @@ export default class ChatChannel extends RestModel {
return; return;
} }
if (this.currentUserMembership.last_read_message_id >= messageId) { if (this.currentUserMembership.lastReadMessageId >= messageId) {
return; return;
} }
@ -250,59 +305,3 @@ export default class ChatChannel extends RestModel {
}); });
} }
} }
ChatChannel.reopenClass({
create(args) {
args = args || {};
this._initUserModels(args);
this._initUserMembership(args);
this._remapKey(args, "chatable_type", "chatableType");
this._remapKey(args, "memberships_count", "membershipsCount");
this._remapKey(args, "last_message_sent_at", "lastMessageSentAt");
this._remapKey(args, "threading_enabled", "threadingEnabled");
return this._super(args);
},
_remapKey(obj, oldKey, newKey) {
delete Object.assign(obj, { [newKey]: obj[oldKey] })[oldKey];
},
_initUserModels(args) {
if (args.chatable?.users?.length) {
for (let i = 0; i < args.chatable?.users?.length; i++) {
const userData = args.chatable.users[i];
args.chatable.users[i] = User.create(userData);
}
}
},
_initUserMembership(args) {
if (args.currentUserMembership instanceof UserChatChannelMembership) {
return;
}
args.currentUserMembership = UserChatChannelMembership.create(
args.current_user_membership || {
following: false,
muted: false,
unread_count: 0,
unread_mentions: 0,
}
);
delete args.current_user_membership;
},
});
export function createDirectMessageChannelDraft() {
return ChatChannel.create({
isDraft: true,
chatable_type: CHATABLE_TYPES.directMessageChannel,
chatable: {
users: [],
},
});
}

View File

@ -0,0 +1,26 @@
import User from "discourse/models/user";
import { tracked } from "@glimmer/tracking";
export default class ChatDirectMessage {
static create(args = {}) {
return new ChatDirectMessage(args);
}
@tracked id;
@tracked users = null;
constructor(args = {}) {
this.id = args.chatable.id;
this.users = this.#initUsers(args.chatable.users || []);
}
#initUsers(users) {
return users.map((user) => {
if (!user || user instanceof User) {
return user;
}
return User.create(user);
});
}
}

View File

@ -166,7 +166,7 @@ export default class ChatMessage {
} }
get read() { get read() {
return this.channel.currentUserMembership?.last_read_message_id >= this.id; return this.channel.currentUserMembership?.lastReadMessageId >= this.id;
} }
get firstMessageOfTheDayAt() { get firstMessageOfTheDayAt() {

View File

@ -1,28 +1,36 @@
import RestModel from "discourse/models/rest";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import User from "discourse/models/user"; import User from "discourse/models/user";
export default class UserChatChannelMembership extends RestModel {
export default class UserChatChannelMembership {
static create(args = {}) {
return new UserChatChannelMembership(args);
}
@tracked following = false; @tracked following = false;
@tracked muted = false; @tracked muted = false;
@tracked unread_count = 0; @tracked unreadCount = 0;
@tracked unread_mentions = 0; @tracked unreadMentions = 0;
@tracked desktop_notification_level = null; @tracked desktopNotificationLevel = null;
@tracked mobile_notification_level = null; @tracked mobileNotificationLevel = null;
@tracked last_read_message_id = null; @tracked lastReadMessageId = null;
} @tracked user = null;
UserChatChannelMembership.reopenClass({ constructor(args = {}) {
create(args) { this.following = args.following;
args = args || {}; this.muted = args.muted;
this._initUser(args); this.unreadCount = args.unread_count;
return this._super(args); this.unreadMentions = args.unread_mentions;
}, this.desktopNotificationLevel = args.desktop_notification_level;
this.mobileNotificationLevel = args.mobile_notification_level;
this.lastReadMessageId = args.last_read_message_id;
this.user = this.#initUserModel(args.user);
}
_initUser(args) { #initUserModel(user) {
if (!args.user || args.user instanceof User) { if (!user || user instanceof User) {
return; return user;
} }
args.user = User.create(args.user); return User.create(user);
}, }
}); }

View File

@ -61,11 +61,10 @@ export default class ChatChannelsManager extends Service {
return this.chatApi.followChannel(model.id).then((membership) => { return this.chatApi.followChannel(model.id).then((membership) => {
model.currentUserMembership.following = membership.following; model.currentUserMembership.following = membership.following;
model.currentUserMembership.muted = membership.muted; model.currentUserMembership.muted = membership.muted;
model.currentUserMembership.desktop_notification_level = model.currentUserMembership.desktopNotificationLevel =
membership.desktop_notification_level; membership.desktopNotificationLevel;
model.currentUserMembership.mobile_notification_level = model.currentUserMembership.mobileNotificationLevel =
membership.mobile_notification_level; membership.mobileNotificationLevel;
return model; return model;
}); });
} else { } else {
@ -97,7 +96,7 @@ export default class ChatChannelsManager extends Service {
get unreadCount() { get unreadCount() {
let count = 0; let count = 0;
this.publicMessageChannels.forEach((channel) => { this.publicMessageChannels.forEach((channel) => {
count += channel.currentUserMembership.unread_count || 0; count += channel.currentUserMembership.unreadCount || 0;
}); });
return count; return count;
} }
@ -106,9 +105,9 @@ export default class ChatChannelsManager extends Service {
let count = 0; let count = 0;
this.channels.forEach((channel) => { this.channels.forEach((channel) => {
if (channel.isDirectMessageChannel) { if (channel.isDirectMessageChannel) {
count += channel.currentUserMembership.unread_count || 0; count += channel.currentUserMembership.unreadCount || 0;
} }
count += channel.currentUserMembership.unread_mentions || 0; count += channel.currentUserMembership.unreadMentions || 0;
}); });
return count; return count;
} }
@ -159,8 +158,8 @@ export default class ChatChannelsManager extends Service {
#sortDirectMessageChannels(channels) { #sortDirectMessageChannels(channels) {
return channels.sort((a, b) => { return channels.sort((a, b) => {
const unreadCountA = a.currentUserMembership.unread_count || 0; const unreadCountA = a.currentUserMembership.unreadCount || 0;
const unreadCountB = b.currentUserMembership.unread_count || 0; const unreadCountB = b.currentUserMembership.unreadCount || 0;
if (unreadCountA === unreadCountB) { if (unreadCountA === unreadCountB) {
return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt) return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt)
? -1 ? -1

View File

@ -2,6 +2,7 @@ import Service, { inject as service } from "@ember/service";
import I18n from "I18n"; import I18n from "I18n";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
import ChatChannelArchive from "../models/chat-channel-archive";
export default class ChatSubscriptionsManager extends Service { export default class ChatSubscriptionsManager extends Service {
@service store; @service store;
@ -125,13 +126,7 @@ export default class ChatSubscriptionsManager extends Service {
return; return;
} }
channel.setProperties({ channel.archive = ChatChannelArchive.create(busData);
archive_failed: busData.archive_failed,
archive_completed: busData.archive_completed,
archived_messages: busData.archived_messages,
archive_topic_id: busData.archive_topic_id,
total_messages: busData.total_messages,
});
}); });
} }
@ -139,8 +134,8 @@ export default class ChatSubscriptionsManager extends Service {
_onNewMentions(busData) { _onNewMentions(busData) {
this.chatChannelsManager.find(busData.channel_id).then((channel) => { this.chatChannelsManager.find(busData.channel_id).then((channel) => {
const membership = channel.currentUserMembership; const membership = channel.currentUserMembership;
if (busData.message_id > membership?.last_read_message_id) { if (busData.message_id > membership?.lastReadMessageId) {
membership.unread_mentions = (membership.unread_mentions || 0) + 1; membership.unreadMentions = (membership.unreadMentions || 0) + 1;
} }
}); });
} }
@ -186,20 +181,18 @@ export default class ChatSubscriptionsManager extends Service {
this.chatChannelsManager.find(busData.channel_id).then((channel) => { this.chatChannelsManager.find(busData.channel_id).then((channel) => {
if (busData.user_id === this.currentUser.id) { if (busData.user_id === this.currentUser.id) {
// User sent message, update tracking state to no unread // User sent message, update tracking state to no unread
channel.currentUserMembership.last_read_message_id = busData.message_id; channel.currentUserMembership.lastReadMessageId = busData.message_id;
} else { } else {
// Ignored user sent message, update tracking state to no unread // Ignored user sent message, update tracking state to no unread
if (this.currentUser.ignored_users.includes(busData.username)) { if (this.currentUser.ignored_users.includes(busData.username)) {
channel.currentUserMembership.last_read_message_id = channel.currentUserMembership.lastReadMessageId = busData.message_id;
busData.message_id;
} else { } else {
// Message from other user. Increment trackings state // Message from other user. Increment trackings state
if ( if (
busData.message_id > busData.message_id >
(channel.currentUserMembership.last_read_message_id || 0) (channel.currentUserMembership.lastReadMessageId || 0)
) { ) {
channel.currentUserMembership.unread_count = channel.currentUserMembership.unreadCount++;
channel.currentUserMembership.unread_count + 1;
} }
} }
} }
@ -256,11 +249,10 @@ export default class ChatSubscriptionsManager extends Service {
@bind @bind
_updateChannelTrackingData(channelId, trackingData) { _updateChannelTrackingData(channelId, trackingData) {
this.chatChannelsManager.find(channelId).then((channel) => { this.chatChannelsManager.find(channelId).then((channel) => {
channel.currentUserMembership.last_read_message_id = channel.currentUserMembership.lastReadMessageId =
trackingData.last_read_message_id; trackingData.last_read_message_id;
channel.currentUserMembership.unread_count = trackingData.unread_count; channel.currentUserMembership.unreadCount = trackingData.unread_count;
channel.currentUserMembership.unread_mentions = channel.currentUserMembership.unreadMentions = trackingData.mention_count;
trackingData.mention_count;
}); });
} }
@ -289,7 +281,7 @@ export default class ChatSubscriptionsManager extends Service {
channel.isDirectMessageChannel && channel.isDirectMessageChannel &&
!channel.currentUserMembership.following !channel.currentUserMembership.following
) { ) {
channel.currentUserMembership.unread_count = 1; channel.currentUserMembership.unreadCount = 1;
} }
this.chatChannelsManager.follow(channel); this.chatChannelsManager.follow(channel);
@ -341,9 +333,7 @@ export default class ChatSubscriptionsManager extends Service {
.find(busData.chat_channel_id, { fetchIfNotFound: false }) .find(busData.chat_channel_id, { fetchIfNotFound: false })
.then((channel) => { .then((channel) => {
if (channel) { if (channel) {
channel.setProperties({ channel.membershipsCount = busData.memberships_count;
memberships_count: busData.memberships_count,
});
this.appEvents.trigger("chat:refresh-channel-members"); this.appEvents.trigger("chat:refresh-channel-members");
} }
}); });
@ -353,11 +343,9 @@ export default class ChatSubscriptionsManager extends Service {
_onChannelEdits(busData) { _onChannelEdits(busData) {
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => { this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
if (channel) { if (channel) {
channel.setProperties({ channel.title = busData.name;
title: busData.name, channel.description = busData.description;
description: busData.description, channel.slug = busData.slug;
slug: busData.slug,
});
} }
}); });
} }
@ -365,15 +353,15 @@ export default class ChatSubscriptionsManager extends Service {
@bind @bind
_onChannelStatus(busData) { _onChannelStatus(busData) {
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => { this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
channel.set("status", busData.status); channel.status = busData.status;
// it is not possible for the user to set their last read message id // 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 // if the channel has been archived, because all the messages have
// been deleted. we don't want them seeing the blue dot anymore so // been deleted. we don't want them seeing the blue dot anymore so
// just completely reset the unreads // just completely reset the unreads
if (busData.status === CHANNEL_STATUSES.archived) { if (busData.status === CHANNEL_STATUSES.archived) {
channel.currentUserMembership.unread_count = 0; channel.currentUserMembership.unreadCount = 0;
channel.currentUserMembership.unread_mentions = 0; channel.currentUserMembership.unreadMentions = 0;
} }
}); });
} }

View File

@ -283,7 +283,7 @@ export default class Chat extends Service {
const membership = channel.currentUserMembership; const membership = channel.currentUserMembership;
if (channel.isDirectMessageChannel) { if (channel.isDirectMessageChannel) {
if (!dmChannelWithUnread && membership.unread_count > 0) { if (!dmChannelWithUnread && membership.unreadCount > 0) {
dmChannelWithUnread = channel.id; dmChannelWithUnread = channel.id;
} else if (!dmChannel) { } else if (!dmChannel) {
dmChannel = channel.id; dmChannel = channel.id;
@ -292,7 +292,7 @@ export default class Chat extends Service {
if (membership.unread_mentions > 0) { if (membership.unread_mentions > 0) {
publicChannelWithMention = channel.id; publicChannelWithMention = channel.id;
return; // <- We have a public channel with a mention. Break and return this. return; // <- We have a public channel with a mention. Break and return this.
} else if (!publicChannelWithUnread && membership.unread_count > 0) { } else if (!publicChannelWithUnread && membership.unreadCount > 0) {
publicChannelWithUnread = channel.id; publicChannelWithUnread = channel.id;
} else if ( } else if (
!defaultChannel && !defaultChannel &&

View File

@ -20,6 +20,7 @@ RSpec.describe "Channel selector modal", type: :system, js: true do
find("body").send_keys([key_modifier, "k"]) find("body").send_keys([key_modifier, "k"])
find("#chat-channel-selector-input").fill_in(with: channel_1.title) find("#chat-channel-selector-input").fill_in(with: channel_1.title)
find(".chat-channel-selection-row[data-id='#{channel_1.id}']").click find(".chat-channel-selection-row[data-id='#{channel_1.id}']").click
channel_page.send_message("Hello world") channel_page.send_message("Hello world")
expect(channel_page).to have_message(text: "Hello world") expect(channel_page).to have_message(text: "Hello world")
@ -33,6 +34,7 @@ RSpec.describe "Channel selector modal", type: :system, js: true do
find("body").send_keys([key_modifier, "k"]) find("body").send_keys([key_modifier, "k"])
find("#chat-channel-selector-input").fill_in(with: user_1.username) find("#chat-channel-selector-input").fill_in(with: user_1.username)
find(".chat-channel-selection-row[data-id='#{user_1.id}']").click find(".chat-channel-selection-row[data-id='#{user_1.id}']").click
channel_page.send_message("Hello world") channel_page.send_message("Hello world")
expect(channel_page).to have_message(text: "Hello world") expect(channel_page).to have_message(text: "Hello world")
@ -69,7 +71,6 @@ RSpec.describe "Channel selector modal", type: :system, js: true do
fab!(:channel_1) { Fabricate(:private_category_channel, group: group_1) } fab!(:channel_1) { Fabricate(:private_category_channel, group: group_1) }
it "it doesnt include limited access channel" do it "it doesnt include limited access channel" do
chat_page.visit_channel(channel_1)
find("body").send_keys([key_modifier, "k"]) find("body").send_keys([key_modifier, "k"])
find("#chat-channel-selector-input").fill_in(with: channel_1.title) find("#chat-channel-selector-input").fill_in(with: channel_1.title)

View File

@ -126,11 +126,11 @@ RSpec.describe "Drawer", type: :system, js: true do
session.quit session.quit
end end
expect(page).to have_content("onlyonce", count: 1) expect(page).to have_content("onlyonce", count: 1, wait: 20)
chat_page.visit_channel(channel_2) chat_page.visit_channel(channel_2)
expect(page).to have_content("onlyonce", count: 0) expect(page).to have_content("onlyonce", count: 0, wait: 20)
end end
end end
end end

View File

@ -10,15 +10,13 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.set("channel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.channel.set( this.channel.description =
"description", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
);
}); });
test("escapes channel title", async function (assert) { test("escapes channel title", async function (assert) {
this.channel.set("title", "<div class='xss'>evil</div>"); this.channel.title = "<div class='xss'>evil</div>";
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
@ -26,7 +24,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("escapes channel description", async function (assert) { test("escapes channel description", async function (assert) {
this.channel.set("description", "<div class='xss'>evil</div>"); this.channel.description = "<div class='xss'>evil</div>";
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
@ -34,14 +32,14 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("Closed channel", async function (assert) { test("Closed channel", async function (assert) {
this.channel.set("status", "closed"); this.channel.status = "closed";
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.true(exists(".chat-channel-card.-closed")); assert.true(exists(".chat-channel-card.-closed"));
}); });
test("Archived channel", async function (assert) { test("Archived channel", async function (assert) {
this.channel.set("status", "archived"); this.channel.status = "archived";
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.true(exists(".chat-channel-card.-archived")); assert.true(exists(".chat-channel-card.-archived"));
@ -59,7 +57,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("Joined channel", async function (assert) { test("Joined channel", async function (assert) {
this.channel.currentUserMembership.set("following", true); this.channel.currentUserMembership.following = true;
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.strictEqual( assert.strictEqual(
@ -77,7 +75,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("Memberships count", async function (assert) { test("Memberships count", async function (assert) {
this.channel.set("membershipsCount", 4); this.channel.membershipsCount = 4;
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.strictEqual( assert.strictEqual(
@ -87,7 +85,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("No description", async function (assert) { test("No description", async function (assert) {
this.channel.set("description", null); this.channel.description = null;
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.false(exists(".chat-channel-card__description")); assert.false(exists(".chat-channel-card__description"));
@ -118,7 +116,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
}); });
test("Read restricted chatable", async function (assert) { test("Read restricted chatable", async function (assert) {
this.channel.set("chatable.read_restricted", true); this.channel.chatable.read_restricted = true;
await render(hbs`<ChatChannelCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelCard @channel={{this.channel}} />`);
assert.true(exists(".d-icon-lock")); assert.true(exists(".d-icon-lock"));

View File

@ -5,20 +5,15 @@ import hbs from "htmlbars-inline-precompile";
import pretender from "discourse/tests/helpers/create-pretender"; import pretender from "discourse/tests/helpers/create-pretender";
import I18n from "I18n"; import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import fabricators from "../helpers/fabricators";
module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) { module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("accepts an optional onLeaveChannel callback", async function (assert) { test("accepts an optional onLeaveChannel callback", async function (assert) {
this.set("foo", 1); this.foo = 1;
this.set("onLeaveChannel", () => this.set("foo", 2)); this.onLeaveChannel = () => (this.foo = 2);
this.set("channel", { this.channel = fabricators.directMessageChatChannel({ users: [{ id: 1 }] });
id: 1,
chatable_type: "DirectMessage",
chatable: {
users: [{ id: 1 }],
},
});
await render( await render(
hbs`<ChatChannelLeaveBtn @channel={{this.channel}} @onLeaveChannel={{this.onLeaveChannel}} />` hbs`<ChatChannelLeaveBtn @channel={{this.channel}} @onLeaveChannel={{this.onLeaveChannel}} />`
@ -34,7 +29,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
}); });
test("has a specific title for direct message channel", async function (assert) { test("has a specific title for direct message channel", async function (assert) {
this.set("channel", { chatable_type: "DirectMessage" }); this.channel = fabricators.directMessageChatChannel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -43,7 +38,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
}); });
test("has a specific title for message channel", async function (assert) { test("has a specific title for message channel", async function (assert) {
this.set("channel", { chatable_type: "Topic" }); this.channel = fabricators.chatChannel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -53,7 +48,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
test("is not visible on mobile", async function (assert) { test("is not visible on mobile", async function (assert) {
this.site.mobileView = true; this.site.mobileView = true;
this.set("channel", { chatable_type: "Topic" }); this.channel = fabricators.chatChannel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);

View File

@ -29,7 +29,7 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
test("unreadIndicator", async function (assert) { test("unreadIndicator", async function (assert) {
this.channel = fabricators.directMessageChatChannel(); this.channel = fabricators.directMessageChatChannel();
this.channel.currentUserMembership.unread_count = 1; this.channel.currentUserMembership.unreadCount = 1;
this.unreadIndicator = true; this.unreadIndicator = true;
await render( await render(

View File

@ -15,10 +15,9 @@ module(
"channel", "channel",
fabricators.chatChannel({ chatable_type: "Category" }) fabricators.chatChannel({ chatable_type: "Category" })
); );
this.channel.setProperties({
description: "Important stuff is announced here.", this.channel.description = "Important stuff is announced here.";
title: "announcements", this.channel.title = "announcements";
});
this.currentUser.set("has_chat_enabled", true); this.currentUser.set("has_chat_enabled", true);
this.siteSettings.chat_enabled = true; this.siteSettings.chat_enabled = true;
}); });
@ -49,7 +48,7 @@ module(
}); });
test("no channel description", async function (assert) { test("no channel description", async function (assert) {
this.channel.set("description", null); this.channel.description = null;
await render(hbs`<ChatChannelPreviewCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelPreviewCard @channel={{this.channel}} />`);
@ -83,7 +82,7 @@ module(
}); });
test("closed channel", async function (assert) { test("closed channel", async function (assert) {
this.channel.set("status", "closed"); this.channel.status = "closed";
await render(hbs`<ChatChannelPreviewCard @channel={{this.channel}} />`); await render(hbs`<ChatChannelPreviewCard @channel={{this.channel}} />`);
assert.false( assert.false(

View File

@ -131,7 +131,7 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
assert.dom(".chat-channel-row").doesNotHaveClass("has-unread"); assert.dom(".chat-channel-row").doesNotHaveClass("has-unread");
this.categoryChatChannel.currentUserMembership.unread_count = 1; this.categoryChatChannel.currentUserMembership.unreadCount = 1;
await render(hbs`<ChatChannelRow @channel={{this.categoryChatChannel}} />`); await render(hbs`<ChatChannelRow @channel={{this.categoryChatChannel}} />`);

View File

@ -10,12 +10,9 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("category channel", async function (assert) { test("category channel", async function (assert) {
this.set( this.channel = fabricators.chatChannel({
"channel", chatable_type: CHATABLE_TYPES.categoryChannel,
fabricators.chatChannel({ });
chatable_type: CHATABLE_TYPES.categoryChannel,
})
);
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -30,13 +27,10 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - escapes title", async function (assert) { test("category channel - escapes title", async function (assert) {
this.set( this.channel = fabricators.chatChannel({
"channel", chatable_type: CHATABLE_TYPES.categoryChannel,
fabricators.chatChannel({ title: "<div class='xss'>evil</div>",
chatable_type: CHATABLE_TYPES.categoryChannel, });
title: "<div class='xss'>evil</div>",
})
);
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -44,13 +38,10 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - read restricted", async function (assert) { test("category channel - read restricted", async function (assert) {
this.set( this.channel = fabricators.chatChannel({
"channel", chatable_type: CHATABLE_TYPES.categoryChannel,
fabricators.chatChannel({ chatable: { read_restricted: true },
chatable_type: CHATABLE_TYPES.categoryChannel, });
chatable: { read_restricted: true },
})
);
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -58,13 +49,10 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - not read restricted", async function (assert) { test("category channel - not read restricted", async function (assert) {
this.set( this.channel = fabricators.chatChannel({
"channel", chatable_type: CHATABLE_TYPES.categoryChannel,
fabricators.chatChannel({ chatable: { read_restricted: false },
chatable_type: CHATABLE_TYPES.categoryChannel, });
chatable: { read_restricted: false },
})
);
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -72,7 +60,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("direct message channel - one user", async function (assert) { test("direct message channel - one user", async function (assert) {
this.set("channel", fabricators.directMessageChatChannel()); this.channel = fabricators.directMessageChatChannel();
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -98,7 +86,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
avatar_template: "/letter_avatar_proxy/v3/letter/t/31188e/{size}.png", avatar_template: "/letter_avatar_proxy/v3/letter/t/31188e/{size}.png",
}); });
this.set("channel", channel); this.channel = channel;
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);

View File

@ -11,8 +11,8 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
function generateMessageProps(messageData = {}) { function generateMessageProps(messageData = {}) {
const chatChannel = ChatChannel.create({ const channel = ChatChannel.create({
chatable: { id: 1 }, chatable_id: 1,
chatable_type: "Category", chatable_type: "Category",
id: 9, id: 9,
title: "Site", title: "Site",
@ -21,15 +21,15 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
unread_count: 0, unread_count: 0,
muted: false, muted: false,
}, },
canDeleteSelf: true, can_delete_self: true,
canDeleteOthers: true, can_delete_others: true,
canFlag: true, can_flag: true,
userSilenced: false, user_silenced: false,
canModerate: true, can_moderate: true,
}); });
return { return {
message: ChatMessage.create( message: ChatMessage.create(
chatChannel, channel,
Object.assign( Object.assign(
{ {
id: 178, id: 178,
@ -44,7 +44,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
messageData messageData
) )
), ),
chatChannel, channel,
afterExpand: () => {}, afterExpand: () => {},
onHoverMessage: () => {}, onHoverMessage: () => {},
messageDidEnterViewport: () => {}, messageDidEnterViewport: () => {},
@ -55,7 +55,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
const template = hbs` const template = hbs`
<ChatMessage <ChatMessage
@message={{this.message}} @message={{this.message}}
@channel={{this.chatChannel}} @channel={{this.channel}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}
/> />
@ -64,6 +64,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
test("Message with edits", async function (assert) { test("Message with edits", async function (assert) {
this.setProperties(generateMessageProps({ edited: true })); this.setProperties(generateMessageProps({ edited: true }));
await render(template); await render(template);
assert.true( assert.true(
exists(".chat-message-edited"), exists(".chat-message-edited"),
"has the correct edited css class" "has the correct edited css class"
@ -83,6 +84,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
test("Hidden message", async function (assert) { test("Hidden message", async function (assert) {
this.setProperties(generateMessageProps({ hidden: true })); this.setProperties(generateMessageProps({ hidden: true }));
await render(template); await render(template);
assert.true( assert.true(
exists(".chat-message-hidden .chat-message-expand"), exists(".chat-message-hidden .chat-message-expand"),
"has the correct hidden css class and expand button within" "has the correct hidden css class and expand button within"

View File

@ -2,9 +2,9 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators"; import fabricators from "../helpers/fabricators";
import MockPresenceChannel from "../helpers/mock-presence-channel";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render, settled } from "@ember/test-helpers";
import { joinChannel } from "discourse/tests/helpers/presence-pretender";
module( module(
"Discourse Chat | Component | chat-replying-indicator", "Discourse Chat | Component | chat-replying-indicator",
@ -12,155 +12,148 @@ module(
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("not displayed when no one is replying", async function (assert) { test("not displayed when no one is replying", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
assert.false(exists(".chat-replying-indicator__text")); assert.false(exists(".chat-replying-indicator__text"));
}); });
test("displays indicator when user is replying", async function (assert) { test("displays indicator when user is replying", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
const sam = { id: 1, username: "sam" }; await joinChannel("/chat-reply/1", {
this.set("presenceChannel.users", [sam]); id: 1,
avatar_template: "/images/avatar.png",
username: "sam",
});
assert.strictEqual( assert.strictEqual(
query(".chat-replying-indicator__text").innerText, query(".chat-replying-indicator__text").innerText,
`${sam.username} is typing` `sam is typing`
); );
}); });
test("displays indicator when 2 or 3 users are replying", async function (assert) { test("displays indicator when 2 or 3 users are replying", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
const sam = { id: 1, username: "sam" }; await joinChannel("/chat-reply/1", {
const mark = { id: 2, username: "mark" }; id: 1,
this.set("presenceChannel.users", [sam, mark]); avatar_template: "/images/avatar.png",
username: "sam",
});
await joinChannel("/chat-reply/1", {
id: 2,
avatar_template: "/images/avatar.png",
username: "mark",
});
assert.strictEqual( assert.strictEqual(
query(".chat-replying-indicator__text").innerText, query(".chat-replying-indicator__text").innerText,
`${sam.username} and ${mark.username} are typing` `sam and mark are typing`
); );
}); });
test("displays indicator when 3 users are replying", async function (assert) { test("displays indicator when 3 users are replying", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
const sam = { id: 1, username: "sam" }; await joinChannel("/chat-reply/1", {
const mark = { id: 2, username: "mark" }; id: 1,
const joffrey = { id: 3, username: "joffrey" }; avatar_template: "/images/avatar.png",
this.set("presenceChannel.users", [sam, mark, joffrey]); username: "sam",
});
await joinChannel("/chat-reply/1", {
id: 2,
avatar_template: "/images/avatar.png",
username: "mark",
});
await joinChannel("/chat-reply/1", {
id: 3,
avatar_template: "/images/avatar.png",
username: "joffrey",
});
assert.strictEqual( assert.strictEqual(
query(".chat-replying-indicator__text").innerText, query(".chat-replying-indicator__text").innerText,
`${sam.username}, ${mark.username} and ${joffrey.username} are typing` `sam, mark and joffrey are typing`
); );
}); });
test("displays indicator when more than 3 users are replying", async function (assert) { test("displays indicator when more than 3 users are replying", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
const sam = { id: 1, username: "sam" }; await joinChannel("/chat-reply/1", {
const mark = { id: 2, username: "mark" }; id: 1,
const joffrey = { id: 3, username: "joffrey" }; avatar_template: "/images/avatar.png",
const taylor = { id: 4, username: "taylor" }; username: "sam",
this.set("presenceChannel.users", [sam, mark, joffrey, taylor]); });
await joinChannel("/chat-reply/1", {
id: 2,
avatar_template: "/images/avatar.png",
username: "mark",
});
await joinChannel("/chat-reply/1", {
id: 3,
avatar_template: "/images/avatar.png",
username: "joffrey",
});
await joinChannel("/chat-reply/1", {
id: 4,
avatar_template: "/images/avatar.png",
username: "taylor",
});
assert.strictEqual( assert.strictEqual(
query(".chat-replying-indicator__text").innerText, query(".chat-replying-indicator__text").innerText,
`${sam.username}, ${mark.username} and 2 others are typing` `sam, mark and 2 others are typing`
); );
}); });
test("filters current user from list of repliers", async function (assert) { test("filters current user from list of repliers", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
const sam = { id: 1, username: "sam" }; await joinChannel("/chat-reply/1", {
this.set("presenceChannel.users", [sam, this.currentUser]); id: 1,
avatar_template: "/images/avatar.png",
username: "sam",
});
await joinChannel("/chat-reply/1", this.currentUser);
assert.strictEqual( assert.strictEqual(
query(".chat-replying-indicator__text").innerText, query(".chat-replying-indicator__text").innerText,
`${sam.username} is typing` `sam is typing`
); );
}); });
test("resets presence when channel is draft", async function (assert) { test("resets presence when channel is draft", async function (assert) {
this.set("chatChannel", fabricators.chatChannel()); this.channel = fabricators.chatChannel();
this.set(
"presenceChannel",
MockPresenceChannel.create({
name: `/chat-reply/${this.chatChannel.id}`,
subscribed: true,
})
);
await render( await render(hbs`<ChatReplyingIndicator @channel={{this.channel}} />`);
hbs`<ChatReplyingIndicator @presenceChannel={{this.presenceChannel}} @chatChannel={{this.chatChannel}} />`
);
assert.true(this.presenceChannel.subscribed); assert.dom(".chat-replying-indicator.is-subscribed").exists();
this.set("chatChannel", fabricators.chatChannel({ isDraft: true })); this.channel.isDraft = true;
assert.false(this.presenceChannel.subscribed);
await settled();
assert.dom(".chat-replying-indicator.is-subscribed").doesNotExist();
}); });
} }
); );

View File

@ -1,9 +1,9 @@
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import I18n from "I18n"; import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import fabricators from "../helpers/fabricators";
module( module(
"Discourse Chat | Component | chat-retention-reminder-text", "Discourse Chat | Component | chat-retention-reminder-text",
@ -11,7 +11,7 @@ module(
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("when setting is set on 0", async function (assert) { test("when setting is set on 0", async function (assert) {
this.channel = ChatChannel.create({ chatable_type: "Category" }); this.channel = fabricators.chatChannel();
this.siteSettings.chat_channel_retention_days = 0; this.siteSettings.chat_channel_retention_days = 0;
await render( await render(
@ -25,7 +25,7 @@ module(
test("when channel is a public channel", async function (assert) { test("when channel is a public channel", async function (assert) {
const count = 10; const count = 10;
this.channel = ChatChannel.create({ chatable_type: "Category" }); this.channel = fabricators.chatChannel();
this.siteSettings.chat_channel_retention_days = count; this.siteSettings.chat_channel_retention_days = count;
await render( await render(
@ -39,9 +39,7 @@ module(
test("when channel is a DM channel", async function (assert) { test("when channel is a DM channel", async function (assert) {
const count = 10; const count = 10;
this.channel = ChatChannel.create({ this.channel = fabricators.directMessageChatChannel();
chatable_type: "DirectMessage",
});
this.siteSettings.chat_dm_retention_days = count; this.siteSettings.chat_dm_retention_days = count;
await render( await render(

View File

@ -2,7 +2,7 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, fillIn, render } from "@ember/test-helpers"; import { click, fillIn, render } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { createDirectMessageChannelDraft } from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import fabricators from "../helpers/fabricators"; import fabricators from "../helpers/fabricators";
import { module, test } from "qunit"; import { module, test } from "qunit";
@ -25,7 +25,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("search", async function (assert) { test("search", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -37,7 +37,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("select/deselect", async function (assert) { test("select/deselect", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -54,7 +54,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("no search results", async function (assert) { test("no search results", async function (assert) {
this.set("chat", mockChat(this, { users: [] })); this.set("chat", mockChat(this, { users: [] }));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -66,7 +66,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("loads user on first load", async function (assert) { test("loads user on first load", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -78,7 +78,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("do not load more users after selection", async function (assert) { test("do not load more users after selection", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -90,7 +90,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("apply is-focused to filter-area on focus input", async function (assert) { test("apply is-focused to filter-area on focus input", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} /><button class="test-blur">blur</button>` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} /><button class="test-blur">blur</button>`
@ -105,7 +105,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
test("state is reset on channel change", async function (assert) { test("state is reset on channel change", async function (assert) {
this.set("chat", mockChat(this)); this.set("chat", mockChat(this));
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
@ -115,7 +115,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
assert.strictEqual(query(".filter-usernames").value, "hawk"); assert.strictEqual(query(".filter-usernames").value, "hawk");
this.set("channel", fabricators.chatChannel()); this.set("channel", fabricators.chatChannel());
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
assert.strictEqual(query(".filter-usernames").value, ""); assert.strictEqual(query(".filter-usernames").value, "");
assert.true(exists(".filter-area.is-focused")); assert.true(exists(".filter-area.is-focused"));
@ -129,7 +129,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
}; };
const chat = mockChat(this, { users: [userWithStatus] }); const chat = mockChat(this, { users: [userWithStatus] });
this.set("chat", chat); this.set("chat", chat);
this.set("channel", createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
await render( await render(
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />` hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`

View File

@ -138,7 +138,7 @@ export function directMessageChannelPretender(
opts = { unread_count: 0, muted: false } opts = { unread_count: 0, muted: false }
) { ) {
let copy = cloneJSON(directMessageChannels[0]); let copy = cloneJSON(directMessageChannels[0]);
copy.chat_channel.currentUserMembership.unread_count = opts.unread_count; copy.chat_channel.currentUserMembership.unreadCount = opts.unread_count;
copy.chat_channel.currentUserMembership.muted = opts.muted; copy.chat_channel.currentUserMembership.muted = opts.muted;
server.get("/chat/chat_channels/75.json", () => helper.response(copy)); server.get("/chat/chat_channels/75.json", () => helper.response(copy));
} }
@ -150,13 +150,13 @@ export function chatChannelPretender(server, helper, changes = []) {
let found; let found;
found = copy.public_channels.find((c) => c.id === change.id); found = copy.public_channels.find((c) => c.id === change.id);
if (found) { if (found) {
found.currentUserMembership.unread_count = change.unread_count; found.currentUserMembership.unreadCount = change.unread_count;
found.currentUserMembership.muted = change.muted; found.currentUserMembership.muted = change.muted;
} }
if (!found) { if (!found) {
found = copy.direct_message_channels.find((c) => c.id === change.id); found = copy.direct_message_channels.find((c) => c.id === change.id);
if (found) { if (found) {
found.currentUserMembership.unread_count = change.unread_count; found.currentUserMembership.unreadCount = change.unread_count;
found.currentUserMembership.muted = change.muted; found.currentUserMembership.muted = change.muted;
} }
} }

View File

@ -1,15 +0,0 @@
import EmberObject from "@ember/object";
export default class MockPresenceChannel extends EmberObject {
users = [];
name = null;
subscribed = false;
async unsubscribe() {
this.set("subscribed", false);
}
async subscribe() {
this.set("subscribed", true);
}
}

View File

@ -83,12 +83,12 @@ acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) {
set(this.currentUser, "admin", true); set(this.currentUser, "admin", true);
set(this.currentUser, "moderator", true); set(this.currentUser, "moderator", true);
channel.set("status", "read_only"); channel.status = "read_only";
assert.notOk(this.chatGuardian.canArchiveChannel(channel)); assert.notOk(this.chatGuardian.canArchiveChannel(channel));
channel.set("status", "open"); channel.status = "open";
channel.set("status", "archived"); channel.status = "archived";
assert.notOk(this.chatGuardian.canArchiveChannel(channel)); assert.notOk(this.chatGuardian.canArchiveChannel(channel));
channel.set("status", "open"); channel.status = "open";
}); });
}); });