UX: implements swipe on row channel

This commit is contained in:
Joffrey JAFFEUX 2023-09-06 15:58:23 +02:00
parent 19567daeb9
commit 63bdd93622
7 changed files with 386 additions and 193 deletions

View File

@ -0,0 +1,204 @@
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";
export default class ChatChannelRow extends Component {
<template>
<LinkTo
@route="chat.channel"
@models={{@channel.routeModels}}
class={{concatClass
"chat-channel-row"
(if @channel.focused "focused")
(if @channel.currentUserMembership.muted "muted")
(if @options.leaveButton "can-leave")
(if (eq this.chat.activeChannel.id @channel.id) "active")
(if this.channelHasUnread "has-unread")
}}
tabindex="0"
data-chat-channel-id={{@channel.id}}
{{didInsert this.startTrackingStatus}}
{{willDestroy this.stopTrackingStatus}}
{{this.handleSwipe}}
{{(if this.scheduleRowRemoval (modifier this.rowRemoval))}}
>
<ChatChannelTitle @channel={{@channel}} />
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} />
{{#if
(and @options.leaveButton @channel.isFollowing this.site.desktopView)
}}
<ToggleChannelMembershipButton
@channel={{@channel}}
@options={{hash
leaveClass="btn-flat chat-channel-leave-btn"
labelType="none"
leaveIcon="times"
leaveTitle=(if
@channel.isDirectMessageChannel
this.leaveDirectMessageLabel
this.leaveChanelLabel
)
}}
/>
{{/if}}
<div
class={{concatClass
"chat-channel-row__action-btn"
(if this.canCancelAction "-cancel" "-remove")
}}
{{this.removeButton}}
>
{{#if this.canCancelAction}}
{{this.cancelActionLabel}}
{{else}}
{{this.removeActionLabel}}
{{/if}}
</div>
</LinkTo>
</template>
@service router;
@service chat;
@service currentUser;
@service site;
@service api;
@tracked scheduleRowRemoval = false;
@tracked reachedThreshold = false;
@tracked canCancelAction = false;
removeButton = modifier((element) => {
this.removeButton = element;
this.removeButton.style.left = window.innerWidth + "px";
});
rowRemoval = modifier((element) => {
element.classList.add("-fade-out");
const handler = discourseLater(
() => this.chat.unfollowChannel(this.args.channel).catch(popupAjaxError),
250
);
return () => {
cancel(handler);
};
});
handleSwipe = modifier((element) => {
this.element = element;
element.addEventListener("touchstart", this.onSwipeStart);
element.addEventListener("touchmove", this.onSwipeMove);
element.addEventListener("touchend", this.onSwipeEnd);
return () => {
element.removeEventListener("touchstart", this.onSwipeStart);
element.removeEventListener("touchmove", this.onSwipeMove);
element.removeEventListener("touchend", this.onSwipeEnd);
};
});
@bind
onSwipeStart(event) {
if (!this.removeButton) {
return;
}
this.reachedThreshold = false;
this.canCancelAction = false;
this.initialX = event.changedTouches[0].screenX;
}
@bind
onSwipeMove(event) {
event.preventDefault();
const diff = this.initialX - event.changedTouches[0].screenX;
if (diff < 10) {
this.canCancelAction = false;
this.reachedThreshold = false;
this.element.style.left = "0px";
return;
}
if (diff >= window.innerWidth / 3) {
this.canCancelAction = false;
this.reachedThreshold = true;
return;
} else {
if (this.reachedThreshold) {
this.canCancelAction = true;
}
}
this.removeButton.style.width = diff + "px";
this.element.style.left =
-(this.initialX - event.changedTouches[0].screenX) + "px";
}
@bind
onSwipeEnd() {
const diff = this.initialX - event.changedTouches[0].screenX;
if (diff >= window.innerWidth / 3) {
this.scheduleRowRemoval = true;
}
this.element.style.left = "0px";
}
get cancelActionLabel() {
return "Cancel";
}
get removeActionLabel() {
return "Remove";
}
get leaveDirectMessageLabel() {
return I18n.t("chat.direct_messages.leave");
}
get leaveChanelLabel() {
return I18n.t("chat.channel_settings.leave_channel");
}
@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;
}
}

View File

