UX: adds chat send shortcut user preference (#30473)

Users can now decide if they want to send a message on:
- <kbd>enter</kbd>
- <kbd>meta + enter</kbd>

If you choose <kbd>meta + enter</kbd>, <kbd>enter</kbd> will add a
linebreak.

<img width="192" alt="Screenshot 2025-01-21 at 12 57 48"
src="https://github.com/user-attachments/assets/abfd6f8b-83b3-4e6f-be67-8f63d536ca8a"
/>
This commit is contained in:
Joffrey JAFFEUX 2025-01-22 13:17:45 +01:00 committed by GitHub
parent a66e5ff728
commit 2cff8c82e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 195 additions and 10 deletions

View File

@ -611,6 +611,12 @@ textarea {
font-weight: normal;
}
}
.radio-group {
display: flex;
flex-direction: column;
gap: 0.5em;
}
}
// Special elements

View File

@ -296,6 +296,7 @@ end
# sidebar_show_count_of_new_items :boolean default(FALSE), not null
# watched_precedence_over_muted :boolean
# chat_separate_sidebar_mode :integer default(0), not null
# chat_send_shortcut :integer default(0), not null
# topics_unread_when_closed :boolean default(TRUE), not null
# show_thread_title_prompts :boolean default(TRUE), not null
# enable_smart_lists :boolean default(TRUE), not null

View File

