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:
parent
a66e5ff728
commit
2cff8c82e3
|
@ -611,6 +611,12 @@ textarea {
|
|||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
// Special elements
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
Loading…
Reference in New Issue