mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 18:30:26 -06:00
UI: improves remove channel animation (#23585)
Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
This commit is contained in:
parent
2427af4c46
commit
ae27beb01a
@ -15,11 +15,10 @@ 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";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
const RESET_CLASS = "-reset";
|
||||
const FADEOUT_CLASS = "-fade-out";
|
||||
|
||||
export default class ChatChannelRow extends Component {
|
||||
@ -40,94 +39,109 @@ export default class ChatChannelRow extends Component {
|
||||
data-chat-channel-id={{@channel.id}}
|
||||
{{didInsert this.startTrackingStatus}}
|
||||
{{willDestroy this.stopTrackingStatus}}
|
||||
{{(if this.shouldHandleSwipe (modifier this.registerSwipableRow))}}
|
||||
{{(if this.shouldHandleSwipe (modifier this.handleSwipe))}}
|
||||
{{(if this.shouldRemoveChannel (modifier this.onRemoveChannel))}}
|
||||
{{(if this.shouldResetRow (modifier this.onResetRow))}}
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} />
|
||||
<div
|
||||
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
|
||||
(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.leaveChannelLabel
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#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.leaveChannelLabel
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.shouldHandleSwipe}}
|
||||
{{#if this.showRemoveButton}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"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}}
|
||||
{{this.cancelActionLabel}}
|
||||
{{else}}
|
||||
{{this.removeActionLabel}}
|
||||
{{/if}}
|
||||
{{icon "times-circle"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</template>
|
||||
|
||||
@service router;
|
||||
@service chat;
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
@service site;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
@service chat;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service site;
|
||||
|
||||
@tracked isAtThreshold = false;
|
||||
@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;
|
||||
});
|
||||
@tracked showRemoveButton = false;
|
||||
@tracked swipableRow = null;
|
||||
@tracked shouldReset = false;
|
||||
@tracked diff = 0;
|
||||
@tracked rowStyle = null;
|
||||
@tracked canSwipe = true;
|
||||
|
||||
registerSwipableRow = modifier((element) => {
|
||||
this.swipableRow = element;
|
||||
});
|
||||
|
||||
onRemoveChannel = modifier(() => {
|
||||
this.swipableRow.classList.add(FADEOUT_CLASS);
|
||||
onReset = modifier((element) => {
|
||||
const handler = () => {
|
||||
this.rowStyle = htmlSafe("margin-right: 0px;");
|
||||
this.showRemoveButton = false;
|
||||
this.shouldReset = false;
|
||||
};
|
||||
|
||||
const handler = discourseLater(() => {
|
||||
this.chat.unfollowChannel(this.args.channel).catch(popupAjaxError);
|
||||
}, 250);
|
||||
element.addEventListener("transitionend", handler, { once: true });
|
||||
|
||||
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) => {
|
||||
element.addEventListener("touchstart", this.onSwipeStart, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener("touchmove", this.onSwipe);
|
||||
element.addEventListener("touchmove", this.onSwipe, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener("touchend", this.onSwipeEnd);
|
||||
|
||||
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
|
||||
onSwipeStart(event) {
|
||||
this.hasReachedThreshold = false;
|
||||
this.isCancelling = false;
|
||||
this._lastX = this.initialX = event.changedTouches[0].screenX;
|
||||
this._initialX = event.changedTouches[0].screenX;
|
||||
}
|
||||
|
||||
@bind
|
||||
onSwipe(event) {
|
||||
const touchX = event.changedTouches[0].screenX;
|
||||
const diff = this.initialX - touchX;
|
||||
this.showRemoveButton = true;
|
||||
this.shouldReset = false;
|
||||
this.isAtThreshold = false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
const threshold = window.innerWidth / 3;
|
||||
const touchX = event.changedTouches[0].screenX;
|
||||
|
||||
this.diff = this._initialX - touchX;
|
||||
this.isAtThreshold = this.diff >= threshold;
|
||||
|
||||
// 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 (this.diff > 25) {
|
||||
if (this.isAtThreshold) {
|
||||
this.diff = threshold + (this.diff - threshold) * 0.1;
|
||||
}
|
||||
|
||||
if (diff >= window.innerWidth / 3) {
|
||||
this.isCancelling = false;
|
||||
this.hasReachedThreshold = true;
|
||||
return;
|
||||
this.rowStyle = htmlSafe(`margin-right: ${this.diff}px;`);
|
||||
} else {
|
||||
this.isCancelling = !this._towardsThreshold;
|
||||
}
|
||||
|
||||
if (diff > 25) {
|
||||
this.actionButton.style.width = diff + "px";
|
||||
this.swipableRow.style.left = -(this.initialX - touchX) + "px";
|
||||
this.rowStyle = htmlSafe("margin-right: 0px;");
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onSwipeEnd(event) {
|
||||
this._lastX = null;
|
||||
const diff = this.initialX - event.changedTouches[0].screenX;
|
||||
|
||||
if (diff >= window.innerWidth / 3) {
|
||||
this.swipableRow.style.left = "0px";
|
||||
onSwipeEnd() {
|
||||
if (this.isAtThreshold) {
|
||||
this.rowStyle = htmlSafe("margin-right: 0px;");
|
||||
this.shouldRemoveChannel = true;
|
||||
return;
|
||||
} else {
|
||||
this.shouldReset = true;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
@ -7,6 +7,11 @@
|
||||
cursor: pointer;
|
||||
color: var(--primary-high);
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
@ -1,45 +1,70 @@
|
||||
.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;
|
||||
}
|
||||
will-change: height, opacity, left;
|
||||
height: 4em;
|
||||
position: relative;
|
||||
|
||||
&.-fade-out {
|
||||
background-color: var(--danger-low);
|
||||
.chat-channel-row__content {
|
||||
background-color: var(--danger-low);
|
||||
}
|
||||
|
||||
height: 0 !important;
|
||||
overflow: hidden;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
&.-reset {
|
||||
transition: left 0.25s ease-in-out;
|
||||
&__content {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user