UX: improves chat message long press and touch (#21984)

This commit attempts to refactor our long press logic to make it more resilient and precise.

With this improvement two very UX/UI changes have been made:
- scale animation on long press
- prevents click on reaction to propagate to the message which would cause the active state of the message to trigger
This commit is contained in:
Joffrey JAFFEUX 2023-06-08 08:21:32 +02:00 committed by GitHub
parent 482ef0782d
commit 6513ca69da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 42 deletions

View File

@ -47,17 +47,18 @@ export default class ChatMessageReaction extends Component {
@action @action
handleTouchStart(event) { handleTouchStart(event) {
event.stopPropagation(); this.handleClick(event);
} }
@action @action
handleClick() { handleClick(event) {
event.stopPropagation();
event.preventDefault();
this.args.onReaction?.( this.args.onReaction?.(
this.args.reaction.emoji, this.args.reaction.emoji,
this.args.reaction.reacted ? "remove" : "add" this.args.reaction.reacted ? "remove" : "add"
); );
return false;
} }
get popoverContent() { get popoverContent() {

View File

@ -11,12 +11,14 @@
{{did-insert this.decorateCookedMessage}} {{did-insert this.decorateCookedMessage}}
{{did-update this.decorateCookedMessage @message.id}} {{did-update this.decorateCookedMessage @message.id}}
{{did-update this.decorateCookedMessage @message.version}} {{did-update this.decorateCookedMessage @message.version}}
{{on "touchmove" this.handleTouchMove passive=true}}
{{on "touchstart" this.handleTouchStart passive=true}}
{{on "touchend" this.handleTouchEnd}}
{{on "mouseenter" this.onMouseEnter passive=true}} {{on "mouseenter" this.onMouseEnter passive=true}}
{{on "mouseleave" this.onMouseLeave passive=true}} {{on "mouseleave" this.onMouseLeave passive=true}}
{{on "mousemove" this.onMouseMove passive=true}} {{on "mousemove" this.onMouseMove passive=true}}
{{chat/on-long-press
this.handleLongPressStart
this.handleLongPressEnd
this.onLongPressCancel
}}
class={{concat-class class={{concat-class
"chat-message-container" "chat-message-container"
(if this.pane.selectingMessages "selecting-messages") (if this.pane.selectingMessages "selecting-messages")

View File

@ -1,4 +1,3 @@
import { isTesting } from "discourse-common/config/environment";
import { action } from "@ember/object"; import { action } from "@ember/object";
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import I18n from "I18n"; import I18n from "I18n";
@ -258,47 +257,19 @@ export default class ChatMessage extends Component {
} }
@action @action
handleTouchStart(event) { handleLongPressStart(element) {
event.stopPropagation(); element.classList.add("is-long-pressed");
// if zoomed don't track long press
if (isZoomed()) {
return;
}
// when testing this must be triggered immediately because there
// is no concept of "long press" there, the Ember `tap` test helper
// does send the touchstart/touchend events but immediately, see
// https://github.com/emberjs/ember-test-helpers/blob/master/API.md#tap
if (isTesting()) {
this._handleLongPress();
}
this._touchStartAt = Date.now();
this._isPressingHandler = discourseLater(this._handleLongPress, 500);
} }
@action @action
handleTouchMove(event) { onLongPressCancel(element) {
event.stopPropagation(); element.classList.remove("is-long-pressed");
cancel(this._isPressingHandler);
} }
@action @action
handleTouchEnd(event) { handleLongPressEnd(element) {
event.stopPropagation(); element.classList.remove("is-long-pressed");
// this is to prevent the long press to register as a click
if (Date.now() - this._touchStartAt >= 500) {
event.preventDefault();
}
cancel(this._isPressingHandler);
}
@action
_handleLongPress() {
if (isZoomed()) { if (isZoomed()) {
// if zoomed don't handle long press // if zoomed don't handle long press
return; return;

View File

@ -0,0 +1,81 @@
import Modifier from "ember-modifier";
import { registerDestructor } from "@ember/destroyable";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later";
function cancelEvent(event) {
event.stopPropagation();
event.preventDefault();
}
export default class ChatOnLongPress extends Modifier {
@service capabilities;
@service site;
constructor(owner, args) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());
}
get enabled() {
return this.capabilities.touch && this.site.mobileView;
}
modify(element, [onLongPressStart, onLongPressEnd, onLongPressCancel]) {
if (!this.enabled) {
return;
}
this.element = element;
this.onLongPressStart = onLongPressStart || (() => {});
this.onLongPressEnd = onLongPressEnd || (() => {});
this.onLongPressCancel = onLongPressCancel || (() => {});
element.addEventListener("touchstart", this.handleTouchStart, {
passive: true,
});
}
@bind
onCancel() {
cancel(this.timeout);
this.element.removeEventListener("touchmove", this.onCancel);
this.element.removeEventListener("touchend", this.onCancel);
this.element.removeEventListener("touchcancel", this.onCancel);
this.onLongPressCancel(this.element);
}
@bind
handleTouchStart(event) {
if (event.touches.length > 1) {
return;
}
this.onLongPressStart(this.element, event);
this.element.addEventListener("touchmove", this.onCancel);
this.element.addEventListener("touchend", this.onCancel);
this.element.addEventListener("touchcancel", this.onCancel);
this.timeout = discourseLater(() => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.onLongPressEnd(this.element, event);
this.element.addEventListener("touchend", cancelEvent, {
once: true,
});
}, 400);
}
cleanup() {
if (!this.enabled) {
return;
}
this.onCancel();
}
}

View File

@ -8,4 +8,25 @@
@include user-select(none); @include user-select(none);
} }
} }
.chat-message-container {
transition: transform 400ms;
transform: scale(1);
&.is-long-pressed {
animation: scale-animation 400ms;
}
}
@keyframes scale-animation {
0% {
transform: scale(1);
}
80% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}
} }