@ -309,7 +309,6 @@ export default class ChatComposer extends Component {
if (
this.site.mobileView ||
event.altKey ||
event.metaKey ||
this.#isAutocompleteDisplayed()
) {
return;
@ -320,18 +319,22 @@ export default class ChatComposer extends Component {
}
if (event.key === "Enter") {
if (event.shiftKey) {
// Shift+Enter: insert newline
// if we are inside a code block just insert newline
const { pre } = this.composer.textarea.getSelected({ lineVal: true });
if (this.composer.textarea.isInside(pre, /(^|\n)```/g)) {
return;
}
// Ctrl+Enter, plain Enter: send
if (!event.ctrlKey) {
// if we are inside a code block just insert newline
const { pre } = this.composer.textarea.getSelected({ lineVal: true });
if (this.composer.textarea.isInside(pre, /(^|\n)```/g)) {
return;
}
const shortcutPreference =
this.currentUser.user_option.chat_send_shortcut;
const send =
(shortcutPreference === "enter" && !event.shiftKey) ||
event.ctrlKey ||
event.metaKey;
if (!send) {
// insert newline
return;
}
this.onSend();

View File

@ -4,6 +4,8 @@ import { service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse/lib/decorators";
import { isTesting } from "discourse/lib/environment";
import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
import { translateModKey } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import { CHAT_SOUNDS } from "discourse/plugins/chat/discourse/services/chat-audio-manager";
@ -16,6 +18,7 @@ const CHAT_ATTRS = [
"chat_email_frequency",
"chat_header_indicator_preference",
"chat_separate_sidebar_mode",
"chat_send_shortcut",
];
export const HEADER_INDICATOR_PREFERENCE_NEVER = "never";
@ -29,6 +32,21 @@ export default class PreferencesChatController extends Controller {
subpageTitle = i18n("chat.admin.title");
chatSendShortcutOptions = [
{
label: i18n("chat.send_shortcut.enter.label"),
value: "enter",
description: i18n("chat.send_shortcut.enter.description"),
},
{
label: i18n("chat.send_shortcut.meta_enter.label", {
meta_key: translateModKey(PLATFORM_KEY_MODIFIER),
}),
value: "meta_enter",
description: i18n("chat.send_shortcut.meta_enter.description"),
},
];
emailFrequencyOptions = [
{ name: i18n("chat.email_frequency.never"), value: "never" },
{ name: i18n("chat.email_frequency.when_away"), value: "when_away" },
@ -77,6 +95,10 @@ export default class PreferencesChatController extends Controller {
}
}
get chatSendShortcut() {
return this.model.get("user_option.chat_send_shortcut");
}
@discourseComputed
chatSounds() {
return Object.keys(CHAT_SOUNDS).map((value) => {

View File

@ -8,6 +8,7 @@ const CHAT_SOUND = "chat_sound";
const CHAT_EMAIL_FREQUENCY = "chat_email_frequency";
const CHAT_HEADER_INDICATOR_PREFERENCE = "chat_header_indicator_preference";
const CHAT_SEPARATE_SIDEBAR_MODE = "chat_separate_sidebar_mode";
const CHAT_SEND_SHORTCUT = "chat_send_shortcut";
export default {
name: "chat-user-options",
@ -24,6 +25,7 @@ export default {
api.addSaveableUserOptionField(CHAT_EMAIL_FREQUENCY);
api.addSaveableUserOptionField(CHAT_HEADER_INDICATOR_PREFERENCE);
api.addSaveableUserOptionField(CHAT_SEPARATE_SIDEBAR_MODE);
api.addSaveableUserOptionField(CHAT_SEND_SHORTCUT);
}
});
},

View File

@ -116,6 +116,40 @@
/>
</div>
<div
class="control-group chat-setting controls-dropdown"
data-setting-name="user_chat_send_shortcut"
>
<div class="radio-group">
{{#each this.chatSendShortcutOptions as |option|}}
<div class="radio-group-option">
<label class="controls">
<input
type="radio"
name="chat_send_shortcut"
id={{concat "chat_send_shortcut_" option.value}}
value={{option.value}}
checked={{eq
this.model.user_option.chat_send_shortcut
option.value
}}
{{on
"change"
(withEventValue
(fn (mut this.model.user_option.chat_send_shortcut))
)
}}
/>
{{option.label}}
</label>
<span class="control-instructions">
{{option.description}}
</span>
</div>
{{/each}}
</div>
</div>
<SaveControls
@id="user_chat_preference_save"
@model={{this.model}}

View File

@ -136,6 +136,13 @@ en:
never: "Never"
separate_sidebar_mode:
title: "Show separate sidebar modes for forum and chat"
send_shortcut:
enter:
label: "Send by Enter"
description: "New line by Shift + Enter"
meta_enter:
label: "Send by %{meta_key} + Enter"
description: "New line by Enter"
enable: "Enable chat"
flag: "Flag"
emoji: "Insert emoji"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddChatSendShortcutPreference < ActiveRecord::Migration[7.2]
def change
add_column :user_options, :chat_send_shortcut, :integer, default: 0, null: false
end
end

View File

@ -39,6 +39,10 @@ module Chat
@chat_separate_sidebar_mode ||= { default: 0, never: 1, always: 2, fullscreen: 3 }
end
def base.chat_send_shortcut
@chat_send_shortcut ||= { enter: 0, meta_enter: 1 }
end
# Avoid attempting to override when autoloading
if !base.method_defined?(:chat_separate_sidebar_mode_default?)
base.enum :chat_separate_sidebar_mode,
@ -46,6 +50,10 @@ module Chat
prefix: "chat_separate_sidebar_mode"
end
if !base.method_defined?(:chat_send_shortcut_default?)
base.enum :chat_send_shortcut, base.chat_send_shortcut, prefix: "chat_send_shortcut"
end
if !base.method_defined?(:show_thread_title_prompts?)
base.attribute :show_thread_title_prompts, :boolean, default: true
end

View File

@ -61,6 +61,7 @@ after_initialize do
UserUpdater::OPTION_ATTR.push(:chat_email_frequency)
UserUpdater::OPTION_ATTR.push(:chat_header_indicator_preference)
UserUpdater::OPTION_ATTR.push(:chat_separate_sidebar_mode)
UserUpdater::OPTION_ATTR.push(:chat_send_shortcut)
register_reviewable_type Chat::ReviewableMessage
@ -246,6 +247,10 @@ after_initialize do
object.chat_separate_sidebar_mode
end
add_to_serializer(:user_option, :chat_send_shortcut) { object.chat_send_shortcut }
add_to_serializer(:current_user_option, :chat_send_shortcut) { object.chat_send_shortcut }
on(:site_setting_changed) do |name, old_value, new_value|
user_option_field = Chat::RETENTION_SETTINGS_TO_USER_OPTION_FIELDS[name.to_sym]
begin

View File

@ -104,6 +104,74 @@ RSpec.describe "Chat composer", type: :system do
expect(channel_page.composer.value).to eq("bb")
end
context "when user preference is set to send on enter" do
before { current_user.user_option.update!(chat_send_shortcut: 0) }
context "when pressing enter" do
it "sends the message" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").enter_shortcut
expect(channel_page.messages).to have_message(text: "testenter")
end
end
context "when pressing shift + enter" do
it "adds a linebreak" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").shift_enter_shortcut
expect(channel_page.composer.value).to eq("testenter\n")
end
end
context "when pressing meta + enter" do
it "sends the message" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").meta_enter_shortcut
expect(channel_page.messages).to have_message(text: "testenter")
end
end
end
context "when user preference is set to send on meta + enter" do
before { current_user.user_option.update!(chat_send_shortcut: 1) }
context "when pressing enter" do
it "adds a linebreak" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").enter_shortcut
expect(channel_page.composer.value).to eq("testenter\n")
end
end
context "when pressing shift + enter" do
it "adds a linebreak" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").shift_enter_shortcut
expect(channel_page.composer.value).to eq("testenter\n")
end
end
context "when pressing meta + enter" do
it "sends the message" do
chat_page.visit_channel(channel_1)
channel_page.composer.fill_in(with: "testenter").meta_enter_shortcut
expect(channel_page.messages).to have_message(text: "testenter")
end
end
end
end
context "when editing a message with no length" do

View File

@ -42,6 +42,7 @@ module PageObjects
def fill_in(**args)
input.fill_in(**args)
self
end
def value
@ -105,6 +106,18 @@ module PageObjects
def focused?
component.has_css?(".chat-composer.is-focused")
end
def enter_shortcut
input.send_keys(:enter)
end
def shift_enter_shortcut
input.send_keys(%i[shift enter])
end
def meta_enter_shortcut
input.send_keys([PLATFORM_KEY_MODIFIER, :enter])
end
end
end
end

View File

@ -62,6 +62,15 @@ RSpec.describe "User chat preferences", type: :system do
expect(select_kit).to have_selected_value("fullscreen")
end
it "can select send shorcut sidebar mode" do
visit("/my/preferences")
find(".user-nav__preferences-chat", visible: :all).click
find("#chat_send_shortcut_meta_enter").click
find(".save-changes").click
expect(page).to have_checked_field("chat_send_shortcut_meta_enter")
end
context "as an admin on another user's preferences" do
fab!(:current_user) { Fabricate(:admin) }
fab!(:user_1) { Fabricate(:user) }