FEATURE: channels can allow/disallow @all/@here mentions (#19317)

The settings tab of each category channel should now present the option to allow or disallow channel wide mentions: @here and @all.

When disallowed, using these mentions in the channel should have no effect.
This commit is contained in:
Joffrey JAFFEUX 2022-12-05 17:03:51 +01:00 committed by GitHub
parent 569299b7a9
commit 68c4f16a73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 417 additions and 126 deletions

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
CHAT_CHANNEL_EDITABLE_PARAMS = %i[name description] CHAT_CHANNEL_EDITABLE_PARAMS = %i[name description]
CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users] CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
class Chat::Api::ChatChannelsController < Chat::Api class Chat::Api::ChatChannelsController < Chat::Api
def index def index

View File

@ -121,24 +121,25 @@ end
# #
# Table name: chat_channels # Table name: chat_channels
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# chatable_id :integer not null # chatable_id :integer not null
# deleted_at :datetime # deleted_at :datetime
# deleted_by_id :integer # deleted_by_id :integer
# featured_in_category_id :integer # featured_in_category_id :integer
# delete_after_seconds :integer # delete_after_seconds :integer
# chatable_type :string not null # chatable_type :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# name :string # name :string
# description :text # description :text
# status :integer default("open"), not null # status :integer default("open"), not null
# user_count :integer default(0), not null # user_count :integer default(0), not null
# last_message_sent_at :datetime not null # last_message_sent_at :datetime not null
# auto_join_users :boolean default(FALSE), not null # auto_join_users :boolean default(FALSE), not null
# user_count_stale :boolean default(FALSE), not null # allow_channel_wide_mentions :boolean default(TRUE), not null
# slug :string # user_count_stale :boolean default(FALSE), not null
# type :string # slug :string
# type :string
# #
# Indexes # Indexes
# #

View File

@ -3,6 +3,7 @@
class ChatChannelSerializer < ApplicationSerializer class ChatChannelSerializer < ApplicationSerializer
attributes :id, attributes :id,
:auto_join_users, :auto_join_users,
:allow_channel_wide_mentions,
:chatable, :chatable,
:chatable_id, :chatable_id,
:chatable_type, :chatable_type,

View File

