mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 01:07:19 +08:00
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:
@ -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);
|
||||
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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
|
||||
|
Reference in New Issue
Block a user