UX: enhances chat copy features (#23770)

- Allows to copy quotes from mobile
- Allows to copy text of a message from mobile
- Allows to select messages by clicking on it when selection has started

Note this commit is also now using toasts to show a confirmation of copy, and refactors system specs helpers concerning secondary actions.

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Joffrey JAFFEUX
2023-10-04 16:14:37 +02:00
committed by GitHub
parent 24feb20abc
commit 08df8fc1d1
20 changed files with 273 additions and 162 deletions

View File

@ -84,6 +84,7 @@ export default class ChatMessage extends Component {
{{on "mouseenter" this.onMouseEnter passive=true}}
{{on "mouseleave" this.onMouseLeave passive=true}}
{{on "mousemove" this.onMouseMove passive=true}}
{{on "click" this.toggleCheckIfPossible passive=true}}
{{ChatOnLongPress
this.onLongPressStart
this.onLongPressEnd
@ -193,6 +194,7 @@ export default class ChatMessage extends Component {
@service chatThreadPane;
@service chatChannelsManager;
@service router;
@service toasts;
@tracked isActive = false;
@ -278,10 +280,27 @@ export default class ChatMessage extends Component {
recursiveExpand(this.args.message);
}
@action
toggleCheckIfPossible(event) {
if (!this.pane.selectingMessages) {
return;
}
if (event.shiftKey) {
this.messageInteractor.bulkSelect(!this.args.message.selected);
return;
}
this.messageInteractor.select(!this.args.message.selected);
}
@action
toggleChecked(event) {
event.stopPropagation();
if (event.shiftKey) {
this.messageInteractor.bulkSelect(event.target.checked);
return;
}
this.messageInteractor.select(event.target.checked);

View File

@ -6,23 +6,62 @@ import { isTesting } from "discourse-common/config/environment";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
import ChatModalMoveMessageToChannel from "discourse/plugins/chat/discourse/components/chat/modal/move-message-to-channel";
import DButton from "discourse/components/d-button";
import not from "truth-helpers/helpers/not";
import I18n from "I18n";
export default class ChatSelectionManager extends Component {
<template>
<div
class="chat-selection-management"
data-last-copy-successful={{this.lastCopySuccessful}}
>
<div class="chat-selection-management__buttons">
<DButton
@icon="quote-left"
@label="chat.selection.quote_selection"
@disabled={{not this.anyMessagesSelected}}
@action={{this.quoteMessages}}
id="chat-quote-btn"
/>
<DButton
@icon="copy"
@label="chat.selection.copy"
@disabled={{not this.anyMessagesSelected}}
@action={{this.copyMessages}}
id="chat-copy-btn"
/>
{{#if this.enableMove}}
<DButton
@icon="sign-out-alt"
@label="chat.selection.move_selection_to_channel"
@disabled={{not this.anyMessagesSelected}}
@action={{this.openMoveMessageModal}}
id="chat-move-to-channel-btn"
/>
{{/if}}
<DButton
@icon="times"
@label="chat.selection.cancel"
@action={{@pane.cancelSelecting}}
id="chat-cancel-selection-btn"
class="btn-secondary cancel-btn"
/>
</div>
</div>
</template>
@service("composer") topicComposer;
@service router;
@service modal;
@service site;
@service toasts;
@service("chat-api") api;
// NOTE: showCopySuccess is used to display the message which animates
// after a delay. The on-animation-end helper is not really usable in
// system specs because it fires straight away, so we use lastCopySuccessful
// with a data attr instead so it's not instantly mutated.
@tracked showCopySuccess = false;
@tracked lastCopySuccessful = false;
get enableMove() {
return this.args.enableMove ?? false;
}
@ -96,16 +135,17 @@ export default class ChatSelectionManager extends Component {
@action
async copyMessages() {
try {
this.lastCopySuccessful = false;
this.showCopySuccess = false;
if (!isTesting()) {
// clipboard API throws errors in tests
await clipboardCopyAsync(this.generateQuote);
}
this.showCopySuccess = true;
this.lastCopySuccessful = true;
this.toasts.success({
duration: 3000,
data: {
message: I18n.t("chat.quote.copy_success"),
},
});
}
} catch (error) {
popupAjaxError(error);
}

View File

@ -1,51 +0,0 @@
<div
class="chat-selection-management"
data-last-copy-successful={{this.lastCopySuccessful}}
>
<div class="chat-selection-management__buttons">
<DButton
@icon="quote-left"
@label="chat.selection.quote_selection"
@disabled={{not this.anyMessagesSelected}}
@action={{this.quoteMessages}}
id="chat-quote-btn"
/>
{{#if this.site.desktopView}}
<DButton
@icon="copy"
@label="chat.selection.copy"
@disabled={{not this.anyMessagesSelected}}
@action={{this.copyMessages}}
id="chat-copy-btn"
/>
{{/if}}
{{#if this.enableMove}}
<DButton
@icon="sign-out-alt"
@label="chat.selection.move_selection_to_channel"
@disabled={{not this.anyMessagesSelected}}
@action={{this.openMoveMessageModal}}
id="chat-move-to-channel-btn"
/>
{{/if}}
<DButton
@icon="times"
@label="chat.selection.cancel"
@action={{@pane.cancelSelecting}}
id="chat-cancel-selection-btn"
class="btn-secondary cancel-btn"
/>
</div>
{{#if this.showCopySuccess}}
<span
class="chat-selection-management__copy-success"
{{chat/on-animation-end (fn (mut this.showCopySuccess) false)}}
>
{{i18n "chat.quote.copy_success"}}
</span>
{{/if}}
</div>

View File

@ -45,6 +45,7 @@ export default class ChatMessageInteractor {
@service router;
@service modal;
@service capabilities;
@service toasts;
@tracked message = null;
@tracked context = null;
@ -166,6 +167,14 @@ export default class ChatMessageInteractor {
icon: "link",
});
if (this.site.mobileView) {
buttons.push({
id: "copyText",
name: I18n.t("chat.copy_text"),
icon: "clipboard",
});
}
if (this.canEdit) {
buttons.push({
id: "edit",
@ -237,6 +246,14 @@ export default class ChatMessageInteractor {
}
}
copyText() {
clipboardCopy(this.message.message);
this.toasts.success({
duration: 3000,
data: { message: I18n.t("chat.text_copied") },
});
}
copyLink() {
const { protocol, host } = window.location;
const channelId = this.message.channel.id;
@ -251,6 +268,10 @@ export default class ChatMessageInteractor {
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
clipboardCopy(url);
this.toasts.success({
duration: 3000,
data: { message: I18n.t("chat.link_copied") },
});
}
@action