FEATURE: enable_public_channels site setting (#22565)

`SiteSetting.enable_public_channels` allows site admin to decide if public channels are available at all. There's no distinction between admins or not as we expect admins to create private category channels if they want to limit usage.
This commit is contained in:
Joffrey JAFFEUX
2023-07-13 10:00:25 +02:00
committed by GitHub
parent 9c9058d0c3
commit c996e5502f
23 changed files with 326 additions and 153 deletions

View File

@ -27,6 +27,7 @@ module Chat
# @option params_to_create [Boolean] threading_enabled
# @return [Service::Base::Context]
policy :public_channels_enabled
policy :can_create_channel
contract
model :category, :fetch_category
@ -57,6 +58,10 @@ module Chat
private
def public_channels_enabled
SiteSetting.enable_public_channels
end
def can_create_channel(guardian:, **)
guardian.can_create_chat_channel?
end

View File

@ -56,6 +56,7 @@ module Chat
def fetch_category_channels(guardian:, **)
return if context.mode == :user
return if !SiteSetting.enable_public_channels
context.category_channels =
::Chat::ChannelFetcher.secured_public_channel_search(

View File

@ -12,6 +12,7 @@ export default class ChannelsList extends Component {
@service chatStateManager;
@service chatChannelsManager;
@service site;
@service siteSettings;
@service session;
@service currentUser;
@service modal;
@ -71,6 +72,10 @@ export default class ChannelsList extends Component {
}
get displayPublicChannels() {
if (!this.siteSettings.enable_public_channels) {
return false;
}
if (this.publicMessageChannelsEmpty) {
return (
this.currentUser?.staff ||

View File

@ -113,6 +113,7 @@ export default class ChatMessageCreator extends Component {
@service site;
@service router;
@service currentUser;
@service siteSettings;
@tracked selection = new TrackedArray();
@tracked activeSelection = new TrackedArray();
@ -124,10 +125,25 @@ export default class ChatMessageCreator extends Component {
@tracked _activeResultIdentifier = null;
get placeholder() {
if (this.hasSelectedUsers) {
return I18n.t("chat.new_message_modal.user_search_placeholder");
} else {
return I18n.t("chat.new_message_modal.default_search_placeholder");
if (
this.siteSettings.enable_public_channels &&
this.chat.userCanDirectMessage
) {
if (this.hasSelectedUsers) {
return I18n.t("chat.new_message_modal.user_search_placeholder");
} else {
return I18n.t("chat.new_message_modal.default_search_placeholder");
}
} else if (this.siteSettings.enable_public_channels) {
return I18n.t(
"chat.new_message_modal.default_channel_search_placeholder"
);
} else if (this.chat.userCanDirectMessage) {
if (this.hasSelectedUsers) {
return I18n.t("chat.new_message_modal.user_search_placeholder");
} else {
return I18n.t("chat.new_message_modal.default_user_search_placeholder");
}
}
}

View File

@ -1,8 +1,10 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-new-message"
@title="chat.new_message_modal.title"
@inline={{@inline}}
>
<Chat::MessageCreator @onClose={{@closeModal}} />
</DModal>
{{#if this.shouldRender}}
<DModal
@closeModal={{@closeModal}}
class="chat-modal-new-message"
@title="chat.new_message_modal.title"
@inline={{@inline}}
>
<Chat::MessageCreator @onClose={{@closeModal}} />
</DModal>
{{/if}}

View File

@ -0,0 +1,14 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class ChatModalNewMessage extends Component {
@service chat;
@service siteSettings;
get shouldRender() {
return (
this.siteSettings.enable_public_channels || this.chat.userCanDirectMessage
);
}
}

View File

@ -21,155 +21,159 @@ export default {
return;
}
this.siteSettings = container.lookup("service:site-settings");
withPluginApi("1.3.0", (api) => {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
const SidebarChatChannelsSectionLink = class extends BaseCustomSidebarSectionLink {
constructor({ channel, chatService }) {
super(...arguments);
this.channel = channel;
this.chatService = chatService;
}
get name() {
return dasherize(this.channel.slugifiedTitle);
}
get classNames() {
const classes = [];
if (this.channel.currentUserMembership.muted) {
classes.push("sidebar-section-link--muted");
if (this.siteSettings.enable_public_channels) {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
const SidebarChatChannelsSectionLink = class extends BaseCustomSidebarSectionLink {
constructor({ channel, chatService }) {
super(...arguments);
this.channel = channel;
this.chatService = chatService;
}
if (this.channel.id === this.chatService.activeChannel?.id) {
classes.push("sidebar-section-link--active");
get name() {
return dasherize(this.channel.slugifiedTitle);
}
classes.push(`channel-${this.channel.id}`);
get classNames() {
const classes = [];
return classes.join(" ");
}
if (this.channel.currentUserMembership.muted) {
classes.push("sidebar-section-link--muted");
}
get route() {
return "chat.channel";
}
if (this.channel.id === this.chatService.activeChannel?.id) {
classes.push("sidebar-section-link--active");
}
get models() {
return this.channel.routeModels;
}
classes.push(`channel-${this.channel.id}`);
get text() {
return htmlSafe(emojiUnescape(this.channel.escapedTitle));
}
get prefixType() {
return "icon";
}
get prefixValue() {
return "d-chat";
}
get prefixColor() {
return this.channel.chatable.color;
}
get title() {
return this.channel.escapedDescription
? htmlSafe(this.channel.escapedDescription)
: `${this.channel.escapedTitle} ${I18n.t("chat.title")}`;
}
get prefixBadge() {
return this.channel.chatable.read_restricted ? "lock" : "";
}
get suffixType() {
return "icon";
}
get suffixValue() {
return this.channel.tracking.unreadCount > 0 ? "circle" : "";
}
get suffixCSSClass() {
return this.channel.tracking.mentionCount > 0
? "urgent"
: "unread";
}
};
const SidebarChatChannelsSection = class extends BaseCustomSidebarSection {
@tracked currentUserCanJoinPublicChannels =
this.sidebar.currentUser &&
(this.sidebar.currentUser.staff ||
this.sidebar.currentUser.has_joinable_public_channels);
constructor() {
super(...arguments);
if (container.isDestroyed) {
return;
return classes.join(" ");
}
this.chatService = container.lookup("service:chat");
this.chatChannelsManager = container.lookup(
"service:chat-channels-manager"
);
this.router = container.lookup("service:router");
}
get sectionLinks() {
return this.chatChannelsManager.publicMessageChannels.map(
(channel) =>
new SidebarChatChannelsSectionLink({
channel,
chatService: this.chatService,
})
);
}
get route() {
return "chat.channel";
}
get name() {
return "chat-channels";
}
get models() {
return this.channel.routeModels;
}
get title() {
return I18n.t("chat.chat_channels");
}
get text() {
return htmlSafe(emojiUnescape(this.channel.escapedTitle));
}
get text() {
return I18n.t("chat.chat_channels");
}
get prefixType() {
return "icon";
}
get actions() {
return [
{
id: "browseChannels",
title: I18n.t("chat.channels_list_popup.browse"),
action: () => this.router.transitionTo("chat.browse.open"),
},
];
}
get prefixValue() {
return "d-chat";
}
get actionsIcon() {
return "pencil-alt";
}
get prefixColor() {
return this.channel.chatable.color;
}
get links() {
return this.sectionLinks;
}
get title() {
return this.channel.escapedDescription
? htmlSafe(this.channel.escapedDescription)
: `${this.channel.escapedTitle} ${I18n.t("chat.title")}`;
}
get displaySection() {
return (
this.sectionLinks.length > 0 ||
this.currentUserCanJoinPublicChannels
);
}
};
get prefixBadge() {
return this.channel.chatable.read_restricted ? "lock" : "";
}
return SidebarChatChannelsSection;
}
);
get suffixType() {
return "icon";
}
get suffixValue() {
return this.channel.tracking.unreadCount > 0 ? "circle" : "";
}
get suffixCSSClass() {
return this.channel.tracking.mentionCount > 0
? "urgent"
: "unread";
}
};
const SidebarChatChannelsSection = class extends BaseCustomSidebarSection {
@tracked currentUserCanJoinPublicChannels =
this.sidebar.currentUser &&
(this.sidebar.currentUser.staff ||
this.sidebar.currentUser.has_joinable_public_channels);
constructor() {
super(...arguments);
if (container.isDestroyed) {
return;
}
this.chatService = container.lookup("service:chat");
this.chatChannelsManager = container.lookup(
"service:chat-channels-manager"
);
this.router = container.lookup("service:router");
}
get sectionLinks() {
return this.chatChannelsManager.publicMessageChannels.map(
(channel) =>
new SidebarChatChannelsSectionLink({
channel,
chatService: this.chatService,
})
);
}
get name() {
return "chat-channels";
}
get title() {
return I18n.t("chat.chat_channels");
}
get text() {
return I18n.t("chat.chat_channels");
}
get actions() {
return [
{
id: "browseChannels",
title: I18n.t("chat.channels_list_popup.browse"),
action: () => this.router.transitionTo("chat.browse.open"),
},
];
}
get actionsIcon() {
return "pencil-alt";
}
get links() {
return this.sectionLinks;
}
get displaySection() {
return (
this.sectionLinks.length > 0 ||
this.currentUserCanJoinPublicChannels
);
}
};
return SidebarChatChannelsSection;
}
);
}
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {

View File

@ -1,8 +1,16 @@
import DiscourseRoute from "discourse/routes/discourse";
import { inject as service } from "@ember/service";
import { defaultHomepage } from "discourse/lib/utilities";
export default class ChatBrowseIndexRoute extends DiscourseRoute {
@service chat;
@service siteSettings;
beforeModel() {
if (!this.siteSettings.enable_public_channels) {
return this.transitionTo(`discovery.${defaultHomepage()}`);
}
}
activate() {
this.chat.activeChannel = null;

View File

@ -330,6 +330,8 @@ en:
add_user_short: <span>Add user</span>
open_channel: <span>Open channel</span>
default_search_placeholder: "#a-channel, @somebody or anything"
default_channel_search_placeholder: "#a-channel"
default_user_search_placeholder: "@somebody"
user_search_placeholder: "...add more users"
disabled_user: "has disabled chat"
no_items: "No items"

View File

@ -1,6 +1,7 @@
en:
site_settings:
chat_enabled: "Enable the chat plugin."
enable_public_channels: "Enable public channels based on categories."
chat_allowed_groups: "Users in these groups can chat. Note that staff can always access chat."
chat_channel_retention_days: "Chat messages in regular channels will be retained for this many days. Set to '0' to retain messages forever."
chat_dm_retention_days: "Chat messages in personal chat channels will be retained for this many days. Set to '0' to retain messages forever."

View File

@ -2,6 +2,9 @@ chat:
chat_enabled:
default: true
client: true
enable_public_channels:
default: true
client: true
chat_allowed_groups:
client: true
type: group_list

View File

@ -86,6 +86,8 @@ module Chat
end
def self.secured_public_channel_search(guardian, options = {})
return ::Chat::Channel.none if !SiteSetting.enable_public_channels
allowed_channel_ids = generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true)
channels = Chat::Channel.includes(:last_message, chatable: [:topic_only_relative_url])

View File

@ -23,7 +23,7 @@ module Chat
end
def self.lookup(guardian, slugs)
if SiteSetting.enable_experimental_hashtag_autocomplete
if SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.enable_public_channels
return [] if !guardian.can_chat?
Chat::ChannelFetcher
.secured_public_channel_slug_lookup(guardian, slugs)
@ -39,7 +39,7 @@ module Chat
limit,
condition = HashtagAutocompleteService.search_conditions[:contains]
)
if SiteSetting.enable_experimental_hashtag_autocomplete
if SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.enable_public_channels
return [] if !guardian.can_chat?
Chat::ChannelFetcher
.secured_public_channel_search(
@ -61,7 +61,7 @@ module Chat
end
def self.search_without_term(guardian, limit)
if SiteSetting.enable_experimental_hashtag_autocomplete
if SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.enable_public_channels
return [] if !guardian.can_chat?
allowed_channel_ids_sql =
Chat::ChannelFetcher.generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true)

View File

@ -201,6 +201,12 @@ describe Chat::ChannelFetcher do
).to match_array([category_channel.id])
end
it "returns an empty array when public channels are disabled" do
SiteSetting.enable_public_channels = false
expect(described_class.secured_public_channels(guardian, following: nil)).to be_empty
end
it "can filter by channel name, or category name" do
expect(
described_class.secured_public_channels(

View File

@ -79,6 +79,11 @@ RSpec.describe Chat::ChannelHashtagDataSource do
Group.refresh_automatic_groups!
expect(described_class.lookup(Guardian.new(user), ["random"])).to be_empty
end
it "returns an empty array if public channels are disabled" do
SiteSetting.enable_public_channels = false
expect(described_class.lookup(guardian, ["random"])).to eq([])
end
end
describe "#search" do
@ -144,6 +149,11 @@ RSpec.describe Chat::ChannelHashtagDataSource do
Group.refresh_automatic_groups!
expect(described_class.search(Guardian.new(user), "rand", 10)).to be_empty
end
it "returns an empty array if public channels are disabled" do
SiteSetting.enable_public_channels = false
expect(described_class.search(guardian, "rand", 10)).to eq([])
end
end
describe "#search_without_term" do
@ -172,6 +182,11 @@ RSpec.describe Chat::ChannelHashtagDataSource do
)
end
it "returns an empty array if public channels are disabled" do
SiteSetting.enable_public_channels = false
expect(described_class.search_without_term(guardian, 5)).to eq([])
end
it "does not return channels the user does not have permission to view" do
expect(described_class.search_without_term(guardian, 5).map(&:slug)).not_to include("secret")
end

View File

@ -16,6 +16,14 @@ RSpec.describe Chat::CreateCategoryChannel do
let(:guardian) { Guardian.new(current_user) }
let(:params) { { guardian: guardian, category_id: category_id, name: "cool channel" } }
context "when public channels are disabled" do
fab!(:current_user) { Fabricate(:user) }
before { SiteSetting.enable_public_channels = false }
it { is_expected.to fail_a_policy(:public_channels_enabled) }
end
context "when the current user cannot make a channel" do
fab!(:current_user) { Fabricate(:user) }

View File

@ -52,6 +52,14 @@ RSpec.describe Chat::SearchChatable do
expect(result.category_channels).to_not include(private_channel_1)
end
end
context "when public channels are disabled" do
it "does not return category channels" do
SiteSetting.enable_public_channels = false
expect(described_class.call(params).category_channels).to be_blank
end
end
end
context "when term is prefixed with #" do

View File

@ -20,6 +20,15 @@ RSpec.describe "Browse page", type: :system do
end
end
context "when public channels are disabled" do
before { SiteSetting.enable_public_channels = false }
it "redirects to homepage" do
visit("/chat/browse") # no page object here as we actually don't load it
expect(page).to have_current_path("/latest")
end
end
context "when user has chat enabled" do
context "when visiting browse page" do
it "defaults to open filer" do

View File

@ -20,6 +20,47 @@ RSpec.describe "New message", type: :system do
expect(chat_page.message_creator).to be_opened
end
context "when public channels are disabled" do
fab!(:channel_1) { Fabricate(:chat_channel) }
before do
SiteSetting.enable_public_channels = false
channel_1.add(current_user)
end
it "doesn’t list public channels" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to be_not_listing(channel_1)
end
it "has a correct placeholder" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator.input["placeholder"]).to eq(
I18n.t("js.chat.new_message_modal.default_user_search_placeholder"),
)
end
end
context "when public channels are disabled and user can't create direct message" do
fab!(:current_user) { Fabricate(:user) }
before do
SiteSetting.enable_public_channels = false
SiteSetting.direct_message_enabled_groups = Group::AUTO_GROUPS[:staff]
end
it "doesn’t list public channels" do
visit("/")
chat_page.open_new_message(ensure_open: false)
expect(chat_page.message_creator).to be_closed
end
end
context "when the the content is not filtered" do
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:channel_2) { Fabricate(:chat_channel) }

View File

@ -29,9 +29,9 @@ module PageObjects
visit("/chat")
end
def open_new_message
def open_new_message(ensure_open: true)
send_keys([PLATFORM_KEY_MODIFIER, "k"])
find(".chat-modal-new-message")
find(".chat-modal-new-message") if ensure_open
end
def has_drawer?(channel_id: nil, expanded: true)

View File

@ -24,6 +24,10 @@ module PageObjects
page.has_css?(SELECTOR)
end
def closed?
page.has_no_css?(SELECTOR)
end
def enter_shortcut
input.send_keys(:enter)
end

View File

@ -3,19 +3,27 @@
module PageObjects
module Pages
class Sidebar < PageObjects::Pages::Base
PUBLIC_CHANNELS_SECTION_SELECTOR = ".sidebar-section[data-section-name='chat-channels']"
DM_CHANNELS_SECTION_SELECTOR = ".sidebar-section[data-section-name='chat-dms']"
def has_no_public_channels_section?
has_no_css?(PUBLIC_CHANNELS_SECTION_SELECTOR)
end
def channels_section
find(".sidebar-section[data-section-name='chat-channels']")
find(PUBLIC_CHANNELS_SECTION_SELECTOR)
end
def channels_section
find(PUBLIC_CHANNELS_SECTION_SELECTOR)
end
def dms_section
find(".sidebar-section[data-section-name='chat-dms']")
find(DM_CHANNELS_SECTION_SELECTOR)
end
def open_browse
find(
".sidebar-section[data-section-name='chat-channels'] .sidebar-section-header-button",
visible: false,
).click
channels_section.find(".sidebar-section-header-button", visible: false).click
end
def open_channel(channel)

View File

@ -8,6 +8,7 @@ RSpec.describe "Navigation", type: :system do
fab!(:category_channel) { Fabricate(:category_channel) }
fab!(:category_channel_2) { Fabricate(:category_channel) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
before do
chat_system_bootstrap(user, [category_channel, category_channel_2])
@ -40,6 +41,16 @@ RSpec.describe "Navigation", type: :system do
expect(page).to have_no_css("#d-sidebar")
end
end
context "when public channels are disabled" do
before { SiteSetting.enable_public_channels = false }
it "has public channels section" do
visit("/")
expect(sidebar_page).to have_no_public_channels_section
end
end
end
context "when visiting on mobile" do