@ -1,35 +0,0 @@
<LinkTo
@route="chat.channel"
@models={{@channel.routeModels}}
class={{concat-class
"chat-channel-row"
(if @channel.focused "focused")
(if @channel.currentUserMembership.muted "muted")
(if @options.leaveButton "can-leave")
(if (eq this.chat.activeChannel.id @channel.id) "active")
(if this.channelHasUnread "has-unread")
}}
tabindex="0"
data-chat-channel-id={{@channel.id}}
{{did-insert this.startTrackingStatus}}
{{will-destroy this.stopTrackingStatus}}
>
<ChatChannelTitle @channel={{@channel}} />
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} />
{{#if (and @options.leaveButton @channel.isFollowing this.site.desktopView)}}
<ToggleChannelMembershipButton
@channel={{@channel}}
@options={{hash
leaveClass="btn-flat chat-channel-leave-btn"
labelType="none"
leaveIcon="times"
leaveTitle=(if
@channel.isDirectMessageChannel
(i18n "chat.direct_messages.leave")
(i18n "chat.channel_settings.leave_channel")
)
}}
/>
{{/if}}
</LinkTo>

View File

@ -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;
}
}

View File

@ -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 {
<template>
{{! template-lint-disable no-invalid-interactive }}
{{#if this.shouldRender}}
{{#if (eq @context "channel")}}
<ChatMessageSeparatorDate
@fetchMessagesByDate={{@fetchMessagesByDate}}
@message={{@message}}
/>
<ChatMessageSeparatorNew @message={{@message}} />
{{/if}}
<div
class={{concatClass
"chat-message-container"
(if this.pane.selectingMessages "-selectable")
(if @message.highlighted "-highlighted")
(if (eq @message.user.id this.currentUser.id) "is-by-current-user")
(if @message.staged "-staged" "-persisted")
(if this.hasActiveState "-active")
(if @message.bookmark "-bookmarked")
(if @message.deletedAt "-deleted")
(if @message.selected "-selected")
(if @message.error "-errored")
(if this.showThreadIndicator "has-thread-indicator")
(if this.hideUserInfo "-user-info-hidden")
(if this.hasReply "has-reply")
}}
data-id={{@message.id}}
data-thread-id={{@message.thread.id}}
{{didInsert this.didInsertMessage}}
{{didUpdate this.didUpdateMessageId @message.id}}
{{didUpdate this.didUpdateMessageVersion @message.version}}
{{willDestroy this.willDestroyMessage}}
{{on "mouseenter" this.onMouseEnter passive=true}}
{{on "mouseleave" this.onMouseLeave passive=true}}
{{on "mousemove" this.onMouseMove passive=true}}
{{ChatOnLongPress
this.onLongPressStart
this.onLongPressEnd
this.onLongPressCancel
}}
...attributes
>
{{#if this.show}}
{{#if this.pane.selectingMessages}}
<Input
@type="checkbox"
class="chat-message-selector"
@checked={{@message.selected}}
{{on "click" this.toggleChecked}}
/>
{{/if}}
{{#if this.deletedAndCollapsed}}
<div class="chat-message-text -deleted">
<DButton
@action={{this.expand}}
@translatedLabel={{this.deletedMessageLabel}}
class="btn-flat chat-message-expand"
/>
</div>
{{else if this.hiddenAndCollapsed}}
<div class="chat-message-text -hidden">
<DButton
@action={{this.expand}}
@label="chat.hidden"
class="btn-flat chat-message-expand"
/>
</div>
{{else}}
<div class="chat-message">
{{#unless this.hideReplyToInfo}}
<ChatMessageInReplyToIndicator @message={{@message}} />
{{/unless}}
{{#if this.hideUserInfo}}
<ChatMessageLeftGutter @message={{@message}} />
{{else}}
<ChatMessageAvatar @message={{@message}} />
{{/if}}
<div class="chat-message-content">
<ChatMessageInfo
@message={{@message}}
@show={{not this.hideUserInfo}}
/>
<ChatMessageText
@cooked={{@message.cooked}}
@uploads={{@message.uploads}}
@edited={{@message.edited}}
>
{{#if @message.reactions.length}}
<div class="chat-message-reaction-list">
{{#each @message.reactions as |reaction|}}
<ChatMessageReaction
@reaction={{reaction}}
@onReaction={{this.messageInteractor.react}}
@message={{@message}}
@showTooltip={{true}}
/>
{{/each}}
{{#if this.shouldRenderOpenEmojiPickerButton}}
<DButton
@action={{this.messageInteractor.openEmojiPicker}}
@icon="discourse-emojis"
@title="chat.react"
@forwardEvent={{true}}
class="chat-message-react-btn"
/>
{{/if}}
</div>
{{/if}}
</ChatMessageText>
<ChatMessageError
@message={{@message}}
@onRetry={{@resendStagedMessage}}
/>
</div>
{{#if this.showThreadIndicator}}
<ChatMessageThreadIndicator @message={{@message}} />
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
{{/if}}
</template>
@service site;
@service dialog;
@service currentUser;

View File

@ -1,130 +0,0 @@
{{! template-lint-disable no-invalid-interactive }}
{{#if this.shouldRender}}
{{#if (eq @context "channel")}}
<ChatMessageSeparatorDate
@fetchMessagesByDate={{@fetchMessagesByDate}}
@message={{@message}}
/>
<ChatMessageSeparatorNew @message={{@message}} />
{{/if}}
<div
class={{concat-class
"chat-message-container"
(if this.pane.selectingMessages "-selectable")
(if @message.highlighted "-highlighted")
(if (eq @message.user.id this.currentUser.id) "is-by-current-user")
(if @message.staged "-staged" "-persisted")
(if this.hasActiveState "-active")
(if @message.bookmark "-bookmarked")
(if @message.deletedAt "-deleted")
(if @message.selected "-selected")
(if @message.error "-errored")
(if this.showThreadIndicator "has-thread-indicator")
(if this.hideUserInfo "-user-info-hidden")
(if this.hasReply "has-reply")
}}
data-id={{@message.id}}
data-thread-id={{@message.thread.id}}
{{did-insert this.didInsertMessage}}
{{did-update this.didUpdateMessageId @message.id}}
{{did-update this.didUpdateMessageVersion @message.version}}
{{will-destroy this.willDestroyMessage}}
{{on "mouseenter" this.onMouseEnter passive=true}}
{{on "mouseleave" this.onMouseLeave passive=true}}
{{on "mousemove" this.onMouseMove passive=true}}
{{chat/on-long-press
this.onLongPressStart
this.onLongPressEnd
this.onLongPressCancel
}}
...attributes
>
{{#if this.show}}
{{#if this.pane.selectingMessages}}
<Input
@type="checkbox"
class="chat-message-selector"
@checked={{@message.selected}}
{{on "click" this.toggleChecked}}
/>
{{/if}}
{{#if this.deletedAndCollapsed}}
<div class="chat-message-text -deleted">
<DButton
@action={{this.expand}}
@translatedLabel={{this.deletedMessageLabel}}
class="btn-flat chat-message-expand"
/>
</div>
{{else if this.hiddenAndCollapsed}}
<div class="chat-message-text -hidden">
<DButton
@action={{this.expand}}
@label="chat.hidden"
class="btn-flat chat-message-expand"
/>
</div>
{{else}}
<div class="chat-message">
{{#unless this.hideReplyToInfo}}
<ChatMessageInReplyToIndicator @message={{@message}} />
{{/unless}}
{{#if this.hideUserInfo}}
<Chat::Message::LeftGutter @message={{@message}} />
{{else}}
<Chat::Message::Avatar @message={{@message}} />
{{/if}}
<div class="chat-message-content">
<Chat::Message::Info
@message={{@message}}
@show={{not this.hideUserInfo}}
/>
<ChatMessageText
@cooked={{@message.cooked}}
@uploads={{@message.uploads}}
@edited={{@message.edited}}
>
{{#if @message.reactions.length}}
<div class="chat-message-reaction-list">
{{#each @message.reactions as |reaction|}}
<ChatMessageReaction
@reaction={{reaction}}
@onReaction={{this.messageInteractor.react}}
@message={{@message}}
@showTooltip={{true}}
/>
{{/each}}
{{#if this.shouldRenderOpenEmojiPickerButton}}
<DButton
@action={{this.messageInteractor.openEmojiPicker}}
@icon="discourse-emojis"
@title="chat.react"
@forwardEvent={{true}}
class="chat-message-react-btn"
/>
{{/if}}
</div>
{{/if}}
</ChatMessageText>
<Chat::Message::Error
@message={{@message}}
@onRetry={{@resendStagedMessage}}
/>
</div>
{{#if this.showThreadIndicator}}
<ChatMessageThreadIndicator @message={{@message}} />
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
{{/if}}

View File

@ -97,6 +97,34 @@
position: relative;
cursor: pointer;
color: var(--primary-high);
transition: height 0.25s ease-out, opacity 0.25s ease-out;
transform-origin: top center;
opacity: 1;
will-change: height, opacity, left;
&__action-btn {
display: flex;
align-items: center;
position: absolute;
top: 0px;
bottom: 0px;
padding-inline: 0.5rem;
color: var(--primary);
&.-cancel {
background: var(--tertiary);
}
&.-remove {
background: var(--danger);
}
}
&.-fade-out {
opacity: 0;
height: 0 !important;
overflow: hidden;
}
@media (hover: none) {
&:hover,

View File

@ -10,6 +10,7 @@
.channels-list-container {
background: var(--secondary);
overflow: hidden;
}
.chat-channel-row {