From b8d5f951f61cfbfac6a7e8582c6dacd710815c36 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 11 Sep 2023 14:51:13 +0200 Subject: [PATCH] UX: implements swipe on row channel (#23436) On mobile swiping a channel row will now show a "Remove" option. Holding this to the end will now remove this row from your list of followed direct message channels. Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com> --- .../discourse/components/channels-list.hbs | 9 +- .../discourse/components/channels-list.js | 6 - .../discourse/components/chat-channel-row.gjs | 252 ++++++++++++++++++ .../discourse/components/chat-channel-row.hbs | 35 --- .../discourse/components/chat-channel-row.js | 28 -- .../{chat-message.js => chat-message.gjs} | 153 +++++++++++ .../discourse/components/chat-message.hbs | 130 --------- .../stylesheets/common/chat-channel-row.scss | 146 ++++++++++ .../assets/stylesheets/common/chat-index.scss | 147 ---------- .../chat/assets/stylesheets/common/index.scss | 1 + .../stylesheets/mobile/chat-channel-row.scss | 54 ++++ .../assets/stylesheets/mobile/chat-index.scss | 19 +- .../chat/assets/stylesheets/mobile/index.scss | 1 + plugins/chat/config/locales/client.en.yml | 1 + .../spec/system/list_channels/mobile_spec.rb | 5 +- 15 files changed, 621 insertions(+), 366 deletions(-) create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js rename plugins/chat/assets/javascripts/discourse/components/{chat-message.js => chat-message.gjs} (61%) delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-message.hbs create mode 100644 plugins/chat/assets/stylesheets/common/chat-channel-row.scss create mode 100644 plugins/chat/assets/stylesheets/mobile/chat-channel-row.scss diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs index 70660effd69..8c033e3352a 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs @@ -43,7 +43,14 @@ -
+
{{#if this.publicChannelsEmpty}}
{{i18n "chat.no_public_channels"}} diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.js b/plugins/chat/assets/javascripts/discourse/components/channels-list.js index b7011cfbae8..486a9dc6f8c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.js +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.js @@ -65,12 +65,6 @@ export default class ChannelsList extends Component { return this.chat.userCanDirectMessage; } - get publicChannelClasses() { - return `channels-list-container public-channels ${ - this.inSidebar ? "collapsible-sidebar-section" : "" - }`; - } - get displayPublicChannels() { if (!this.siteSettings.enable_public_channels) { return false; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs new file mode 100644 index 00000000000..358f1ba1106 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs @@ -0,0 +1,252 @@ +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { LinkTo } from "@ember/routing"; +import concatClass from "discourse/helpers/concat-class"; +import eq from "truth-helpers/helpers/eq"; +import and from "truth-helpers/helpers/and"; +import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title"; +import ChatChannelMetadata from "discourse/plugins/chat/discourse/components/chat-channel-metadata"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import ToggleChannelMembershipButton from "discourse/plugins/chat/discourse/components/toggle-channel-membership-button"; +import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; +import { hash } from "@ember/helper"; +import I18n from "I18n"; +import { modifier } from "ember-modifier"; +import { bind } from "discourse-common/utils/decorators"; +import { tracked } from "@glimmer/tracking"; +import discourseLater from "discourse-common/lib/later"; +import { cancel } from "@ember/runloop"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const RESET_CLASS = "-reset"; +const FADEOUT_CLASS = "-fade-out"; + +export default class ChatChannelRow extends Component { + + + @service router; + @service chat; + @service capabilities; + @service currentUser; + @service site; + @service api; + + @tracked shouldRemoveChannel = false; + @tracked hasReachedThreshold = false; + @tracked isCancelling = false; + @tracked shouldResetRow = false; + @tracked actionButton; + @tracked swipableRow; + + positionActionButton = modifier((element) => { + element.style.left = "100%"; + }); + + registerActionButton = modifier((element) => { + this.actionButton = element; + }); + + registerSwipableRow = modifier((element) => { + this.swipableRow = element; + }); + + onRemoveChannel = modifier(() => { + this.swipableRow.classList.add(FADEOUT_CLASS); + + const handler = discourseLater(() => { + this.chat.unfollowChannel(this.args.channel).catch(popupAjaxError); + }, 250); + + return () => { + cancel(handler); + }; + }); + + handleSwipe = modifier((element) => { + element.addEventListener("touchstart", this.onSwipeStart, { + passive: true, + }); + element.addEventListener("touchmove", this.onSwipe); + element.addEventListener("touchend", this.onSwipeEnd); + + return () => { + element.removeEventListener("touchstart", this.onSwipeStart); + element.removeEventListener("touchmove", this.onSwipe); + element.removeEventListener("touchend", this.onSwipeEnd); + }; + }); + + onResetRow = modifier(() => { + this.swipableRow.classList.add(RESET_CLASS); + this.swipableRow.style.left = "0px"; + + const handler = discourseLater(() => { + this.isCancelling = false; + this.hasReachedThreshold = false; + this.shouldResetRow = false; + this.swipableRow.classList.remove(RESET_CLASS); + }, 250); + + return () => { + cancel(handler); + this.swipableRow.classList.remove(RESET_CLASS); + }; + }); + + _lastX = null; + _towardsThreshold = false; + + @bind + onSwipeStart(event) { + this.hasReachedThreshold = false; + this.isCancelling = false; + this._lastX = this.initialX = event.changedTouches[0].screenX; + } + + @bind + onSwipe(event) { + event.preventDefault(); + + const touchX = event.changedTouches[0].screenX; + const diff = this.initialX - touchX; + + // we don't state to be too sensitive to the touch + if (Math.abs(this._lastX - touchX) > 5) { + this._towardsThreshold = this._lastX >= touchX; + this._lastX = touchX; + } + + // ensures we will go back to the initial position when swiping very fast + if (diff < 10) { + this.isCancelling = false; + this.hasReachedThreshold = false; + this.swipableRow.style.left = "0px"; + return; + } + + if (diff >= window.innerWidth / 3) { + this.isCancelling = false; + this.hasReachedThreshold = true; + return; + } else { + this.isCancelling = !this._towardsThreshold; + } + + this.actionButton.style.width = diff + "px"; + this.swipableRow.style.left = -(this.initialX - touchX) + "px"; + } + + @bind + onSwipeEnd(event) { + this._lastX = null; + const diff = this.initialX - event.changedTouches[0].screenX; + + if (diff >= window.innerWidth / 3) { + this.swipableRow.style.left = "0px"; + this.shouldRemoveChannel = true; + return; + } + + this.isCancelling = true; + this.shouldResetRow = true; + } + + get shouldHandleSwipe() { + return this.capabilities.touch && this.args.channel.isDirectMessageChannel; + } + + get cancelActionLabel() { + return I18n.t("cancel_value"); + } + + get removeActionLabel() { + return I18n.t("chat.remove"); + } + + get leaveDirectMessageLabel() { + return I18n.t("chat.direct_messages.leave"); + } + + get leaveChannelLabel() { + return I18n.t("chat.channel_settings.leave_channel"); + } + + get channelHasUnread() { + return this.args.channel.tracking.unreadCount > 0; + } + + get #firstDirectMessageUser() { + return this.args.channel?.chatable?.users?.firstObject; + } + + @action + startTrackingStatus() { + this.#firstDirectMessageUser?.trackStatus(); + } + + @action + stopTrackingStatus() { + this.#firstDirectMessageUser?.stopTrackingStatus(); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs deleted file mode 100644 index 3d1e442ae8d..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs +++ /dev/null @@ -1,35 +0,0 @@ - - - - - {{#if (and @options.leaveButton @channel.isFollowing this.site.desktopView)}} - - {{/if}} - \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js deleted file mode 100644 index efd003a89ab..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js +++ /dev/null @@ -1,28 +0,0 @@ -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; -import { action } from "@ember/object"; - -export default class ChatChannelRow extends Component { - @service router; - @service chat; - @service currentUser; - @service site; - - @action - startTrackingStatus() { - this.#firstDirectMessageUser?.trackStatus(); - } - - @action - stopTrackingStatus() { - this.#firstDirectMessageUser?.stopTrackingStatus(); - } - - get channelHasUnread() { - return this.args.channel.tracking.unreadCount > 0; - } - - get #firstDirectMessageUser() { - return this.args.channel?.chatable?.users?.firstObject; - } -} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs similarity index 61% rename from plugins/chat/assets/javascripts/discourse/components/chat-message.js rename to plugins/chat/assets/javascripts/discourse/components/chat-message.gjs index 57f796172c9..da8eecde90f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs @@ -12,6 +12,26 @@ import discourseDebounce from "discourse-common/lib/debounce"; import { bind } from "discourse-common/utils/decorators"; import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-mention"; import { tracked } from "@glimmer/tracking"; +import ChatMessageSeparatorDate from "discourse/plugins/chat/discourse/components/chat-message-separator-date"; +import ChatMessageSeparatorNew from "discourse/plugins/chat/discourse/components/chat-message-separator-new"; +import concatClass from "discourse/helpers/concat-class"; +import DButton from "discourse/components/d-button"; +import ChatMessageInReplyToIndicator from "discourse/plugins/chat/discourse/components/chat-message-in-reply-to-indicator"; +import ChatMessageLeftGutter from "discourse/plugins/chat/discourse/components/chat/message/left-gutter"; +import ChatMessageAvatar from "discourse/plugins/chat/discourse/components/chat/message/avatar"; +import ChatMessageError from "discourse/plugins/chat/discourse/components/chat/message/error"; +import ChatMessageInfo from "discourse/plugins/chat/discourse/components/chat/message/info"; +import ChatMessageText from "discourse/plugins/chat/discourse/components/chat-message-text"; +import ChatMessageReaction from "discourse/plugins/chat/discourse/components/chat-message-reaction"; +import ChatMessageThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator"; +import eq from "truth-helpers/helpers/eq"; +import not from "truth-helpers/helpers/not"; +import { on } from "@ember/modifier"; +import { Input } from "@ember/component"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import didUpdate from "@ember/render-modifiers/modifiers/did-update"; +import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; +import ChatOnLongPress from "discourse/plugins/chat/discourse/modifiers/chat/on-long-press"; let _chatMessageDecorators = []; let _tippyInstances = []; @@ -28,6 +48,139 @@ export const MENTION_KEYWORDS = ["here", "all"]; export const MESSAGE_CONTEXT_THREAD = "thread"; export default class ChatMessage extends Component { + + @service site; @service dialog; @service currentUser; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs deleted file mode 100644 index 6727dfcfae7..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs +++ /dev/null @@ -1,130 +0,0 @@ -{{! template-lint-disable no-invalid-interactive }} - -{{#if this.shouldRender}} - {{#if (eq @context "channel")}} - - - {{/if}} - -
- {{#if this.show}} - {{#if this.pane.selectingMessages}} - - {{/if}} - - {{#if this.deletedAndCollapsed}} -
- -
- {{else if this.hiddenAndCollapsed}} -
- -
- {{else}} -
- {{#unless this.hideReplyToInfo}} - - {{/unless}} - - {{#if this.hideUserInfo}} - - {{else}} - - {{/if}} - -
- - - - {{#if @message.reactions.length}} -
- {{#each @message.reactions as |reaction|}} - - {{/each}} - - {{#if this.shouldRenderOpenEmojiPickerButton}} - - {{/if}} -
- {{/if}} -
- - -
- - {{#if this.showThreadIndicator}} - - {{/if}} -
- {{/if}} - {{/if}} -
-{{/if}} \ No newline at end of file diff --git a/plugins/chat/assets/stylesheets/common/chat-channel-row.scss b/plugins/chat/assets/stylesheets/common/chat-channel-row.scss new file mode 100644 index 00000000000..cfacc351a5a --- /dev/null +++ b/plugins/chat/assets/stylesheets/common/chat-channel-row.scss @@ -0,0 +1,146 @@ +.chat-channel-row { + align-items: center; + box-sizing: border-box; + display: flex; + justify-content: space-between; + position: relative; + cursor: pointer; + color: var(--primary-high); + + @media (hover: none) { + &:hover, + &:focus { + background: transparent; + } + + &:active { + background: var(--primary-low); + } + } + + @media (hover: hover) { + &:hover, + &.active { + background: var(--primary-very-low); + } + + &.can-leave:hover { + .toggle-channel-membership-button.-leave { + display: block; + + > * { + pointer-events: auto; + } + } + + .chat-channel-metadata { + display: none; + } + } + } + + &:hover, + &.active { + .chat-channel-title { + &, + .category-chat-name, + .dm-usernames { + color: var(--primary); + } + + .d-icon-lock { + background-color: var(--primary-low); + } + } + } + + &:visited { + color: var(--primary-high); + } + + &.muted { + opacity: 0.65; + } + + .chat-channel-title { + &__users-count { + width: var(--channel-list-avatar-size); + height: var(--channel-list-avatar-size); + padding: 0; + font-size: var(--font-up-1); + justify-content: center; + } + + &__avatar { + .chat-user-avatar { + img { + width: calc(var(--channel-list-avatar-size) - 2px); + height: calc(var(--channel-list-avatar-size) - 2px); + } + } + } + &__user-info { + @include ellipsis; + } + &__usernames { + display: flex; + align-items: center; + justify-content: start; + } + .user-status-message { + display: inline-block; + font-size: var(--font-down-2); + margin-right: 0.5rem; + + &-description { + color: var(--primary-medium); + } + } + } + + .chat-channel-metadata { + display: flex; + align-items: flex-end; + flex-direction: column; + margin-left: 0.5em; + + &__date { + color: var(--primary-high); + font-size: var(--font-down-2); + white-space: nowrap; + } + + .chat-channel-unread-indicator { + @include chat-unread-indicator; + display: flex; + align-items: center; + justify-content: center; + width: 8px; + height: 8px; + + &.-urgent { + width: auto; + height: auto; + min-width: 0.6em; + padding: 0.3em 0.5em; + } + } + } + + &.unfollowing { + opacity: 0; + } + + .toggle-channel-membership-button.-leave { + display: none; + margin-left: auto; + } + .badge-wrapper { + align-items: center; + margin-right: 0; + } + + .emoji { + margin-left: 0.3em; + } +} diff --git a/plugins/chat/assets/stylesheets/common/chat-index.scss b/plugins/chat/assets/stylesheets/common/chat-index.scss index fbc7977bfa0..0d4df7f5349 100644 --- a/plugins/chat/assets/stylesheets/common/chat-index.scss +++ b/plugins/chat/assets/stylesheets/common/chat-index.scss @@ -88,151 +88,4 @@ padding-top: 1rem; } } - - .chat-channel-row { - align-items: center; - box-sizing: border-box; - display: flex; - justify-content: space-between; - position: relative; - cursor: pointer; - color: var(--primary-high); - - @media (hover: none) { - &:hover, - &:focus { - background: transparent; - } - - &:active { - background: var(--primary-low); - } - } - - @media (hover: hover) { - &:hover, - &.active { - background: var(--primary-very-low); - } - - &.can-leave:hover { - .toggle-channel-membership-button.-leave { - display: block; - - > * { - pointer-events: auto; - } - } - - .chat-channel-metadata { - display: none; - } - } - } - - &:hover, - &.active { - .chat-channel-title { - &, - .category-chat-name, - .dm-usernames { - color: var(--primary); - } - - .d-icon-lock { - background-color: var(--primary-low); - } - } - } - - &:visited { - color: var(--primary-high); - } - - &.muted { - opacity: 0.65; - } - - .chat-channel-title { - &__users-count { - width: var(--channel-list-avatar-size); - height: var(--channel-list-avatar-size); - padding: 0; - font-size: var(--font-up-1); - justify-content: center; - } - - &__avatar { - .chat-user-avatar { - img { - width: calc(var(--channel-list-avatar-size) - 2px); - height: calc(var(--channel-list-avatar-size) - 2px); - } - } - } - &__user-info { - @include ellipsis; - } - &__usernames { - display: flex; - align-items: center; - justify-content: start; - } - .user-status-message { - display: inline-block; - font-size: var(--font-down-2); - margin-right: 0.5rem; - - &-description { - color: var(--primary-medium); - } - } - } - - .chat-channel-metadata { - display: flex; - align-items: flex-end; - flex-direction: column; - margin-left: 0.5em; - - &__date { - color: var(--primary-high); - font-size: var(--font-down-2); - white-space: nowrap; - } - - .chat-channel-unread-indicator { - @include chat-unread-indicator; - display: flex; - align-items: center; - justify-content: center; - width: 8px; - height: 8px; - - &.-urgent { - width: auto; - height: auto; - min-width: 0.6em; - padding: 0.3em 0.5em; - } - } - } - - &.unfollowing { - opacity: 0; - } - - .toggle-channel-membership-button.-leave { - display: none; - margin-left: auto; - } - .badge-wrapper { - align-items: center; - margin-right: 0; - } - - .emoji { - margin-left: 0.3em; - } - } } diff --git a/plugins/chat/assets/stylesheets/common/index.scss b/plugins/chat/assets/stylesheets/common/index.scss index 5fb2bca2b54..d9ab114867a 100644 --- a/plugins/chat/assets/stylesheets/common/index.scss +++ b/plugins/chat/assets/stylesheets/common/index.scss @@ -63,3 +63,4 @@ @import "chat-modal-channel-summary"; @import "chat-modal-move-message-to-channel"; @import "chat-scroll-to-bottom"; +@import "chat-channel-row"; diff --git a/plugins/chat/assets/stylesheets/mobile/chat-channel-row.scss b/plugins/chat/assets/stylesheets/mobile/chat-channel-row.scss new file mode 100644 index 00000000000..ba8c582816c --- /dev/null +++ b/plugins/chat/assets/stylesheets/mobile/chat-channel-row.scss @@ -0,0 +1,54 @@ +.chat-channel-row { + height: 4em; + margin: 0; + padding: 0 1.5rem; + border-radius: 0; + border-bottom: 1px solid var(--primary-low); + transition: height 0.25s ease-in-out, opacity 0.25s ease-out; + transform-origin: top center; + will-change: height, left; + + &__action-btn { + display: flex; + align-items: center; + position: absolute; + top: 0px; + bottom: 0px; + padding-inline: 1rem; + + &.-cancel { + background: var(--primary-very-low); + color: var(--primary); + } + + &.-leave { + background: var(--danger); + color: var(--primary-very-low); + } + } + + &__action-btn-icon { + margin-left: 0.5rem; + } + + &.-fade-out { + background-color: var(--danger-low); + height: 0 !important; + overflow: hidden; + opacity: 0.5 !important; + } + + &.-reset { + transition: left 0.25s ease-in-out; + } + + .chat-channel-metadata { + .chat-channel-unread-indicator { + font-size: var(--font-down-2); + margin-top: 0.25rem; + } + &__date { + font-size: var(--font-down-2); + } + } +} diff --git a/plugins/chat/assets/stylesheets/mobile/chat-index.scss b/plugins/chat/assets/stylesheets/mobile/chat-index.scss index c4f7f696131..47352eef3bd 100644 --- a/plugins/chat/assets/stylesheets/mobile/chat-index.scss +++ b/plugins/chat/assets/stylesheets/mobile/chat-index.scss @@ -10,24 +10,7 @@ .channels-list-container { background: var(--secondary); - } - - .chat-channel-row { - height: 4em; - margin: 0; - padding: 0 1.5rem; - border-radius: 0; - border-bottom: 1px solid var(--primary-low); - - .chat-channel-metadata { - .chat-channel-unread-indicator { - font-size: var(--font-down-2); - margin-top: 0.25rem; - } - &__date { - font-size: var(--font-down-2); - } - } + overflow: hidden; } .chat-channel-divider { diff --git a/plugins/chat/assets/stylesheets/mobile/index.scss b/plugins/chat/assets/stylesheets/mobile/index.scss index f9fdace3e91..833388c5a50 100644 --- a/plugins/chat/assets/stylesheets/mobile/index.scss +++ b/plugins/chat/assets/stylesheets/mobile/index.scss @@ -14,3 +14,4 @@ @import "chat-modal-thread-settings"; @import "chat-message-thread-indicator"; @import "chat-message-creator"; +@import "chat-channel-row"; diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml index 2bba4bb4ad2..4b36fb9d635 100644 --- a/plugins/chat/config/locales/client.en.yml +++ b/plugins/chat/config/locales/client.en.yml @@ -83,6 +83,7 @@ en: click_to_join: "Click here to view available channels." close: "Close" + remove: "Remove" collapse: "Collapse Chat Drawer" expand: "Expand Chat Drawer" confirm_flag: "Are you sure you want to flag %{username}'s message?" diff --git a/plugins/chat/spec/system/list_channels/mobile_spec.rb b/plugins/chat/spec/system/list_channels/mobile_spec.rb index 9c0efbb0e9c..38782a880a7 100644 --- a/plugins/chat/spec/system/list_channels/mobile_spec.rb +++ b/plugins/chat/spec/system/list_channels/mobile_spec.rb @@ -27,7 +27,10 @@ RSpec.describe "List channels | mobile", type: :system, mobile: true do context "when not member of the channel" do it "doesn’t show the channel" do visit("/chat") - expect(page.find(".public-channels")).to have_no_content(category_channel_1.name) + + expect(page.find(".public-channels", visible: :all)).to have_no_content( + category_channel_1.name, + ) end end end