@ -0,0 +1,11 @@
<span
{{did-update this.activate @property}}
{{will-destroy this.teardown}}
class={{concat-class "chat-channel-settings-saved-indicator" (if this.isActive "is-active")}}
role="status"
>
{{#if this.isActive}}
{{d-icon "check"}}
<span>{{i18n "saved"}}</span>
{{/if}}
</span>

View File

@ -0,0 +1,28 @@
import discourseLater from "discourse-common/lib/later";
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { cancel } from "@ember/runloop";
const ACTIVE_DURATION = 2000;
export default class ChatChannelSettingsSavedIndicator extends Component {
@tracked isActive = false;
property = null;
@action
activate() {
cancel(this._deactivateHandler);
this.isActive = true;
this._deactivateHandler = discourseLater(() => {
this.isActive = false;
}, ACTIVE_DURATION);
}
@action
teardown() {
cancel(this._deactivateHandler);
}
}

View File

@ -2,58 +2,122 @@
<div class="chat-form__field"> <div class="chat-form__field">
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.mute"}}</span> <span>{{i18n "chat.settings.mute"}}</span>
<ChatChannelSettingsSavedIndicator
@property={{this.channel.current_user_membership.muted}}
/>
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox @content={{this.mutedOptions}} @value={{this.channel.current_user_membership.muted}} @valueProperty="value" @class="channel-settings-view__muted-selector" @onChange={{action (fn this.saveNotificationSettings "muted")}} /> <ComboBox
{{#if this.savedMuted}} @content={{this.mutedOptions}}
<span class="channel-settings-view__saved" role="status" aria-label={{i18n "chat.channel_settings.save_label.mute_channel"}}> @value={{this.channel.current_user_membership.muted}}
{{d-icon "check"}} {{i18n "saved"}} @valueProperty="value"
</span> @class="channel-settings-view__muted-selector"
{{/if}} @onChange={{action (fn this.saveNotificationSettings "muted")}}
/>
</div> </div>
</div> </div>
{{#unless this.channel.current_user_membership.muted}} {{#unless this.channel.current_user_membership.muted}}
<div class="chat-form__field"> <div class="chat-form__field">
<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
@property={{this.channel.current_user_membership.desktop_notification_level}}
/>
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox @content={{this.notificationLevels}} @value={{this.channel.current_user_membership.desktop_notification_level}} @valueProperty="value" @class="channel-settings-view__desktop-notification-level-selector" @onChange={{action (fn this.saveNotificationSettings "desktop_notification_level")}} /> <ComboBox
{{#if this.savedDesktopNotificationLevel}} @content={{this.notificationLevels}}
<span class="channel-settings-view__saved" role="status" aria-label={{i18n "chat.channel_settings.save_label.desktop_notification"}}> @value={{this.channel.current_user_membership.desktop_notification_level}}
{{d-icon "check"}} {{i18n "saved"}} @valueProperty="value"
</span> @class="channel-settings-view__desktop-notification-level-selector"
{{/if}} @onChange={{action
(fn this.saveNotificationSettings "desktop_notification_level")
}}
/>
</div> </div>
</div> </div>
<div class="chat-form__field"> <div class="chat-form__field">
<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
@property={{this.channel.current_user_membership.mobile_notification_level}}
/>
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox @content={{this.notificationLevels}} @value={{this.channel.current_user_membership.mobile_notification_level}} @valueProperty="value" @class="channel-settings-view__mobile-notification-level-selector" @onChange={{action (fn this.saveNotificationSettings "mobile_notification_level")}} /> <ComboBox
{{#if this.savedMobileNotificationLevel}} @content={{this.notificationLevels}}
<span class="channel-settings-view__saved" role="status" aria-label={{i18n "chat.channel_settings.save_label.mobile_notification"}}> @value={{this.channel.current_user_membership.mobile_notification_level}}
{{d-icon "check"}} {{i18n "saved"}} @valueProperty="value"
</span> @class="channel-settings-view__mobile-notification-level-selector"
{{/if}} @onChange={{action
(fn this.saveNotificationSettings "mobile_notification_level")
}}
/>
</div> </div>
</div> </div>
{{/unless}} {{/unless}}
<div class="chat-retention-info">{{d-icon "info-circle"}}{{i18n "chat.settings.retention_info" days=this.siteSettings.chat_channel_retention_days}}</div> <div class="chat-retention-info">
{{d-icon "info-circle"}}
{{i18n
"chat.settings.retention_info"
days=this.siteSettings.chat_channel_retention_days
}}
</div>
</div> </div>
{{#if (chat-guardian "can-edit-chat-channel")}} {{#if this.adminSectionAvailable}}
<h3 class="chat-form__section-admin-title">{{i18n "chat.settings.admin_title"}}</h3> <h3 class="chat-form__section-admin-title">
{{i18n "chat.settings.admin_title"}}
</h3>
{{#if this.autoJoinAvailable}} {{#if this.autoJoinAvailable}}
<div class="chat-form__section"> <div class="chat-form__section">
<div class="chat-form__field"> <div class="chat-form__field">
<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
@property={{this.channel.auto_join_users}}
/>
</label> </label>
<ComboBox @content={{this.autoAddUsersOptions}} @value={{this.channel.auto_join_users}} @valueProperty="value" @class="channel-settings-view__auto-join-selector" @onChange={{action (fn this.onToggleAutoJoinUsers this.channel.auto_join_users)}} /> <ComboBox
<div class="chat-form__description -autojoin">{{i18n "chat.settings.auto_join_users_info" category=this.channel.chatable.name}}</div> @content={{this.autoAddUsersOptions}}
@value={{this.channel.auto_join_users}}
@valueProperty="value"
@class="channel-settings-view__auto-join-selector"
@onChange={{action
(fn this.onToggleAutoJoinUsers this.channel.auto_join_users)
}}
/>
<p class="chat-form__description -autojoin">
{{i18n
"chat.settings.auto_join_users_info"
category=this.channel.chatable.name
}}
</p>
</div>
</div>
{{/if}}
{{#if this.togglingChannelWideMentionsAvailable}}
<div class="chat-form__section">
<div class="chat-form__field">
<label class="chat-form__label">
<span>{{i18n "chat.settings.channel_wide_mentions_label"}}</span>
<ChatChannelSettingsSavedIndicator
@property={{this.channel.allow_channel_wide_mentions}}
/>
</label>
<ComboBox
@content={{this.channelWideMentionsOptions}}
@value={{this.channel.allow_channel_wide_mentions}}
@valueProperty="value"
@class="channel-settings-view__channel-wide-mentions-selector"
@onChange={{this.onToggleChannelWideMentions}}
/>
<p class="chat-form__description -channel-wide-mentions">
{{i18n "chat.settings.channel_wide_mentions_description" channel=this.channel.title}}
</p>
</div> </div>
</div> </div>
{{/if}} {{/if}}
@ -64,22 +128,42 @@
{{#if (chat-guardian "can-edit-chat-channel")}} {{#if (chat-guardian "can-edit-chat-channel")}}
{{#if (chat-guardian "can-archive-channel" this.channel)}} {{#if (chat-guardian "can-archive-channel" this.channel)}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton @action={{action "onArchiveChannel"}} @label="chat.channel_settings.archive_channel" @class="archive-btn chat-form__btn btn-flat" @icon="archive" /> <DButton
@action={{action "onArchiveChannel"}}
@label="chat.channel_settings.archive_channel"
@class="archive-btn chat-form__btn btn-flat"
@icon="archive"
/>
</div> </div>
{{/if}} {{/if}}
{{#if this.channel.isClosed}} {{#if this.channel.isClosed}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton @action={{action "onToggleChannelState"}} @label="chat.channel_settings.open_channel" @class="open-btn chat-form__btn btn-flat" @icon="unlock" /> <DButton
@action={{action "onToggleChannelState"}}
@label="chat.channel_settings.open_channel"
@class="open-btn chat-form__btn btn-flat"
@icon="unlock"
/>
</div> </div>
{{else}} {{else}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton @action={{action "onToggleChannelState"}} @label="chat.channel_settings.close_channel" @class="close-btn chat-form__btn btn-flat" @icon="lock" /> <DButton
@action={{action "onToggleChannelState"}}
@label="chat.channel_settings.close_channel"
@class="close-btn chat-form__btn btn-flat"
@icon="lock"
/>
</div> </div>
{{/if}} {{/if}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton @action={{action "onDeleteChannel"}} @label="chat.channel_settings.delete_channel" @class="delete-btn chat-form__btn btn-flat" @icon="trash-alt" /> <DButton
@action={{action "onDeleteChannel"}}
@label="chat.channel_settings.delete_channel"
@class="delete-btn chat-form__btn btn-flat"
@icon="trash-alt"
/>
</div> </div>
{{/if}} {{/if}}
</div> </div>

View File

@ -4,8 +4,8 @@ import { inject as service } from "@ember/service";
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import I18n from "I18n"; import I18n from "I18n";
import { camelize } from "@ember/string"; import { Promise } from "rsvp";
import discourseLater from "discourse-common/lib/later"; import { reads } from "@ember/object/computed";
const NOTIFICATION_LEVELS = [ const NOTIFICATION_LEVELS = [
{ name: I18n.t("chat.notification_levels.never"), value: "never" }, { name: I18n.t("chat.notification_levels.never"), value: "never" },
@ -23,8 +23,17 @@ const AUTO_ADD_USERS_OPTIONS = [
{ name: I18n.t("no_value"), value: false }, { name: I18n.t("no_value"), value: false },
]; ];
const CHANNEL_WIDE_MENTIONS_OPTIONS = [
{ name: I18n.t("yes_value"), value: true },
{
name: I18n.t("no_value"),
value: false,
},
];
export default class ChatChannelSettingsView extends Component { export default class ChatChannelSettingsView extends Component {
@service chat; @service chat;
@service chatGuardian;
@service router; @service router;
@service dialog; @service dialog;
tagName = ""; tagName = "";
@ -33,57 +42,28 @@ export default class ChatChannelSettingsView extends Component {
notificationLevels = NOTIFICATION_LEVELS; notificationLevels = NOTIFICATION_LEVELS;
mutedOptions = MUTED_OPTIONS; mutedOptions = MUTED_OPTIONS;
autoAddUsersOptions = AUTO_ADD_USERS_OPTIONS; autoAddUsersOptions = AUTO_ADD_USERS_OPTIONS;
channelWideMentionsOptions = CHANNEL_WIDE_MENTIONS_OPTIONS;
isSavingNotificationSetting = false; isSavingNotificationSetting = false;
savedDesktopNotificationLevel = false; savedDesktopNotificationLevel = false;
savedMobileNotificationLevel = false; savedMobileNotificationLevel = false;
savedMuted = false; savedMuted = false;
_updateAutoJoinUsers(value) { @reads("channel.isCategoryChannel") togglingChannelWideMentionsAvailable;
return ChatApi.modifyChatChannel(this.channel.id, {
auto_join_users: value, @computed("channel.isCategoryChannel")
}) get autoJoinAvailable() {
.then((chatChannel) => { return (
this.channel.set("auto_join_users", chatChannel.auto_join_users); this.siteSettings.max_chat_auto_joined_users > 0 &&
}) this.channel.isCategoryChannel
.catch((event) => { );
if (event.jqXHR?.responseJSON?.errors) {
this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}
});
} }
@action @computed("autoJoinAvailable", "togglingChannelWideMentionsAvailable")
saveNotificationSettings(key, value) { get adminSectionAvailable() {
if (this.channel[key] === value) { return (
return; this.chatGuardian.canEditChatChannel &&
} (this.autoJoinAvailable || this.togglingChannelWideMentionsAvailable)
);
const camelizedKey = camelize(`saved_${key}`);
this.set(camelizedKey, false);
const settings = {};
settings[key] = value;
return ChatApi.updateChatChannelNotificationsSettings(
this.channel.id,
settings
)
.then((membership) => {
this.channel.current_user_membership.setProperties({
muted: membership.muted,
desktop_notification_level: membership.desktop_notification_level,
mobile_notification_level: membership.mobile_notification_level,
});
this.set(camelizedKey, true);
})
.finally(() => {
discourseLater(() => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.set(camelizedKey, false);
}, 2000);
});
} }
@computed( @computed(
@ -98,12 +78,24 @@ export default class ChatChannelSettingsView extends Component {
); );
} }
@computed("channel.isCategoryChannel") @action
get autoJoinAvailable() { saveNotificationSettings(key, value) {
return ( if (this.channel[key] === value) {
this.siteSettings.max_chat_auto_joined_users > 0 && return;
this.channel.isCategoryChannel }
);
const settings = {};
settings[key] = value;
return ChatApi.updateChatChannelNotificationsSettings(
this.channel.id,
settings
).then((membership) => {
this.channel.current_user_membership.setProperties({
muted: membership.muted,
desktop_notification_level: membership.desktop_notification_level,
mobile_notification_level: membership.mobile_notification_level,
});
});
} }
@action @action
@ -133,8 +125,17 @@ export default class ChatChannelSettingsView extends Component {
} }
} }
@action
onToggleChannelWideMentions() {
return this._updateChannelProperty(
this.channel,
"allow_channel_wide_mentions",
!this.channel.allow_channel_wide_mentions
);
}
onDisableAutoJoinUsers() { onDisableAutoJoinUsers() {
this._updateAutoJoinUsers(false); return this._updateChannelProperty(this.channel, "auto_join_users", false);
} }
onEnableAutoJoinUsers() { onEnableAutoJoinUsers() {
@ -142,7 +143,26 @@ export default class ChatChannelSettingsView extends Component {
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: () => this._updateAutoJoinUsers(true), didConfirm: () =>
this._updateChannelProperty(this.channel, "auto_join_users", true),
}); });
} }
_updateChannelProperty(channel, property, value) {
if (channel[property] === value) {
return Promise.resolve();
}
const payload = {};
payload[property] = value;
return ChatApi.modifyChatChannel(channel.id, payload)
.then((updatedChannel) => {
channel.set(property, updatedChannel[property]);
})
.catch((event) => {
if (event.jqXHR?.responseJSON?.errors) {
this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}
});
}
} }

View File

@ -130,15 +130,15 @@ export default {
api.addToHeaderIcons("header-chat-link"); api.addToHeaderIcons("header-chat-link");
api.decorateChatMessage(function (chatMessage) { api.decorateChatMessage(function (chatMessage, chatChannel) {
if (!this.currentUser) { if (!this.currentUser) {
return; return;
} }
const highlightable = [ const highlightable = [`@${this.currentUser.username}`];
`@${this.currentUser.username}`, if (chatChannel.allow_channel_wide_mentions) {
...MENTION_KEYWORDS.map((k) => `@${k}`), highlightable.push(...MENTION_KEYWORDS.map((k) => `@${k}`));
]; }
chatMessage.querySelectorAll(".mention").forEach((node) => { chatMessage.querySelectorAll(".mention").forEach((node) => {
const mention = node.textContent.trim(); const mention = node.textContent.trim();

View File

@ -32,20 +32,11 @@
color: var(--primary-medium); color: var(--primary-medium);
} }
// Settings view
.channel-settings-view__saved {
color: var(--success);
padding-left: 0.5rem;
.d-icon-check {
margin-right: 0.25rem;
}
}
.channel-settings-view__desktop-notification-level-selector, .channel-settings-view__desktop-notification-level-selector,
.channel-settings-view__mobile-notification-level-selector, .channel-settings-view__mobile-notification-level-selector,
.channel-settings-view__muted-selector, .channel-settings-view__muted-selector,
.channel-settings-view__auto-join-selector { .channel-settings-view__auto-join-selector,
.channel-settings-view__channel-wide-mentions-selector {
width: 220px; width: 220px;
} }

View File

@ -0,0 +1,9 @@
.chat-channel-settings-saved-indicator {
padding-left: 0.5rem;
color: var(--success);
font-weight: normal;
.d-icon-check {
margin-right: 0.25rem;
}
}

View File

@ -148,7 +148,8 @@
} }
.chat-form { .chat-form {
&__description.-autojoin { &__description.-autojoin,
&__description.-channel-wide-mentions {
max-width: 50%; max-width: 50%;
} }
} }

View File

@ -289,6 +289,8 @@ en:
always: "For all activity" always: "For all activity"
settings: settings:
channel_wide_mentions_label: "Allow @all and @here mentions"
channel_wide_mentions_description: "Allow users to notify all members of #%{channel} with @all or only those who are active in the moment with @here"
auto_join_users_label: "Automatically add users" auto_join_users_label: "Automatically add users"
auto_join_users_info: "Check hourly which users have been active in the last 3 months and, if they have access to the %{category} category, add them to this channel." auto_join_users_info: "Check hourly which users have been active in the last 3 months and, if they have access to the %{category} category, add them to this channel."
enable_auto_join_users: "Automatically add all recently active users" enable_auto_join_users: "Automatically add all recently active users"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAllowChannelWideMentionsToChatChannels < ActiveRecord::Migration[7.0]
def change
add_column :chat_channels, :allow_channel_wide_mentions, :boolean, null: false, default: true
end
end

View File

@ -164,7 +164,7 @@ class Chat::ChatNotifier
def expand_global_mention(to_notify, already_covered_ids) def expand_global_mention(to_notify, already_covered_ids)
typed_global_mention = direct_mentions_from_cooked.include?("@all") typed_global_mention = direct_mentions_from_cooked.include?("@all")
if typed_global_mention if typed_global_mention && @chat_channel.allow_channel_wide_mentions
to_notify[:global_mentions] = members_accepting_channel_wide_notifications to_notify[:global_mentions] = members_accepting_channel_wide_notifications
.where.not(username_lower: normalized_mentions(direct_mentions_from_cooked)) .where.not(username_lower: normalized_mentions(direct_mentions_from_cooked))
.where.not(id: already_covered_ids) .where.not(id: already_covered_ids)
@ -179,7 +179,7 @@ class Chat::ChatNotifier
def expand_here_mention(to_notify, already_covered_ids) def expand_here_mention(to_notify, already_covered_ids)
typed_here_mention = direct_mentions_from_cooked.include?("@here") typed_here_mention = direct_mentions_from_cooked.include?("@here")
if typed_here_mention if typed_here_mention && @chat_channel.allow_channel_wide_mentions
to_notify[:here_mentions] = members_accepting_channel_wide_notifications to_notify[:here_mentions] = members_accepting_channel_wide_notifications
.where("last_seen_at > ?", 5.minutes.ago) .where("last_seen_at > ?", 5.minutes.ago)
.where.not(username_lower: normalized_mentions(direct_mentions_from_cooked)) .where.not(username_lower: normalized_mentions(direct_mentions_from_cooked))

View File

@ -66,6 +66,7 @@ register_asset "stylesheets/common/chat-onebox.scss"
register_asset "stylesheets/common/chat-skeleton.scss" register_asset "stylesheets/common/chat-skeleton.scss"
register_asset "stylesheets/colors.scss", :color_definitions register_asset "stylesheets/colors.scss", :color_definitions
register_asset "stylesheets/common/reviewable-chat-message.scss" register_asset "stylesheets/common/reviewable-chat-message.scss"
register_asset "stylesheets/common/chat-channel-settings-saved-indicator.scss"
register_svg_icon "comments" register_svg_icon "comments"
register_svg_icon "comment-slash" register_svg_icon "comment-slash"

View File

@ -49,6 +49,15 @@ describe Chat::ChatNotifier do
expect(to_notify[list_key]).to be_empty expect(to_notify[list_key]).to be_empty
end end
it "will never mention when channel is not accepting channel wide mentions" do
channel.update!(allow_channel_wide_mentions: false)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "includes all members of a channel except the sender" do it "includes all members of a channel except the sender" do
msg = build_cooked_msg(mention, user_1) msg = build_cooked_msg(mention, user_1)

View File

@ -13,13 +13,25 @@ RSpec.describe ChatChannel do
end end
context "when the slug is not nil" do context "when the slug is not nil" do
before do before { category_channel.update!(slug: "some-cool-channel") }
category_channel.update!(slug: "some-cool-channel")
end
it "includes the slug for the channel" do it "includes the slug for the channel" do
expect(category_channel.relative_url).to eq("/chat/channel/#{category_channel.id}/some-cool-channel") expect(category_channel.relative_url).to eq(
"/chat/channel/#{category_channel.id}/some-cool-channel",
)
end end
end end
end end
describe "#allow_channel_wide_mentions" do
it "defaults to true" do
expect(category_channel.allow_channel_wide_mentions).to be(true)
end
it "cant be nullified" do
expect { category_channel.update!(allow_channel_wide_mentions: nil) }.to raise_error(
ActiveRecord::NotNullViolation,
)
end
end
end end

View File

@ -277,6 +277,17 @@ describe Chat::Api::ChatChannelsController do
expect(response.parsed_body).to match_response_schema("category_chat_channel") expect(response.parsed_body).to match_response_schema("category_chat_channel")
end end
describe "when updating allow_channel_wide_mentions" do
it "sets the new value" do
put "/chat/api/chat_channels/#{chat_channel.id}.json",
params: {
allow_channel_wide_mentions: false,
}
expect(response.parsed_body["allow_channel_wide_mentions"]).to eq(false)
end
end
describe "Updating a channel to add users automatically" do describe "Updating a channel to add users automatically" do
it "sets the channel to auto-update users automatically" do it "sets the channel to auto-update users automatically" do
put "/chat/api/chat_channels/#{chat_channel.id}.json", params: { auto_join_users: true } put "/chat/api/chat_channels/#{chat_channel.id}.json", params: { auto_join_users: true }

View File

@ -17,6 +17,10 @@ describe ChatChannelSerializer do
it "does not return any sort of archive status" do it "does not return any sort of archive status" do
expect(subject.as_json.key?(:archive_completed)).to eq(false) expect(subject.as_json.key?(:archive_completed)).to eq(false)
end end
it "includes allow_channel_wide_mentions" do
expect(subject.as_json.key?(:allow_channel_wide_mentions)).to eq(true)
end
end end
context "when user is staff" do context "when user is staff" do
@ -37,6 +41,10 @@ describe ChatChannelSerializer do
chat_channel.reload chat_channel.reload
expect(subject.as_json.key?(:archive_completed)).to eq(true) expect(subject.as_json.key?(:archive_completed)).to eq(true)
end end
it "includes allow_channel_wide_mentions" do
expect(subject.as_json.key?(:allow_channel_wide_mentions)).to eq(true)
end
end end
end end
end end

View File

@ -31,6 +31,7 @@ export const directMessageChannels = [
muted: false, muted: false,
following: true, following: true,
}, },
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-20T08:14:16.950Z", last_message_sent_at: "2021-07-20T08:14:16.950Z",
message_bus_last_ids: { message_bus_last_ids: {
new_mentions: 0, new_mentions: 0,
@ -66,6 +67,7 @@ export const directMessageChannels = [
muted: false, muted: false,
following: true, following: true,
}, },
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-05T12:04:00.850Z", last_message_sent_at: "2021-07-05T12:04:00.850Z",
message_bus_last_ids: { message_bus_last_ids: {
new_mentions: 0, new_mentions: 0,
@ -107,6 +109,7 @@ export const chatChannels = {
title: "Site", title: "Site",
status: "open", status: "open",
chatable: chatables[1], chatable: chatables[1],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-24T08:14:16.950Z", last_message_sent_at: "2021-07-24T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -126,6 +129,7 @@ export const chatChannels = {
title: "Bug", title: "Bug",
status: "open", status: "open",
chatable: chatables[1], chatable: chatables[1],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-15T08:14:16.950Z", last_message_sent_at: "2021-07-15T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -145,6 +149,7 @@ export const chatChannels = {
title: "Public category", title: "Public category",
status: "open", status: "open",
chatable: chatables[8], chatable: chatables[8],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-14T08:14:16.950Z", last_message_sent_at: "2021-07-14T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -164,6 +169,7 @@ export const chatChannels = {
title: "Public category (read-only)", title: "Public category (read-only)",
status: "read_only", status: "read_only",
chatable: chatables[8], chatable: chatables[8],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-10T08:14:16.950Z", last_message_sent_at: "2021-07-10T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -183,6 +189,7 @@ export const chatChannels = {
title: "Public category (closed)", title: "Public category (closed)",
status: "closed", status: "closed",
chatable: chatables[8], chatable: chatables[8],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-21T08:14:16.950Z", last_message_sent_at: "2021-07-21T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -202,6 +209,7 @@ export const chatChannels = {
title: "Public category (archived)", title: "Public category (archived)",
status: "archived", status: "archived",
chatable: chatables[8], chatable: chatables[8],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-25T08:14:16.950Z", last_message_sent_at: "2021-07-25T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,
@ -221,6 +229,7 @@ export const chatChannels = {
title: "Another Category", title: "Another Category",
status: "open", status: "open",
chatable: chatables[12], chatable: chatables[12],
allow_channel_wide_mentions: true,
last_message_sent_at: "2021-07-02T08:14:16.950Z", last_message_sent_at: "2021-07-02T08:14:16.950Z",
current_user_membership: { current_user_membership: {
unread_count: 0, unread_count: 0,

View File

@ -0,0 +1,31 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render, settled } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
module(
"Discourse Chat | Component | chat-channel-settings-saved-indicator",
function (hooks) {
setupRenderingTest(hooks);
test("when property changes", async function (assert) {
await render(
hbs`<ChatChannelSettingsSavedIndicator @property={{this.property}} />`
);
assert
.dom(".chat-channel-settings-saved-indicator.is-active")
.doesNotExist();
this.set("property", 1);
assert.dom(".chat-channel-settings-saved-indicator.is-active").exists();
await settled();
assert
.dom(".chat-channel-settings-saved-indicator.is-active")
.doesNotExist();
});
}
);

View File

@ -9,7 +9,14 @@ import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-cha
import { module } from "qunit"; import { module } from "qunit";
function membershipFixture(id, options = {}) { function membershipFixture(id, options = {}) {
options = Object.assign({}, options, { muted: false, following: true }); options = Object.assign(
{},
{
muted: false,
following: true,
},
options
);
return { return {
following: options.following, following: options.following,
@ -99,7 +106,7 @@ module(
return [ return [
200, 200,
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
membershipFixture(this.channel.id, { muted: true }), membershipFixture(this.channel.id, { muted: false }),
]; ];
} }
); );
@ -111,6 +118,34 @@ module(
assert.equal(sk.header().value(), "false"); assert.equal(sk.header().value(), "false");
}, },
}); });
componentTest("allow channel wide mentions", {
template: hbs`{{chat-channel-settings-view channel=channel}}`,
beforeEach() {
this.set("channel", fabricators.chatChannel());
},
async test(assert) {
pretender.put(`/chat/api/chat_channels/${this.channel.id}.json`, () => {
return [
200,
{ "Content-Type": "application/json" },
{
allow_channel_wide_mentions: false,
},
];
});
const sk = selectKit(
".channel-settings-view__channel-wide-mentions-selector"
);
await sk.expand();
await sk.selectRowByName("No");
assert.equal(sk.header().value(), "false");
},
});
} }
); );
@ -205,7 +240,7 @@ module(
return [ return [
200, 200,
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
membershipFixture(this.channel.id, { muted: true }), membershipFixture(this.channel.id, { muted: false }),
]; ];
} }
); );
@ -217,5 +252,24 @@ module(
assert.equal(sk.header().value(), "false"); assert.equal(sk.header().value(), "false");
}, },
}); });
componentTest("allow channel wide mentions", {
template: hbs`{{chat-channel-settings-view channel=channel}}`,
beforeEach() {
this.set(
"channel",
fabricators.chatChannel({
chatable_type: CHATABLE_TYPES.directMessageChannel,
})
);
},
async test(assert) {
assert
.dom(".channel-settings-view__channel-wide-mentions-selector")
.doesNotExist();
},
});
} }
); );

View File

@ -31,6 +31,7 @@ export default {
name: "My category name", name: "My category name",
chatable: categoryChatableFabricator(), chatable: categoryChatableFabricator(),
last_message_sent_at: "2021-11-08T21:26:05.710Z", last_message_sent_at: "2021-11-08T21:26:05.710Z",
allow_channel_wide_mentions: true,
message_bus_last_ids: { message_bus_last_ids: {
new_mentions: 0, new_mentions: 0,
new_messages: 0, new_messages: 0,