UI: improves remove channel animation (#23585)

Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
This commit is contained in:
Joffrey JAFFEUX 2023-09-14 18:48:29 +02:00 committed by GitHub
parent 2427af4c46
commit ae27beb01a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 152 additions and 152 deletions

View File

@ -15,11 +15,10 @@ import I18n from "I18n";
import { modifier } from "ember-modifier"; import { modifier } from "ember-modifier";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import discourseLater from "discourse-common/lib/later";
import { cancel } from "@ember/runloop";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import icon from "discourse-common/helpers/d-icon";
import { htmlSafe } from "@ember/template";
const RESET_CLASS = "-reset";
const FADEOUT_CLASS = "-fade-out"; const FADEOUT_CLASS = "-fade-out";
export default class ChatChannelRow extends Component { export default class ChatChannelRow extends Component {
@ -40,94 +39,109 @@ export default class ChatChannelRow extends Component {
data-chat-channel-id={{@channel.id}} data-chat-channel-id={{@channel.id}}
{{didInsert this.startTrackingStatus}} {{didInsert this.startTrackingStatus}}
{{willDestroy this.stopTrackingStatus}} {{willDestroy this.stopTrackingStatus}}
{{(if this.shouldHandleSwipe (modifier this.registerSwipableRow))}}
{{(if this.shouldHandleSwipe (modifier this.handleSwipe))}}
{{(if this.shouldRemoveChannel (modifier this.onRemoveChannel))}} {{(if this.shouldRemoveChannel (modifier this.onRemoveChannel))}}
{{(if this.shouldResetRow (modifier this.onResetRow))}}
> >
<ChatChannelTitle @channel={{@channel}} /> <div
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} /> class={{concatClass
"chat-channel-row__content"
(if this.shouldReset "-animate-reset")
}}
{{(if this.shouldHandleSwipe (modifier this.registerSwipableRow))}}
{{(if this.shouldHandleSwipe (modifier this.handleSwipe))}}
{{(if this.shouldReset (modifier this.onReset))}}
style={{this.rowStyle}}
>
<ChatChannelTitle @channel={{@channel}} />
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} />
{{#if {{#if
(and @options.leaveButton @channel.isFollowing this.site.desktopView) (and @options.leaveButton @channel.isFollowing this.site.desktopView)
}} }}
<ToggleChannelMembershipButton <ToggleChannelMembershipButton
@channel={{@channel}} @channel={{@channel}}
@options={{hash @options={{hash
leaveClass="btn-flat chat-channel-leave-btn" leaveClass="btn-flat chat-channel-leave-btn"
labelType="none" labelType="none"
leaveIcon="times" leaveIcon="times"
leaveTitle=(if leaveTitle=(if
@channel.isDirectMessageChannel @channel.isDirectMessageChannel
this.leaveDirectMessageLabel this.leaveDirectMessageLabel
this.leaveChannelLabel this.leaveChannelLabel
) )
}} }}
/> />
{{/if}} {{/if}}
</div>
{{#if this.shouldHandleSwipe}} {{#if this.showRemoveButton}}
<div <div
class={{concatClass class={{concatClass
"chat-channel-row__action-btn" "chat-channel-row__action-btn"
(if this.isCancelling "-cancel" "-leave") (if this.isAtThreshold "-at-threshold" "-not-at-threshold")
}} }}
{{this.registerActionButton}}
{{this.positionActionButton}}
> >
{{#if this.isCancelling}} {{icon "times-circle"}}
{{this.cancelActionLabel}}
{{else}}
{{this.removeActionLabel}}
{{/if}}
</div> </div>
{{/if}} {{/if}}
</LinkTo> </LinkTo>
</template> </template>
@service router;
@service chat;
@service capabilities;
@service currentUser;
@service site;
@service api; @service api;
@service capabilities;
@service chat;
@service currentUser;
@service router;
@service site;
@tracked isAtThreshold = false;
@tracked shouldRemoveChannel = false; @tracked shouldRemoveChannel = false;
@tracked hasReachedThreshold = false; @tracked showRemoveButton = false;
@tracked isCancelling = false; @tracked swipableRow = null;
@tracked shouldResetRow = false; @tracked shouldReset = false;
@tracked actionButton; @tracked diff = 0;
@tracked swipableRow; @tracked rowStyle = null;
@tracked canSwipe = true;
positionActionButton = modifier((element) => {
element.style.left = "100%";
});
registerActionButton = modifier((element) => {
this.actionButton = element;
});
registerSwipableRow = modifier((element) => { registerSwipableRow = modifier((element) => {
this.swipableRow = element; this.swipableRow = element;
}); });
onRemoveChannel = modifier(() => { onReset = modifier((element) => {
this.swipableRow.classList.add(FADEOUT_CLASS); const handler = () => {
this.rowStyle = htmlSafe("margin-right: 0px;");
this.showRemoveButton = false;
this.shouldReset = false;
};
const handler = discourseLater(() => { element.addEventListener("transitionend", handler, { once: true });
this.chat.unfollowChannel(this.args.channel).catch(popupAjaxError);
}, 250);
return () => { return () => {
cancel(handler); element.removeEventListener("transitionend", handler);
this.rowStyle = htmlSafe("margin-right: 0px;");
this.showRemoveButton = false;
this.shouldReset = false;
}; };
}); });
onRemoveChannel = modifier((element) => {
element.addEventListener(
"transitionend",
() => {
this.chat.unfollowChannel(this.args.channel).catch(popupAjaxError);
},
{ once: true }
);
element.classList.add(FADEOUT_CLASS);
});
handleSwipe = modifier((element) => { handleSwipe = modifier((element) => {
element.addEventListener("touchstart", this.onSwipeStart, { element.addEventListener("touchstart", this.onSwipeStart, {
passive: true, passive: true,
}); });
element.addEventListener("touchmove", this.onSwipe); element.addEventListener("touchmove", this.onSwipe, {
passive: true,
});
element.addEventListener("touchend", this.onSwipeEnd); element.addEventListener("touchend", this.onSwipeEnd);
return () => { return () => {
@ -137,93 +151,49 @@ export default class ChatChannelRow extends Component {
}; };
}); });
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 @bind
onSwipeStart(event) { onSwipeStart(event) {
this.hasReachedThreshold = false; this._initialX = event.changedTouches[0].screenX;
this.isCancelling = false;
this._lastX = this.initialX = event.changedTouches[0].screenX;
} }
@bind @bind
onSwipe(event) { onSwipe(event) {
const touchX = event.changedTouches[0].screenX; this.showRemoveButton = true;
const diff = this.initialX - touchX; this.shouldReset = false;
this.isAtThreshold = false;
// we don't state to be too sensitive to the touch const threshold = window.innerWidth / 3;
if (Math.abs(this._lastX - touchX) > 5) { const touchX = event.changedTouches[0].screenX;
this._towardsThreshold = this._lastX >= touchX;
this._lastX = touchX; this.diff = this._initialX - touchX;
} this.isAtThreshold = this.diff >= threshold;
// ensures we will go back to the initial position when swiping very fast // ensures we will go back to the initial position when swiping very fast
if (diff < 10) { if (this.diff > 25) {
this.isCancelling = false; if (this.isAtThreshold) {
this.hasReachedThreshold = false; this.diff = threshold + (this.diff - threshold) * 0.1;
this.swipableRow.style.left = "0px"; }
return;
}
if (diff >= window.innerWidth / 3) { this.rowStyle = htmlSafe(`margin-right: ${this.diff}px;`);
this.isCancelling = false;
this.hasReachedThreshold = true;
return;
} else { } else {
this.isCancelling = !this._towardsThreshold; this.rowStyle = htmlSafe("margin-right: 0px;");
}
if (diff > 25) {
this.actionButton.style.width = diff + "px";
this.swipableRow.style.left = -(this.initialX - touchX) + "px";
} }
} }
@bind @bind
onSwipeEnd(event) { onSwipeEnd() {
this._lastX = null; if (this.isAtThreshold) {
const diff = this.initialX - event.changedTouches[0].screenX; this.rowStyle = htmlSafe("margin-right: 0px;");
if (diff >= window.innerWidth / 3) {
this.swipableRow.style.left = "0px";
this.shouldRemoveChannel = true; this.shouldRemoveChannel = true;
return; } else {
this.shouldReset = true;
} }
this.isCancelling = true;
this.shouldResetRow = true;
} }
get shouldHandleSwipe() { get shouldHandleSwipe() {
return this.capabilities.touch && this.args.channel.isDirectMessageChannel; return this.capabilities.touch && this.args.channel.isDirectMessageChannel;
} }
get cancelActionLabel() {
return I18n.t("cancel_value");
}
get removeActionLabel() {
return I18n.t("chat.remove");
}
get leaveDirectMessageLabel() { get leaveDirectMessageLabel() {
return I18n.t("chat.direct_messages.leave"); return I18n.t("chat.direct_messages.leave");
} }

View File

@ -7,6 +7,11 @@
cursor: pointer; cursor: pointer;
color: var(--primary-high); color: var(--primary-high);
&__content {
display: flex;
flex-grow: 1;
}
@media (hover: none) { @media (hover: none) {
&:hover, &:hover,
&:focus { &:focus {

View File

@ -1,45 +1,70 @@
.chat-channel-row { .chat-channel-row {
height: 4em;
margin: 0; margin: 0;
padding: 0 1.5rem;
border-radius: 0; border-radius: 0;
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);
transition: height 0.25s ease-in-out, opacity 0.25s ease-out; transition: height 0.25s ease-in-out, opacity 0.25s ease-out;
transform-origin: top center; transform-origin: top center;
will-change: height, left; will-change: height, opacity, left;
height: 4em;
&__action-btn { position: relative;
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 { &.-fade-out {
background-color: var(--danger-low); .chat-channel-row__content {
background-color: var(--danger-low);
}
height: 0 !important; height: 0 !important;
overflow: hidden; overflow: hidden;
opacity: 0.5 !important; opacity: 0.5 !important;
} }
&.-reset { &__content {
transition: left 0.25s ease-in-out; display: flex;
flex-grow: 1;
padding-inline: 1.5rem;
z-index: 2;
background: var(--primary-very-low);
box-sizing: border-box;
height: 100%;
transition: border-radius 0.25s ease-in-out;
&.-animate-reset {
transition: margin-right 0.15s ease-out;
margin-right: 0px !important;
}
}
&__action-btn {
z-index: 1;
display: flex;
align-items: center;
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
background: var(--danger);
color: var(--primary-very-low);
.d-icon {
transform-origin: 50% 50%;
transform-box: fill-box;
transition: scale 0.2s ease-out;
margin: 0 1rem 0 auto;
padding-left: 1rem;
}
&.-not-at-threshold {
.d-icon {
scale: 0.7;
}
}
&.-at-threshold {
.d-icon {
scale: 1.5;
}
}
} }
.chat-channel-metadata { .chat-channel-metadata {