From 6513ca69da732af240edbc72a9489c518435bf3c Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 8 Jun 2023 08:21:32 +0200 Subject: [PATCH] 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 --- .../components/chat-message-reaction.js | 9 ++- .../discourse/components/chat-message.hbs | 8 +- .../discourse/components/chat-message.js | 41 ++-------- .../discourse/modifiers/chat/on-long-press.js | 81 +++++++++++++++++++ .../stylesheets/mobile/chat-message.scss | 21 +++++ 5 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 plugins/chat/assets/javascripts/discourse/modifiers/chat/on-long-press.js diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.js index b909b70319b..eb7ed845128 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.js @@ -47,17 +47,18 @@ export default class ChatMessageReaction extends Component { @action handleTouchStart(event) { - event.stopPropagation(); + this.handleClick(event); } @action - handleClick() { + handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.args.onReaction?.( this.args.reaction.emoji, this.args.reaction.reacted ? "remove" : "add" ); - - return false; } get popoverContent() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs index 0d76c37348c..70a16b5faaa 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs @@ -11,12 +11,14 @@ {{did-insert this.decorateCookedMessage}} {{did-update this.decorateCookedMessage @message.id}} {{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 "mouseleave" this.onMouseLeave passive=true}} {{on "mousemove" this.onMouseMove passive=true}} + {{chat/on-long-press + this.handleLongPressStart + this.handleLongPressEnd + this.onLongPressCancel + }} class={{concat-class "chat-message-container" (if this.pane.selectingMessages "selecting-messages") diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js index c6b2396a403..38da96a3a88 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js @@ -1,4 +1,3 @@ -import { isTesting } from "discourse-common/config/environment"; import { action } from "@ember/object"; import Component from "@glimmer/component"; import I18n from "I18n"; @@ -258,47 +257,19 @@ export default class ChatMessage extends Component { } @action - handleTouchStart(event) { - event.stopPropagation(); - - // 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); + handleLongPressStart(element) { + element.classList.add("is-long-pressed"); } @action - handleTouchMove(event) { - event.stopPropagation(); - - cancel(this._isPressingHandler); + onLongPressCancel(element) { + element.classList.remove("is-long-pressed"); } @action - handleTouchEnd(event) { - event.stopPropagation(); + handleLongPressEnd(element) { + 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 zoomed don't handle long press return; diff --git a/plugins/chat/assets/javascripts/discourse/modifiers/chat/on-long-press.js b/plugins/chat/assets/javascripts/discourse/modifiers/chat/on-long-press.js new file mode 100644 index 00000000000..6a0d0733dec --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/modifiers/chat/on-long-press.js @@ -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(); + } +} diff --git a/plugins/chat/assets/stylesheets/mobile/chat-message.scss b/plugins/chat/assets/stylesheets/mobile/chat-message.scss index 84874803e56..f744b6451d9 100644 --- a/plugins/chat/assets/stylesheets/mobile/chat-message.scss +++ b/plugins/chat/assets/stylesheets/mobile/chat-message.scss @@ -8,4 +8,25 @@ @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); + } + } }