FEATURE: introduces group channels (#24288)

Group channels will allow users to create channels with a name and invite people. It's possible to add people even after creation of the channel. Removing users is not yet possible but will be added in the near future.

Technically a group channel is `direct_message_channel` with a group attribute set to true on its direct message (chatable). This model might evolve in the future but offers much flexibility for now without having to rely on a complex migration.

The commit essentially consists of:
- a migration to set existing direct message channels with more than 2 users to a group
- a new message creator which allows to search, add members, and create groups
- a new `AddUsersToChannel` service
- a modified `SearchChatable` service
This commit is contained in:
Joffrey JAFFEUX
2023-11-10 11:29:28 +01:00
committed by GitHub
parent a8d6dc4d3a
commit ab832cc865
121 changed files with 2969 additions and 2332 deletions

View File

@ -26,7 +26,7 @@ RSpec.describe "Channel - Info - Members page", type: :system do
it "redirects to settings page" do
chat_page.visit_channel_members(channel_1)
expect(page).to have_current_path("/chat/c/#{channel_1.slug}/#{channel_1.id}/info/settings")
expect(page).to have_current_path("/chat/c/#{channel_1.slug}/#{channel_1.id}/info/members")
end
end
@ -64,4 +64,42 @@ RSpec.describe "Channel - Info - Members page", type: :system do
end
end
end
context "when category channel" do
it "doesn’t allow to add members" do
chat_page.visit_channel_members(channel_1)
expect(chat_page).to have_no_css(".chat-channel-members__list-item.-add-member")
end
end
context "when category channel" do
fab!(:channel_1) do
Fabricate(
:direct_message_channel,
slug: "test-channel",
users: [current_user, Fabricate(:user), Fabricate(:user)],
group: true,
)
end
it "allows to add members" do
new_user = Fabricate(:user)
chat_page.visit_channel_members(channel_1)
chat_page.find(".chat-channel-members__list-item.-add-member").click
chat_page.find(".chat-message-creator__members-input").fill_in(with: new_user.username)
chat_page.find(".chat-message-creator__list-item").click
chat_page.find(".add-to-channel").click
expect(chat_page).to have_current_path("/chat/c/#{channel_1.slug}/#{channel_1.id}")
expect(chat_page).to have_content(
I18n.t(
"chat.channel.users_invited_to_channel",
invited_users: "@#{new_user.username}",
inviting_user: "@#{current_user.username}",
count: 1,
),
)
end
end
end

View File

@ -159,6 +159,8 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
context "as staff" do
fab!(:current_user) { Fabricate(:admin) }
before { channel_1.add(current_user) }
it "can edit name" do
chat_page.visit_channel_settings(channel_1)
@ -170,7 +172,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
edit_modal.fill_and_save_name(name)
expect(channel_settings_page).to have_name(name)
expect(page).to have_content(name)
end
it "can edit description" do
@ -191,6 +193,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
it "can edit slug" do
chat_page.visit_channel_settings(channel_1)
edit_modal = channel_settings_page.open_edit_modal
slug = "gonzo-slug"
@ -199,7 +202,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
edit_modal.fill_and_save_slug(slug)
expect(channel_settings_page).to have_slug(slug)
expect(page).to have_current_path("/chat/c/gonzo-slug/#{channel_1.id}")
end
it "can clear the slug to use the autogenerated version based on the name" do
@ -213,7 +216,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
edit_modal.wait_for_auto_generated_slug
edit_modal.save_changes
expect(channel_settings_page).to have_slug("test-channel")
expect(page).to have_current_path("/chat/c/test-channel/#{channel_1.id}")
end
it "shows settings page" do

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
RSpec.describe "Flag message", type: :system do
let(:chat_page) { PageObjects::Pages::Chat.new }
fab!(:current_user) { Fabricate(:user) }
before do
chat_system_bootstrap
sign_in(current_user)
end
it "lists preloaded channels by default" do
channel_1 = Fabricate(:chat_channel)
channel_1.add(current_user)
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to be_listing(channel_1)
end
it "can filter channels" do
channel_1 = Fabricate(:chat_channel)
channel_2 = Fabricate(:chat_channel)
channel_1.add(current_user)
channel_2.add(current_user)
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter(channel_2.title)
expect(chat_page.message_creator).to be_listing(channel_2)
expect(chat_page.message_creator).to be_not_listing(channel_1)
end
it "can filter users" do
user_1 = Fabricate(:user)
user_2 = Fabricate(:user)
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter(user_2.username)
expect(chat_page.message_creator).to be_listing(user_2)
expect(chat_page.message_creator).to be_not_listing(user_1)
end
it "can filter direct message channels" do
channel_1 = Fabricate(:direct_message_channel, users: [current_user])
channel_2 =
Fabricate(
:direct_message_channel,
users: [current_user, Fabricate(:user), Fabricate(:user, username: "user_1")],
)
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter("user_1")
expect(chat_page.message_creator).to be_listing(channel_2)
expect(chat_page.message_creator).to be_not_listing(channel_1)
end
it "can create a new group message" do
user_1 = Fabricate(:user)
user_2 = Fabricate(:user)
visit("/")
chat_page.prefers_full_page
chat_page.open_new_message
chat_page.find("#new-group-chat").click
chat_page.find(".chat-message-creator__new-group-header__input").fill_in(with: "cats")
chat_page.find(".chat-message-creator__members-input").fill_in(with: user_1.username)
chat_page.message_creator.click_row(user_1)
chat_page.find(".chat-message-creator__members-input").fill_in(with: user_2.username)
chat_page.message_creator.click_row(user_2)
page.find(".create-chat-group").click
expect(page).to have_current_path(%r{/chat/c/cats/\d+})
end
end

View File

@ -24,7 +24,7 @@ RSpec.describe "Drawer", type: :system do
drawer_page.open_channel(channel)
page.find(".chat-channel-title").click
expect(page).to have_current_path("/chat/c/#{channel.slug}/#{channel.id}/info/settings")
expect(page).to have_current_path("/chat/c/#{channel.slug}/#{channel.id}/info/members")
end
end
end

View File

@ -1,470 +0,0 @@
# frozen_string_literal: true
RSpec.describe "New message", type: :system do
fab!(:current_user) { Fabricate(:admin) }
let(:chat_page) { PageObjects::Pages::Chat.new }
before do
# simpler user search without having to worry about user search data
SiteSetting.enable_names = false
chat_system_bootstrap
sign_in(current_user)
end
it "cmd + k opens new message" do
visit("/")
chat_page.open_new_message
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 selecting more users than allowed" do
fab!(:current_user) { Fabricate(:trust_level_1) }
fab!(:user_1) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
before { SiteSetting.chat_max_direct_message_users = 1 }
it "shows an error" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter(user_1.username)
chat_page.message_creator.shift_click_row(user_1)
chat_page.message_creator.filter(user_2.username)
chat_page.message_creator.shift_click_row(user_2)
chat_page.message_creator.click_cta
expect(page).to have_content(
I18n.t(
"chat.errors.over_chat_max_direct_message_users",
count: SiteSetting.chat_max_direct_message_users,
),
)
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 content is not filtered" do
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:channel_2) { Fabricate(:chat_channel) }
fab!(:user_1) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
fab!(:direct_message_channel_1) do
Fabricate(:direct_message_channel, users: [current_user, user_1])
end
fab!(:direct_message_channel_2) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
before { channel_1.add(current_user) }
it "lists channels the user is following" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to be_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
expect(chat_page.message_creator).to be_not_listing(direct_message_channel_2)
expect(chat_page.message_creator).to be_listing(user_1)
expect(chat_page.message_creator).to be_not_listing(user_2)
end
end
context "with no selection" do
context "with unread state" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
before do
channel_1.add(user_1)
channel_1.add(current_user)
Fabricate(:chat_message, chat_channel: channel_1, user: user_1)
Fabricate(:chat_message, chat_channel: channel_2, user: user_1)
end
it "shows the correct state" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to have_unread_row(channel_1, urgent: false)
expect(chat_page.message_creator).to have_unread_row(user_1, urgent: true)
end
end
context "when clicking a row" do
context "when the row is a channel" do
fab!(:channel_1) { Fabricate(:chat_channel) }
before { channel_1.add(current_user) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.click_row(channel_1)
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
context "when the row is a user" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.click_row(user_1)
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
end
context "when shift clicking a row" do
context "when the row is a channel" do
fab!(:channel_1) { Fabricate(:chat_channel) }
before { channel_1.add(current_user) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.shift_click_row(channel_1)
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
context "when the row is a user" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
it "adds the user" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.shift_click_row(user_1)
expect(chat_page.message_creator).to be_selecting(user_1)
end
end
end
context "when pressing enter" do
context "when the row is a channel" do
fab!(:channel_1) { Fabricate(:chat_channel) }
before { channel_1.add(current_user) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.click_row(channel_1)
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
context "when the row is a user" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.click_row(user_1)
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
end
context "when pressing shift+enter" do
context "when the row is a channel" do
fab!(:channel_1) { Fabricate(:chat_channel) }
before { channel_1.add(current_user) }
it "opens the channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.shift_enter_shortcut
expect(chat_page).to have_drawer(channel_id: channel_1.id)
end
end
context "when the row is a user" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
it "adds the user" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.shift_enter_shortcut
expect(chat_page.message_creator).to be_selecting(user_1)
end
end
end
context "when navigating content with arrows" do
fab!(:channel_1) { Fabricate(:chat_channel, name: "channela") }
fab!(:channel_2) { Fabricate(:chat_channel, name: "channelb") }
before do
channel_1.add(current_user)
channel_2.add(current_user)
end
it "changes active content" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to be_listing(channel_1, active: true)
chat_page.message_creator.arrow_down_shortcut
expect(chat_page.message_creator).to be_listing(channel_2, active: true)
chat_page.message_creator.arrow_down_shortcut
expect(chat_page.message_creator).to be_listing(channel_1, active: true)
chat_page.message_creator.arrow_up_shortcut
expect(chat_page.message_creator).to be_listing(channel_2, active: true)
end
end
context "with disabled content" do
fab!(:user_1) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
before { user_1.user_option.update!(chat_enabled: false) }
it "doesn’t make the content active" do
visit("/")
chat_page.open_new_message
expect(chat_page.message_creator).to be_listing(user_1, inactive: true, disabled: true)
end
end
end
context "when filtering" do
fab!(:channel_1) { Fabricate(:chat_channel, name: "bob-channel") }
fab!(:user_1) { Fabricate(:user, username: "bob-user") }
fab!(:user_2) { Fabricate(:user) }
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
fab!(:channel_3) { Fabricate(:direct_message_channel, users: [current_user, user_1, user_2]) }
before { channel_1.add(current_user) }
context "when query is the name of the category" do
fab!(:category) { Fabricate(:category, name: "dev") }
fab!(:channel_1) { Fabricate(:category_channel, chatable: category, name: "something dev") }
fab!(:channel_2) { Fabricate(:category_channel, chatable: category, name: "something else") }
it "favors the channel name" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter("dev")
expect(chat_page.message_creator).to be_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
end
end
context "with no prefix" do
it "lists all matching content" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter("bob")
expect(chat_page.message_creator).to be_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
expect(chat_page.message_creator).to be_listing(channel_3)
expect(chat_page.message_creator).to be_listing(user_1)
expect(chat_page.message_creator).to be_not_listing(user_2)
end
end
context "with channel prefix" do
it "lists matching channel" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter("#bob")
expect(chat_page.message_creator).to be_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
expect(chat_page.message_creator).to be_listing(channel_3)
expect(chat_page.message_creator).to be_not_listing(user_1)
expect(chat_page.message_creator).to be_not_listing(user_2)
end
end
context "with user prefix" do
it "lists matching users" do
visit("/")
chat_page.open_new_message
chat_page.message_creator.filter("@bob")
expect(chat_page.message_creator).to be_not_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
expect(chat_page.message_creator).to be_not_listing(channel_3)
expect(chat_page.message_creator).to be_listing(user_1)
expect(chat_page.message_creator).to be_not_listing(user_2)
end
end
end
context "with selection" do
fab!(:channel_1) { Fabricate(:chat_channel, name: "bob-channel") }
fab!(:user_1) { Fabricate(:user, username: "bob-user") }
fab!(:user_2) { Fabricate(:user, username: "bobby-user") }
fab!(:user_3) { Fabricate(:user, username: "sam-user") }
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
fab!(:channel_3) { Fabricate(:direct_message_channel, users: [current_user, user_2]) }
before do
channel_1.add(current_user)
visit("/")
chat_page.open_new_message
chat_page.message_creator.shift_click_row(user_1)
end
context "when pressing enter" do
it "opens the channel" do
chat_page.message_creator.enter_shortcut
expect(chat_page).to have_drawer(channel_id: channel_2.id)
end
end
context "when clicking cta" do
it "opens the channel" do
chat_page.message_creator.click_cta
expect(chat_page).to have_drawer(channel_id: channel_2.id)
end
end
context "when filtering" do
it "shows only matching users regardless of prefix" do
chat_page.message_creator.filter("#bob")
expect(chat_page.message_creator).to be_listing(user_1)
expect(chat_page.message_creator).to be_listing(user_2)
expect(chat_page.message_creator).to be_not_listing(user_3)
expect(chat_page.message_creator).to be_not_listing(channel_1)
expect(chat_page.message_creator).to be_not_listing(channel_2)
expect(chat_page.message_creator).to be_not_listing(channel_3)
end
it "shows selected user as selected in content" do
chat_page.message_creator.filter("@bob")
expect(chat_page.message_creator).to be_listing(user_1, selected: true)
expect(chat_page.message_creator).to be_listing(user_2, selected: false)
end
end
context "when clicking another user" do
it "adds it to the selection" do
chat_page.message_creator.filter("@bob")
chat_page.message_creator.click_row(user_2)
expect(chat_page.message_creator).to be_selecting(user_1)
expect(chat_page.message_creator).to be_selecting(user_2)
end
end
context "when pressing backspace" do
it "removes it" do
chat_page.message_creator.backspace_shortcut
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
chat_page.message_creator.backspace_shortcut
expect(chat_page.message_creator).to be_not_selecting(user_1)
end
end
context "when navigating selection with arrow left/right" do
it "changes active item" do
chat_page.message_creator.filter("@bob")
chat_page.message_creator.click_row(user_2)
chat_page.message_creator.arrow_left_shortcut
expect(chat_page.message_creator).to be_selecting(user_2, active: true)
chat_page.message_creator.arrow_left_shortcut
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
chat_page.message_creator.arrow_left_shortcut
expect(chat_page.message_creator).to be_selecting(user_2, active: true)
chat_page.message_creator.arrow_right_shortcut
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
end
end
context "when clicking selection" do
it "removes it" do
chat_page.message_creator.click_item(user_1)
expect(chat_page.message_creator).to be_not_selecting(user_1)
end
end
end
end

View File

@ -13,7 +13,7 @@ module PageObjects
end
def input
component.find(".chat-message-creator__input")
component.find(".chat-message-creator__search-input__input")
end
def filter(query = "")
@ -104,7 +104,7 @@ module PageObjects
end
def build_row_selector(chatable, **args)
selector = ".chat-message-creator__row"
selector = ".chat-message-creator__list-item"
selector += content_selector(**args)
selector += chatable_selector(chatable, **args)
selector
@ -116,9 +116,9 @@ module PageObjects
selector = ".-selected" if args[:selected]
selector = ":not(.-disabled)" if args[:enabled]
if args[:active]
selector += ".-active"
selector += ".-highlighted"
elsif args[:inactive]
selector += ":not(.-active)"
selector += ":not(.-highlighted)"
end
selector
end
@ -126,14 +126,11 @@ module PageObjects
def chatable_selector(chatable, **args)
selector = ""
if chatable.try(:category_channel?)
selector += ".-channel"
selector += "[data-id='c-#{chatable.id}']"
selector += "[data-identifier='c-#{chatable.id}']"
elsif chatable.try(:direct_message_channel?)
selector += ".-channel"
selector += "[data-id='c-#{chatable.id}']"
selector += "[data-identifier='c-#{chatable.id}']"
else
selector += ".-user"
selector += "[data-id='u-#{chatable.id}']"
selector += "[data-identifier='u-#{chatable.id}']"
end
selector
end

View File

@ -16,8 +16,6 @@ RSpec.describe "User status | sidebar", type: :system do
end
it "shows user status" do
Jobs.run_immediately!
visit("/")
expect(find(".user-status-message .emoji")["alt"]).to eq("heart")
@ -26,8 +24,6 @@ RSpec.describe "User status | sidebar", type: :system do
context "when changing status" do
it "updates status" do
Jobs.run_immediately!
visit("/")
current_user.set_status!("offline", "tooth")
@ -38,8 +34,6 @@ RSpec.describe "User status | sidebar", type: :system do
context "when removing status" do
it "removes status" do
Jobs.run_immediately!
visit("/")
current_user.clear_status!