FEATURE: allows to enable/disable threading in UI (#22307)

Enabling/Disabling threading has been possible through command line until now. This commit introduces two new UIs:

- When creating a channel, it will be available once the category has been selected
- On the settings page of a channel for admins
This commit is contained in:
Joffrey JAFFEUX 2023-06-29 07:19:12 +02:00 committed by GitHub
parent de2febcc0c
commit ea0b8ca38c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 383 additions and 156 deletions

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
CHANNEL_EDITABLE_PARAMS = %i[name description slug] CHANNEL_EDITABLE_PARAMS = %i[name description slug]
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions] CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions threading_enabled]
class Chat::Api::ChannelsController < Chat::ApiController class Chat::Api::ChannelsController < Chat::ApiController
def index def index
@ -36,7 +36,14 @@ class Chat::Api::ChannelsController < Chat::ApiController
def create def create
channel_params = channel_params =
params.require(:channel).permit(:chatable_id, :name, :slug, :description, :auto_join_users) params.require(:channel).permit(
:chatable_id,
:name,
:slug,
:description,
:auto_join_users,
:threading_enabled,
)
# NOTE: We don't allow creating channels for anything but category chatable types # NOTE: We don't allow creating channels for anything but category chatable types
# at the moment. This may change in future, at which point we will need to pass in # at the moment. This may change in future, at which point we will need to pass in

View File

@ -10,6 +10,7 @@ module Chat
# description: "This is the best channel", # description: "This is the best channel",
# slug: "super-channel", # slug: "super-channel",
# category_id: category.id, # category_id: category.id,
# threading_enabled: true,
# ) # )
# #
class CreateCategoryChannel class CreateCategoryChannel
@ -23,6 +24,7 @@ module Chat
# @option params_to_create [String] slug # @option params_to_create [String] slug
# @option params_to_create [Boolean] auto_join_users # @option params_to_create [Boolean] auto_join_users
# @option params_to_create [Integer] category_id # @option params_to_create [Integer] category_id
# @option params_to_create [Boolean] threading_enabled
# @return [Service::Base::Context] # @return [Service::Base::Context]
policy :can_create_channel policy :can_create_channel
@ -42,8 +44,12 @@ module Chat
attribute :slug, :string attribute :slug, :string
attribute :category_id, :integer attribute :category_id, :integer
attribute :auto_join_users, :boolean, default: false attribute :auto_join_users, :boolean, default: false
attribute :threading_enabled, :boolean, default: false
before_validation { self.auto_join_users = auto_join_users.presence || false } before_validation do
self.auto_join_users = auto_join_users.presence || false
self.threading_enabled = threading_enabled.presence || false
end
validates :category_id, presence: true validates :category_id, presence: true
validates :name, length: { maximum: SiteSetting.max_topic_title_length } validates :name, length: { maximum: SiteSetting.max_topic_title_length }
@ -70,6 +76,7 @@ module Chat
description: contract.description, description: contract.description,
user_count: 1, user_count: 1,
auto_join_users: contract.auto_join_users, auto_join_users: contract.auto_join_users,
threading_enabled: contract.threading_enabled,
) )
end end

View File

@ -3,8 +3,8 @@
module Chat module Chat
# Service responsible for updating a chat channel's name, slug, and description. # Service responsible for updating a chat channel's name, slug, and description.
# #
# For a CategoryChannel, the settings for auto_join_users and allow_channel_wide_mentions # For a CategoryChannel, the settings for auto_join_users, allow_channel_wide_mentions
# are also editable. # and threading_enabled are also editable.
# #
# @example # @example
# Service::Chat::UpdateChannel.call( # Service::Chat::UpdateChannel.call(
@ -13,6 +13,7 @@ module Chat
# name: "SuperChannel", # name: "SuperChannel",
# description: "This is the best channel", # description: "This is the best channel",
# slug: "super-channel", # slug: "super-channel",
# threading_enaled: true,
# ) # )
# #
class UpdateChannel class UpdateChannel
@ -43,6 +44,7 @@ module Chat
attribute :name, :string attribute :name, :string
attribute :description, :string attribute :description, :string
attribute :slug, :string attribute :slug, :string
attribute :threading_enabled, :boolean, default: false
attribute :auto_join_users, :boolean, default: false attribute :auto_join_users, :boolean, default: false
attribute :allow_channel_wide_mentions, :boolean, default: true attribute :allow_channel_wide_mentions, :boolean, default: true

View File

