FEATURE: Add copy quote button to post selection menu (#25139)

Merges the design experiment at
https://meta.discourse.org/t/post-quote-copy-to-clipboard-button-feedback/285376
into core.

This adds a new button by default to the menu that pops up when text is
selected in a post.

The normal Quote button that is shown when selecting text within a post
will open the composer with the quote markdown prefilled.

This new "Copy Quote" button copies the quote markdown directly to the
user’s clipboard. This is useful for when you want to copy the quote
elsewhere – to another topic or a chat message for instance – without
having to manually copy from the opened composer, which then has to be
dismissed afterwards. An example of quote markdown:

```
[quote="someuser, post:7, topic:285376"]
In this moment, I am euphoric.
[/quote]
```
This commit is contained in:
Martin Brennan 2024-01-08 10:38:14 +10:00 committed by GitHub
parent a720bdc72b
commit 51016e56dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 83 additions and 3 deletions

View File

@ -12,8 +12,13 @@ import PluginOutlet from "discourse/components/plugin-outlet";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import Sharing from "discourse/lib/sharing"; import Sharing from "discourse/lib/sharing";
import { postUrl, setCaretPosition } from "discourse/lib/utilities"; import {
clipboardCopy,
postUrl,
setCaretPosition,
} from "discourse/lib/utilities";
import { getAbsoluteURL } from "discourse-common/lib/get-url"; import { getAbsoluteURL } from "discourse-common/lib/get-url";
import I18n from "discourse-i18n";
export function fixQuotes(str) { export function fixQuotes(str) {
// u+201c, u+201d = “ ” // u+201c, u+201d = “ ”
@ -27,6 +32,7 @@ export default class PostTextSelectionToolbar extends Component {
@service site; @service site;
@service siteSettings; @service siteSettings;
@service appEvents; @service appEvents;
@service toasts;
@tracked isFastEditing = false; @tracked isFastEditing = false;
@ -96,6 +102,17 @@ export default class PostTextSelectionToolbar extends Component {
event.stopPropagation(); event.stopPropagation();
} }
@action
async copyQuoteToClipboard() {
const text = await this.args.data.buildQuote();
clipboardCopy(text);
this.toasts.success({
duration: 3000,
data: { message: I18n.t("post.quote_copied_to_clibboard") },
});
await this.args.data.hideToolbar();
}
@action @action
async closeFastEdit() { async closeFastEdit() {
this.isFastEditing = false; this.isFastEditing = false;
@ -216,6 +233,16 @@ export default class PostTextSelectionToolbar extends Component {
/> />
{{/if}} {{/if}}
{{#if @data.canCopyQuote}}
<DButton
@icon="copy"
@label="post.quote_copy"
@title="post.quote_copy"
class="btn-flat copy-quote"
{{on "click" this.copyQuoteToClipboard}}
/>
{{/if}}
<PluginOutlet <PluginOutlet
@name="quote-share-buttons-before" @name="quote-share-buttons-before"
@connectorTagName="span" @connectorTagName="span"

View File

@ -204,6 +204,7 @@ export default class PostTextSelection extends Component {
trapTab: false, trapTab: false,
data: { data: {
canEditPost: this.canEditPost, canEditPost: this.canEditPost,
canCopyQuote: this.canCopyQuote,
editPost: this.args.editPost, editPost: this.args.editPost,
supportsFastEdit, supportsFastEdit,
topic: this.args.topic, topic: this.args.topic,
@ -258,6 +259,10 @@ export default class PostTextSelection extends Component {
return this.siteSettings.enable_fast_edit && this.post?.can_edit; return this.siteSettings.enable_fast_edit && this.post?.can_edit;
} }
get canCopyQuote() {
return this.siteSettings.enable_quote_copy;
}
// on Desktop, shows the bar at the beginning of the selection // on Desktop, shows the bar at the beginning of the selection
// on Mobile, shows the bar at the end of the selection // on Mobile, shows the bar at the end of the selection
@cached @cached

View File

@ -3456,6 +3456,8 @@ en:
quote_reply_shortcut: "Quote (or press q)" quote_reply_shortcut: "Quote (or press q)"
quote_edit: "Edit" quote_edit: "Edit"
quote_edit_shortcut: "Edit (or press e)" quote_edit_shortcut: "Edit (or press e)"
quote_copy: "Copy Quote"
quote_copied_to_clibboard: "Quote copied to clipboard"
quote_share: "Share" quote_share: "Share"
edit_reason: "Reason: " edit_reason: "Reason: "
post_number: "post %{number}" post_number: "post %{number}"

View File

@ -2353,7 +2353,8 @@ en:
watched_words_regular_expressions: "Watched words are regular expressions." watched_words_regular_expressions: "Watched words are regular expressions."
enable_diffhtml_preview: "Experimental feature which uses diffHTML to sync preview instead of full re-render" enable_diffhtml_preview: "Experimental feature which uses diffHTML to sync preview instead of full re-render"
enable_fast_edit: "Enables small selection of a post text to be edited inline." enable_fast_edit: "Adds a button to the post selection menu to edit a small selection inline."
enable_quote_copy: "Adds a button to post selection menu to copy the selection to clipboard as a markdown quote."
old_post_notice_days: "Days before post notice becomes old" old_post_notice_days: "Days before post notice becomes old"
new_user_notice_tl: "Minimum trust level required to see new user post notices." new_user_notice_tl: "Minimum trust level required to see new user post notices."

View File

@ -1130,6 +1130,9 @@ posting:
enable_fast_edit: enable_fast_edit:
default: true default: true
client: true client: true
enable_quote_copy:
default: true
client: true
old_post_notice_days: old_post_notice_days:
default: 14 default: 14
max: 36500 max: 36500

View File

@ -173,6 +173,14 @@ module PageObjects
@fast_edit_component.fast_edit_input @fast_edit_component.fast_edit_input
end end
def copy_quote_button_selector
".quote-button .copy-quote"
end
def copy_quote_button
find(copy_quote_button_selector)
end
def click_mention(post, mention) def click_mention(post, mention)
within post_by_number(post) do within post_by_number(post) do
find("a.mention-group", text: mention).click find("a.mention-group", text: mention).click

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
describe "Post selection | Copy quote", type: :system do
let(:topic_page) { PageObjects::Pages::Topic.new }
let(:cdp) { PageObjects::CDP.new }
fab!(:topic)
fab!(:post) { Fabricate(:post, topic: topic, raw: "Hello world it's time for quoting!") }
fab!(:current_user) { Fabricate(:admin) }
before do
sign_in(current_user)
cdp.allow_clipboard
end
it "copies the selection from the post the clipboard" do
topic_page.visit_topic(topic)
select_text_range("#{topic_page.post_by_number_selector(1)} .cooked p", 0, 10)
topic_page.copy_quote_button.click
expect(cdp.read_clipboard.chomp).to eq(<<~QUOTE.chomp)
[quote=\"#{post.user.username}, post:1, topic:#{topic.id}\"]\nHello worl\n[/quote]\n
QUOTE
end
it "does not show the copy quote button if it has been disabled" do
SiteSetting.enable_quote_copy = false
topic_page.visit_topic(topic)
select_text_range("#{topic_page.post_by_number_selector(1)} .cooked p", 0, 10)
expect(page).not_to have_css(topic_page.copy_quote_button_selector)
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
describe "Fast edit", type: :system do describe "Post selection | Fast edit", type: :system do
let(:topic_page) { PageObjects::Pages::Topic.new } let(:topic_page) { PageObjects::Pages::Topic.new }
let(:fast_editor) { PageObjects::Components::FastEditor.new } let(:fast_editor) { PageObjects::Components::FastEditor.new }
fab!(:topic) fab!(:topic)