@ -1,36 +1,36 @@
<div class="chat-form__section"> <div class="chat-form__section">
<div class="chat-form__field"> <div class="chat-form__field -mute">
<label class="chat-form__label"> <label class="chat-form__label">
<span>{{i18n "chat.settings.mute"}}</span> <span>{{i18n "chat.settings.mute"}}</span>
<ChatChannelSettingsSavedIndicator <ChatChannelSettingsSavedIndicator
@property={{this.channel.currentUserMembership.muted}} @property={{@channel.currentUserMembership.muted}}
/> />
</label> </label>
<div class="chat-form__control"> <div class="chat-form__control">
<ComboBox <ComboBox
@content={{this.mutedOptions}} @content={{this.mutedOptions}}
@value={{this.channel.currentUserMembership.muted}} @value={{@channel.currentUserMembership.muted}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__muted-selector" @class="channel-settings-view__selector"
@onChange={{fn this.saveNotificationSettings "muted" "muted"}} @onChange={{fn this.saveNotificationSettings "muted" "muted"}}
/> />
</div> </div>
</div> </div>
{{#unless this.channel.currentUserMembership.muted}} {{#unless @channel.currentUserMembership.muted}}
<div class="chat-form__field"> <div class="chat-form__field -desktop-notification-level">
<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.desktopNotificationLevel}} @property={{@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.desktopNotificationLevel}} @value={{@channel.currentUserMembership.desktopNotificationLevel}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__desktop-notification-level-selector" @class="channel-settings-view__selector"
@onChange={{fn @onChange={{fn
this.saveNotificationSettings this.saveNotificationSettings
"desktopNotificationLevel" "desktopNotificationLevel"
@ -40,19 +40,19 @@
</div> </div>
</div> </div>
<div class="chat-form__field"> <div class="chat-form__field -mobile-notification-level">
<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.mobileNotificationLevel}} @property={{@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.mobileNotificationLevel}} @value={{@channel.currentUserMembership.mobileNotificationLevel}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__mobile-notification-level-selector" @class="channel-settings-view__selector"
@onChange={{fn @onChange={{fn
this.saveNotificationSettings this.saveNotificationSettings
"mobileNotificationLevel" "mobileNotificationLevel"
@ -64,7 +64,7 @@
{{/unless}} {{/unless}}
<div class="chat-retention-info"> <div class="chat-retention-info">
{{d-icon "info-circle"}} {{d-icon "info-circle"}}
<ChatRetentionReminderText @channel={{this.channel}} /> <ChatRetentionReminderText @channel={{@channel}} />
</div> </div>
</div> </div>
@ -72,28 +72,29 @@
<h3 class="chat-form__section-admin-title"> <h3 class="chat-form__section-admin-title">
{{i18n "chat.settings.admin_title"}} {{i18n "chat.settings.admin_title"}}
</h3> </h3>
{{#if this.autoJoinAvailable}} {{#if this.autoJoinAvailable}}
<div class="chat-form__section"> <div class="chat-form__section -autojoin">
<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 <ChatChannelSettingsSavedIndicator
@property={{this.channel.autoJoinUsers}} @property={{@channel.autoJoinUsers}}
/> />
</label> </label>
<ComboBox <ComboBox
@content={{this.autoAddUsersOptions}} @content={{this.autoAddUsersOptions}}
@value={{this.channel.autoJoinUsers}} @value={{@channel.autoJoinUsers}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__auto-join-selector" @class="channel-settings-view__selector"
@onChange={{action @onChange={{action
(fn this.onToggleAutoJoinUsers this.channel.autoJoinUsers) (fn this.onToggleAutoJoinUsers @channel.autoJoinUsers)
}} }}
/> />
<p class="chat-form__description -autojoin"> <p class="chat-form__description">
{{i18n {{i18n
"chat.settings.auto_join_users_info" "chat.settings.auto_join_users_info"
category=this.channel.chatable.name category=@channel.chatable.name
}} }}
</p> </p>
</div> </div>
@ -101,36 +102,62 @@
{{/if}} {{/if}}
{{#if this.togglingChannelWideMentionsAvailable}} {{#if this.togglingChannelWideMentionsAvailable}}
<div class="chat-form__section"> <div class="chat-form__section -channel-wide-mentions">
<div class="chat-form__field"> <div class="chat-form__field">
<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.allowChannelWideMentions}} @property={{@channel.allowChannelWideMentions}}
/> />
</label> </label>
<ComboBox <ComboBox
@content={{this.channelWideMentionsOptions}} @content={{this.channelWideMentionsOptions}}
@value={{this.channel.allowChannelWideMentions}} @value={{@channel.allowChannelWideMentions}}
@valueProperty="value" @valueProperty="value"
@class="channel-settings-view__channel-wide-mentions-selector" @class="channel-settings-view__selector"
@onChange={{this.onToggleChannelWideMentions}} @onChange={{this.onToggleChannelWideMentions}}
/> />
<p class="chat-form__description -channel-wide-mentions"> <p class="chat-form__description">
{{i18n {{i18n
"chat.settings.channel_wide_mentions_description" "chat.settings.channel_wide_mentions_description"
channel=this.channel.title channel=@channel.title
}} }}
</p> </p>
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if this.togglingThreadingAvailable}}
<div class="chat-form__section -threading">
<div class="chat-form__field">
<label class="chat-form__label">
<span>{{i18n "chat.settings.channel_threading_label"}}</span>
<span class="channel-settings-view__channel-threading-tooltip">
{{d-icon "info-circle"}}
<DTooltip>
{{i18n "chat.settings.channel_threading_description"}}
</DTooltip>
</span>
<ChatChannelSettingsSavedIndicator
@property={{@channel.threadingEnabled}}
/>
</label>
<ComboBox
@content={{this.threadingEnabledOptions}}
@value={{@channel.threadingEnabled}}
@valueProperty="value"
@class="channel-settings-view__selector"
@onChange={{this.onToggleThreadingEnabled}}
/>
</div>
</div>
{{/if}}
{{/if}} {{/if}}
{{#unless this.channel.isDirectMessageChannel}} {{#unless @channel.isDirectMessageChannel}}
<div class="chat-form__section"> <div class="chat-form__section">
{{#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" @channel)}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton <DButton
@action={{action "onArchiveChannel"}} @action={{action "onArchiveChannel"}}
@ -141,7 +168,7 @@
</div> </div>
{{/if}} {{/if}}
{{#if this.channel.isClosed}} {{#if @channel.isClosed}}
<div class="chat-form__field"> <div class="chat-form__field">
<DButton <DButton
@action={{action "onToggleChannelState"}} @action={{action "onToggleChannelState"}}

View File

@ -1,9 +1,8 @@
import Component from "@ember/component"; import Component from "@glimmer/component";
import { action, computed } from "@ember/object"; import { action } 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 { 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" },
@ -21,6 +20,11 @@ const AUTO_ADD_USERS_OPTIONS = [
{ name: I18n.t("no_value"), value: false }, { name: I18n.t("no_value"), value: false },
]; ];
const THREADING_ENABLED_OPTIONS = [
{ name: I18n.t("chat.settings.threading_enabled"), value: true },
{ name: I18n.t("chat.settings.threading_disabled"), value: false },
];
const CHANNEL_WIDE_MENTIONS_OPTIONS = [ const CHANNEL_WIDE_MENTIONS_OPTIONS = [
{ name: I18n.t("yes_value"), value: true }, { name: I18n.t("yes_value"), value: true },
{ {
@ -33,13 +37,14 @@ export default class ChatChannelSettingsView extends Component {
@service chat; @service chat;
@service chatApi; @service chatApi;
@service chatGuardian; @service chatGuardian;
@service currentUser;
@service siteSettings;
@service router; @service router;
@service dialog; @service dialog;
tagName = "";
channel = null;
notificationLevels = NOTIFICATION_LEVELS; notificationLevels = NOTIFICATION_LEVELS;
mutedOptions = MUTED_OPTIONS; mutedOptions = MUTED_OPTIONS;
threadingEnabledOptions = THREADING_ENABLED_OPTIONS;
autoAddUsersOptions = AUTO_ADD_USERS_OPTIONS; autoAddUsersOptions = AUTO_ADD_USERS_OPTIONS;
channelWideMentionsOptions = CHANNEL_WIDE_MENTIONS_OPTIONS; channelWideMentionsOptions = CHANNEL_WIDE_MENTIONS_OPTIONS;
isSavingNotificationSetting = false; isSavingNotificationSetting = false;
@ -47,17 +52,25 @@ export default class ChatChannelSettingsView extends Component {
savedMobileNotificationLevel = false; savedMobileNotificationLevel = false;
savedMuted = false; savedMuted = false;
@reads("channel.isCategoryChannel") togglingChannelWideMentionsAvailable; get togglingChannelWideMentionsAvailable() {
return this.args.channel.isCategoryChannel;
}
@computed("channel.isCategoryChannel") get togglingThreadingAvailable() {
get autoJoinAvailable() {
return ( return (
this.siteSettings.max_chat_auto_joined_users > 0 && this.siteSettings.enable_experimental_chat_threaded_discussions &&
this.channel.isCategoryChannel this.args.channel.isCategoryChannel &&
this.currentUser?.admin
);
}
get autoJoinAvailable() {
return (
this.siteSettings.max_chat_auto_joined_users > 0 &&
this.args.channel.isCategoryChannel
); );
} }
@computed("autoJoinAvailable", "togglingChannelWideMentionsAvailable")
get adminSectionAvailable() { get adminSectionAvailable() {
return ( return (
this.chatGuardian.canEditChatChannel() && this.chatGuardian.canEditChatChannel() &&
@ -65,30 +78,29 @@ export default class ChatChannelSettingsView extends Component {
); );
} }
@computed(
"siteSettings.chat_allow_archiving_channels",
"channel.{isArchived,isReadOnly}"
)
get canArchiveChannel() { get canArchiveChannel() {
return ( return (
this.siteSettings.chat_allow_archiving_channels && this.siteSettings.chat_allow_archiving_channels &&
!this.channel.isArchived && !this.args.channel.isArchived &&
!this.channel.isReadOnly !this.args.channel.isReadOnly
); );
} }
@action @action
saveNotificationSettings(frontendKey, backendKey, newValue) { saveNotificationSettings(frontendKey, backendKey, newValue) {
if (this.channel.currentUserMembership[frontendKey] === newValue) { if (this.args.channel.currentUserMembership[frontendKey] === newValue) {
return; return;
} }
const settings = {}; const settings = {};
settings[backendKey] = newValue; settings[backendKey] = newValue;
return this.chatApi return this.chatApi
.updateCurrentUserChannelNotificationsSettings(this.channel.id, settings) .updateCurrentUserChannelNotificationsSettings(
this.args.channel.id,
settings
)
.then((result) => { .then((result) => {
this.channel.currentUserMembership[frontendKey] = this.args.channel.currentUserMembership[frontendKey] =
result.membership[backendKey]; result.membership[backendKey];
}); });
} }
@ -96,76 +108,89 @@ export default class ChatChannelSettingsView extends Component {
@action @action
onArchiveChannel() { onArchiveChannel() {
const controller = showModal("chat-channel-archive-modal"); const controller = showModal("chat-channel-archive-modal");
controller.set("chatChannel", this.channel); controller.set("chatChannel", this.args.channel);
} }
@action @action
onDeleteChannel() { onDeleteChannel() {
const controller = showModal("chat-channel-delete-modal"); const controller = showModal("chat-channel-delete-modal");
controller.set("chatChannel", this.channel); controller.set("chatChannel", this.args.channel);
} }
@action @action
onToggleChannelState() { onToggleChannelState() {
const controller = showModal("chat-channel-toggle"); const controller = showModal("chat-channel-toggle");
controller.set("chatChannel", this.channel); controller.set("chatChannel", this.args.channel);
} }
@action @action
onToggleAutoJoinUsers() { onToggleAutoJoinUsers() {
if (!this.channel.autoJoinUsers) { if (!this.args.channel.autoJoinUsers) {
this.onEnableAutoJoinUsers(); this.onEnableAutoJoinUsers();
} else { } else {
this.onDisableAutoJoinUsers(); this.onDisableAutoJoinUsers();
} }
} }
@action
onToggleThreadingEnabled(value) {
return this._updateChannelProperty(
this.args.channel,
"threading_enabled",
value
).then((result) => {
this.args.channel.threadingEnabled = result.channel.threading_enabled;
});
}
@action @action
onToggleChannelWideMentions() { onToggleChannelWideMentions() {
const newValue = !this.channel.allowChannelWideMentions; const newValue = !this.args.channel.allowChannelWideMentions;
if (this.channel.allowChannelWideMentions === newValue) { if (this.args.channel.allowChannelWideMentions === newValue) {
return; return;
} }
return this._updateChannelProperty( return this._updateChannelProperty(
this.channel, this.args.channel,
"allow_channel_wide_mentions", "allow_channel_wide_mentions",
newValue newValue
).then((result) => { ).then((result) => {
this.channel.allowChannelWideMentions = this.args.channel.allowChannelWideMentions =
result.channel.allow_channel_wide_mentions; result.channel.allow_channel_wide_mentions;
}); });
} }
onDisableAutoJoinUsers() { onDisableAutoJoinUsers() {
if (this.channel.autoJoinUsers === false) { if (this.args.channel.autoJoinUsers === false) {
return; return;
} }
return this._updateChannelProperty( return this._updateChannelProperty(
this.channel, this.args.channel,
"auto_join_users", "auto_join_users",
false false
).then((result) => { ).then((result) => {
this.channel.autoJoinUsers = result.channel.auto_join_users; this.args.channel.autoJoinUsers = result.channel.auto_join_users;
}); });
} }
onEnableAutoJoinUsers() { onEnableAutoJoinUsers() {
if (this.channel.autoJoinUsers === true) { if (this.args.channel.autoJoinUsers === true) {
return; 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.args.channel.chatable.name,
}), }),
didConfirm: () => didConfirm: () =>
this._updateChannelProperty(this.channel, "auto_join_users", true).then( this._updateChannelProperty(
(result) => { this.args.channel,
this.channel.autoJoinUsers = result.channel.auto_join_users; "auto_join_users",
} true
), ).then((result) => {
this.args.channel.autoJoinUsers = result.channel.auto_join_users;
}),
}); });
} }

View File

@ -26,6 +26,8 @@ export default class CreateChannelController extends Controller.extend(
@service chatChannelsManager; @service chatChannelsManager;
@service chatApi; @service chatApi;
@service router; @service router;
@service currentUser;
@service siteSettings;
category = null; category = null;
categoryId = null; categoryId = null;
@ -37,10 +39,18 @@ export default class CreateChannelController extends Controller.extend(
autoJoinUsers = false; autoJoinUsers = false;
autoJoinWarning = ""; autoJoinWarning = "";
loadingPermissionHint = false; loadingPermissionHint = false;
threadingEnabled = false;
@notEmpty("category") categorySelected; @notEmpty("category") categorySelected;
@gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable; @gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable;
get threadingAvailable() {
return (
this.siteSettings.enable_experimental_chat_threaded_discussions &&
this.categorySelected
);
}
@computed("categorySelected", "name") @computed("categorySelected", "name")
get createDisabled() { get createDisabled() {
return !this.categorySelected || isBlank(this.name); return !this.categorySelected || isBlank(this.name);
@ -78,6 +88,7 @@ export default class CreateChannelController extends Controller.extend(
slug: this.slug || this.autoGeneratedSlug, slug: this.slug || this.autoGeneratedSlug,
description: this.description, description: this.description,
auto_join_users: this.autoJoinUsers, auto_join_users: this.autoJoinUsers,
threading_enabled: this.threadingEnabled,
}; };
return this.chatApi return this.chatApi

View File

@ -1,50 +1,50 @@
<DModalBody @title="chat.create_channel.title"> <DModalBody @title="chat.create_channel.title">
<div class="create-channel-control"> <div class="create-channel__control -name">
<label for="channel-name" class="create-channel-label"> <label for="channel-name" class="create-channel__label">
{{i18n "chat.create_channel.name"}} {{i18n "chat.create_channel.name"}}
</label> </label>
<Input <Input
name="channel-name" name="channel-name"
class="create-channel-name-input" class="create-channel__input"
@type="text" @type="text"
@value={{this.name}} @value={{this.name}}
{{on "input" (action "onNameChange" value="target.value")}} {{on "input" (action "onNameChange" value="target.value")}}
/> />
</div> </div>
<div class="create-channel-control"> <div class="create-channel__control -slug">
<label for="channel-slug" class="create-channel-label"> <label for="channel-slug" class="create-channel__label">
{{i18n "chat.create_channel.slug"}}&nbsp; {{i18n "chat.create_channel.slug"}}&nbsp;
<span> <span>
{{d-icon "info-circle"}} {{d-icon "info-circle"}}
<DTooltip>{{i18n <DTooltip>
"chat.channel_edit_name_slug_modal.slug_description" {{i18n "chat.channel_edit_name_slug_modal.slug_description"}}
}}</DTooltip> </DTooltip>
</span> </span>
</label> </label>
<Input <Input
name="channel-slug" name="channel-slug"
class="create-channel-slug-input" class="create-channel__input"
@type="text" @type="text"
@value={{this.slug}} @value={{this.slug}}
placeholder={{this.autoGeneratedSlug}} placeholder={{this.autoGeneratedSlug}}
/> />
</div> </div>
<div class="create-channel-control"> <div class="create-channel__control -description">
<label for="channel-description" class="create-channel-label"> <label for="channel-description" class="create-channel__label">
{{i18n "chat.create_channel.description"}} {{i18n "chat.create_channel.description"}}
</label> </label>
<Input <Input
name="channel-description" name="channel-description"
class="create-channel-description-input" class="create-channel__input"
@type="textarea" @type="textarea"
@value={{this.description}} @value={{this.description}}
/> />
</div> </div>
<div class="create-channel-control"> <div class="create-channel__control">
<label class="create-channel-label"> <label class="create-channel__label">
{{i18n "chat.create_channel.choose_category.label"}} {{i18n "chat.create_channel.choose_category.label"}}
</label> </label>
<CategoryChooser <CategoryChooser
@ -56,7 +56,7 @@
{{#if this.categoryPermissionsHint}} {{#if this.categoryPermissionsHint}}
<div <div
class={{concat-class class={{concat-class
"create-channel-hint" "create-channel__hint"
(if this.loadingPermissionHint "loading-permissions") (if this.loadingPermissionHint "loading-permissions")
}} }}
> >
@ -66,14 +66,14 @@
</div> </div>
{{#if this.autoJoinAvailable}} {{#if this.autoJoinAvailable}}
<div class="create-channel-control"> <div class="create-channel__control -auto-join">
<label class="create-channel-label"> <label class="create-channel__label">
<Input @type="checkbox" @checked={{this.autoJoinUsers}} /> <Input @type="checkbox" @checked={{this.autoJoinUsers}} />
<div class="auto-join-channel"> <div class="auto-join-channel">
<span class="auto-join-channel__label"> <span class="create-channel__label-title">
{{i18n "chat.settings.auto_join_users_label"}} {{i18n "chat.settings.auto_join_users_label"}}
</span> </span>
<p class="auto-join-channel__description"> <p class="create-channel__label-description">
{{#if this.categoryName}} {{#if this.categoryName}}
{{i18n {{i18n
"chat.settings.auto_join_users_info" "chat.settings.auto_join_users_info"
@ -87,6 +87,22 @@
</label> </label>
</div> </div>
{{/if}} {{/if}}
{{#if this.threadingAvailable}}
<div class="create-channel__control -threading-toggle">
<label class="create-channel__label">
<Input @type="checkbox" @checked={{this.threadingEnabled}} />
<div class="threading-channel">
<span class="create-channel__label-title">
{{i18n "chat.create_channel.threading.label"}}
</span>
<p class="create-channel__label-description">
{{i18n "chat.settings.channel_threading_description"}}
</p>
</div>
</label>
</div>
{{/if}}
</DModalBody> </DModalBody>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -37,14 +37,17 @@
font-size: var(--font-down-2); font-size: var(--font-down-2);
} }
.channel-settings-view__desktop-notification-level-selector, .channel-settings-view__selector {
.channel-settings-view__mobile-notification-level-selector,
.channel-settings-view__muted-selector,
.channel-settings-view__auto-join-selector,
.channel-settings-view__channel-wide-mentions-selector {
width: 220px; width: 220px;
} }
.channel-settings-view__channel-threading-tooltip {
padding-left: 0.25rem;
color: var(--tertiary);
cursor: pointer;
}
.channel-settings-view__muted-selector,
.chat-form__btn.delete-btn { .chat-form__btn.delete-btn {
.d-icon { .d-icon {
color: var(--danger); color: var(--danger);

View File

@ -9,9 +9,7 @@
} }
.select-kit.combo-box, .select-kit.combo-box,
.create-channel-name-input, .create-channel__input,
.create-channel-slug-input,
.create-channel-description-input,
#choose-topic-title { #choose-topic-title {
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
@ -24,23 +22,21 @@
} }
} }
.create-channel-hint { .create-channel__hint {
font-size: var(--font-down-1); font-size: var(--font-down-1);
padding-top: 0.25rem; padding-top: 0.25rem;
color: var(--secondary-low); color: var(--secondary-low);
} }
.create-channel-control, .create-channel__control,
.edit-channel-control { .edit-channel-control {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.auto-join-channel { .create-channel__label-description {
&__description { margin: 0;
margin: 0; padding-top: 0.25rem;
padding-top: 0.25rem; color: var(--secondary-low);
color: var(--secondary-low); font-size: var(--font-down-1) !important;
font-size: var(--font-down-1) !important;
}
} }
} }

View File

@ -354,6 +354,8 @@ en:
other: "%{count} members" other: "%{count} members"
create_channel: create_channel:
threading:
label: "Enable threading"
auto_join_users: auto_join_users:
public_category_warning: "%{category} is a public category. Automatically add all recently active users to this channel?" public_category_warning: "%{category} is a public category. Automatically add all recently active users to this channel?"
warning_1_group: warning_1_group:
@ -431,6 +433,8 @@ en:
settings: settings:
channel_wide_mentions_label: "Allow @all and @here mentions" 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" 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"
channel_threading_label: "Threading"
channel_threading_description: "When threading is enabled, replies to a chat message will create a separate conversation, which will exist alongside the main channel."
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. Add them to this channel if they have access to the %{category} category." auto_join_users_info: "Check hourly which users have been active in the last 3 months. Add them to this channel if they have access to the %{category} category."
auto_join_users_info_no_category: "Check hourly which users have been active in the last 3 months. Add them to this channel if they have access to the selected category." auto_join_users_info_no_category: "Check hourly which users have been active in the last 3 months. Add them to this channel if they have access to the selected category."
@ -440,6 +444,8 @@ en:
followed: "Joined" followed: "Joined"
mobile_notification_level: "Mobile push notifications" mobile_notification_level: "Mobile push notifications"
mute: "Mute channel" mute: "Mute channel"
threading_enabled: "Enabled"
threading_disabled: "Disabled"
muted_on: "On" muted_on: "On"
muted_off: "Off" muted_off: "Off"
notifications: "Notifications" notifications: "Notifications"

View File

@ -629,6 +629,7 @@ RSpec.describe Chat::Api::ChannelsController do
chatable_id: category.id, chatable_id: category.id,
name: "channel name", name: "channel name",
description: "My new channel", description: "My new channel",
threading_enabled: false,
}, },
} }
end end
@ -691,6 +692,25 @@ RSpec.describe Chat::Api::ChannelsController do
expect(new_channel.auto_join_users).to eq(true) expect(new_channel.auto_join_users).to eq(true)
end end
it "creates a channel sets threading_enabled to false by default" do
post "/chat/api/channels", params: params
expect(response.status).to eq(200)
new_channel = Chat::Channel.find(response.parsed_body.dig("channel", "id"))
expect(new_channel.threading_enabled).to eq(false)
end
it "creates a channel with threading_enabled set to true" do
params[:channel][:threading_enabled] = true
post "/chat/api/channels", params: params
expect(response.status).to eq(200)
new_channel = Chat::Channel.find(response.parsed_body.dig("channel", "id"))
expect(new_channel.threading_enabled).to eq(true)
end
describe "triggers the auto-join process" do describe "triggers the auto-join process" do
fab!(:chatters_group) { Fabricate(:group) } fab!(:chatters_group) { Fabricate(:group) }
fab!(:user) { Fabricate(:user, last_seen_at: 15.minute.ago) } fab!(:user) { Fabricate(:user, last_seen_at: 15.minute.ago) }
@ -884,6 +904,18 @@ RSpec.describe Chat::Api::ChannelsController do
expect(response.parsed_body["channel"]).to match_response_schema("category_chat_channel") expect(response.parsed_body["channel"]).to match_response_schema("category_chat_channel")
end end
describe "when updating threading_enabled" do
before { SiteSetting.enable_experimental_chat_threaded_discussions = true }
it "sets the new value" do
expect {
put "/chat/api/channels/#{channel.id}", params: { channel: { threading_enabled: true } }
}.to change { channel.reload.threading_enabled }.from(false).to(true)
expect(response.parsed_body["channel"]["threading_enabled"]).to eq(true)
end
end
describe "when updating allow_channel_wide_mentions" do describe "when updating allow_channel_wide_mentions" do
it "sets the new value" do it "sets the new value" do
put "/chat/api/channels/#{channel.id}", put "/chat/api/channels/#{channel.id}",

View File

@ -93,6 +93,29 @@ RSpec.describe Chat::CreateCategoryChannel do
result result
end end
end end
describe "threading_enabled" do
context "when true" do
it "sets threading_enabled to true" do
params[:threading_enabled] = true
expect(result.channel.threading_enabled).to eq(true)
end
end
context "when blank" do
it "sets threading_enabled to false" do
params[:threading_enabled] = nil
expect(result.channel.threading_enabled).to eq(false)
end
end
context "when false" do
it "sets threading_enabled to false" do
params[:threading_enabled] = false
expect(result.channel.threading_enabled).to eq(false)
end
end
end
end end
end end
end end

View File

@ -63,45 +63,73 @@ RSpec.describe Chat::UpdateChannel do
) )
end end
context "when the name is blank" do describe "name" do
before { params[:name] = "" } context "when blank" do
before { params[:name] = "" }
it "nils out the name" do it "nils out the name" do
result result
expect(channel.reload.name).to be_nil expect(channel.reload.name).to be_nil
end
end end
end end
context "when the description is blank" do describe "description" do
before do context "when blank" do
channel.update!(description: "something") before do
params[:description] = "" channel.update!(description: "something")
end params[:description] = ""
end
it "nils out the description" do it "nils out the description" do
result result
expect(channel.reload.description).to be_nil expect(channel.reload.description).to be_nil
end
end end
end end
context "when auto_join_users is set to 'true'" do describe "#auto_join_users" do
before do context "when set to 'true'" do
channel.update!(auto_join_users: false) before do
params[:auto_join_users] = true channel.update!(auto_join_users: false)
params[:auto_join_users] = true
end
it "updates the model accordingly" do
result
expect(channel.reload).to have_attributes(auto_join_users: true)
end
it "auto joins users" do
expect_enqueued_with(
job: Jobs::Chat::AutoJoinChannelMemberships,
args: {
chat_channel_id: channel.id,
},
) { result }
end
end
end
describe "threading_enabled" do
context "when true" do
it "changes the value to true" do
expect {
params[:threading_enabled] = true
result
}.to change { channel.reload.threading_enabled }.from(false).to(true)
end
end end
it "updates the model accordingly" do context "when false" do
result it "changes the value to true" do
expect(channel.reload).to have_attributes(auto_join_users: true) channel.update!(threading_enabled: true)
end
it "auto joins users" do expect {
expect_enqueued_with( params[:threading_enabled] = false
job: Jobs::Chat::AutoJoinChannelMemberships, result
args: { }.to change { channel.reload.threading_enabled }.from(true).to(false)
chat_channel_id: channel.id, end
},
) { result }
end end
end end
end end

View File

@ -56,7 +56,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
context "as a member" do context "as a member" do
before { channel_1.add(current_user) } before { channel_1.add(current_user) }
context "when visitng the settings of a recently joined channel" do context "when visiting the settings of a recently joined channel" do
fab!(:channel_2) { Fabricate(:category_channel) } fab!(:channel_2) { Fabricate(:category_channel) }
it "is correctly populated" do it "is correctly populated" do
@ -82,8 +82,11 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
membership = channel_1.membership_for(current_user) membership = channel_1.membership_for(current_user)
expect { expect {
find(".channel-settings-view__muted-selector").click select_kit =
find(".channel-settings-view__muted-selector [data-name='On']").click PageObjects::Components::SelectKit.new(".-mute .channel-settings-view__selector")
select_kit.expand
select_kit.select_row_by_name("On")
expect(page).to have_content(I18n.t("js.chat.settings.saved")) expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { membership.reload.muted }.from(false).to(true) }.to change { membership.reload.muted }.from(false).to(true)
end end
@ -93,10 +96,13 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
membership = channel_1.membership_for(current_user) membership = channel_1.membership_for(current_user)
expect { expect {
find(".channel-settings-view__desktop-notification-level-selector").click select_kit =
find( PageObjects::Components::SelectKit.new(
".channel-settings-view__desktop-notification-level-selector [data-name='Never']", ".-desktop-notification-level .channel-settings-view__selector",
).click )
select_kit.expand
select_kit.select_row_by_name("Never")
expect(page).to have_content(I18n.t("js.chat.settings.saved")) expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { membership.reload.desktop_notification_level }.from("mention").to("never") }.to change { membership.reload.desktop_notification_level }.from("mention").to("never")
end end
@ -106,10 +112,13 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
membership = channel_1.membership_for(current_user) membership = channel_1.membership_for(current_user)
expect { expect {
find(".channel-settings-view__mobile-notification-level-selector").click select_kit =
find( PageObjects::Components::SelectKit.new(
".channel-settings-view__mobile-notification-level-selector [data-name='Never']", ".-mobile-notification-level .channel-settings-view__selector",
).click )
select_kit.expand
select_kit.select_row_by_name("Never")
expect(page).to have_content(I18n.t("js.chat.settings.saved")) expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { membership.reload.mobile_notification_level }.from("mention").to("never") }.to change { membership.reload.mobile_notification_level }.from("mention").to("never")
end end
@ -133,9 +142,12 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
chat_page.visit_channel_settings(channel_1) chat_page.visit_channel_settings(channel_1)
expect { expect {
find(".channel-settings-view__auto-join-selector").click select_kit =
find(".channel-settings-view__auto-join-selector [data-name='Yes']").click PageObjects::Components::SelectKit.new(".-autojoin .channel-settings-view__selector")
select_kit.expand
select_kit.select_row_by_name("Yes")
find("#dialog-holder .btn-primary").click find("#dialog-holder .btn-primary").click
expect(page).to have_content(I18n.t("js.chat.settings.saved")) expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { channel_1.reload.auto_join_users }.from(false).to(true) }.to change { channel_1.reload.auto_join_users }.from(false).to(true)
end end
@ -144,8 +156,13 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
chat_page.visit_channel_settings(channel_1) chat_page.visit_channel_settings(channel_1)
expect { expect {
find(".channel-settings-view__channel-wide-mentions-selector").click select_kit =
find(".channel-settings-view__channel-wide-mentions-selector [data-name='No']").click PageObjects::Components::SelectKit.new(
".-channel-wide-mentions .channel-settings-view__selector",
)
select_kit.expand
select_kit.select_row_by_name("No")
expect(page).to have_content(I18n.t("js.chat.settings.saved")) expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { channel_1.reload.allow_channel_wide_mentions }.from(true).to(false) }.to change { channel_1.reload.allow_channel_wide_mentions }.from(true).to(false)
end end
@ -160,6 +177,19 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
}.to change { channel_1.reload.status }.from("open").to("closed") }.to change { channel_1.reload.status }.from("open").to("closed")
end end
it "can enable threading" do
SiteSetting.enable_experimental_chat_threaded_discussions = true
chat_page.visit_channel_settings(channel_1)
expect {
select_kit =
PageObjects::Components::SelectKit.new(".-threading .channel-settings-view__selector")
select_kit.expand
select_kit.select_row_by_name("Enabled")
expect(page).to have_content(I18n.t("js.chat.settings.saved"))
}.to change { channel_1.reload.threading_enabled }.from(false).to(true)
end
it "can delete channel" do it "can delete channel" do
chat_page.visit_channel_settings(channel_1) chat_page.visit_channel_settings(channel_1)

View File

@ -32,6 +32,16 @@ RSpec.describe "Create channel", type: :system do
expect(channel_modal).to have_create_hint(Group[:everyone].name) expect(channel_modal).to have_create_hint(Group[:everyone].name)
end end
it "shows threading toggle" do
SiteSetting.enable_experimental_chat_threaded_discussions = true
chat_page.visit_browse
chat_page.new_channel_button.click
channel_modal.select_category(category_1)
expect(channel_modal).to have_threading_toggle
end
it "does not override channel name if that was already specified" do it "does not override channel name if that was already specified" do
chat_page.visit_browse chat_page.visit_browse
chat_page.new_channel_button.click chat_page.new_channel_button.click
@ -138,7 +148,7 @@ RSpec.describe "Create channel", type: :system do
context "for a public category" do context "for a public category" do
before do before do
channel_modal.select_category(category_1) channel_modal.select_category(category_1)
find(".auto-join-channel__label").click find(".-auto-join .create-channel__label").click
channel_modal.click_primary_button channel_modal.click_primary_button
end end
@ -175,7 +185,7 @@ RSpec.describe "Create channel", type: :system do
before do before do
group_1.add(user_1) group_1.add(user_1)
channel_modal.select_category(private_category) channel_modal.select_category(private_category)
find(".auto-join-channel__label").click find(".-auto-join .create-channel__label").click
channel_modal.click_primary_button channel_modal.click_primary_button
end end

View File

@ -9,17 +9,21 @@ module PageObjects
end end
def create_channel_hint def create_channel_hint
find(".create-channel-hint") find(".create-channel__hint")
end end
def slug_input def slug_input
find(".create-channel-slug-input") find(".-slug .create-channel__input")
end end
def has_create_hint?(content) def has_create_hint?(content)
create_channel_hint.has_content?(content) create_channel_hint.has_content?(content)
end end
def has_threading_toggle?
has_selector?(".create-channel__control.-threading-toggle")
end
def fill_name(name) def fill_name(name)
fill_in("channel-name", with: name) fill_in("channel-name", with: name)
end end