Add story gestures

Add swipe up and down gestures
This commit is contained in:
Alexander Zinchuk 2023-10-27 12:50:13 +02:00
parent eddbd476b6
commit 9caa8a560b
15 changed files with 263 additions and 97 deletions

View File

@ -8,7 +8,12 @@ import type { RealTouchEvent } from '../../util/captureEvents';
import { animateNumber, timingFunctions } from '../../util/animation';
import buildClassName from '../../util/buildClassName';
import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD } from '../../util/captureEvents';
import {
captureEvents,
IOS_SCREEN_EDGE_THRESHOLD,
SWIPE_DIRECTION_THRESHOLD,
SWIPE_DIRECTION_TOLERANCE,
} from '../../util/captureEvents';
import { clamp, isBetween, round } from '../../util/math';
import { debounce } from '../../util/schedulers';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment';
@ -459,8 +464,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
const absOffsetX = Math.abs(dragOffsetX);
const absOffsetY = Math.abs(dragOffsetY);
const { x, y, scale } = transformRef.current;
const threshold = 10;
const tolerance = 1.5;
// If user is inactive but is still touching the screen
// we reset last gesture time
@ -490,7 +493,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
// If user is swiping horizontally or horizontal shift is dominant
// we change only horizontal position
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|| Math.abs(x) > threshold || absOffsetX / absOffsetY > tolerance) {
|| Math.abs(x) > SWIPE_DIRECTION_THRESHOLD || absOffsetX / absOffsetY > SWIPE_DIRECTION_TOLERANCE) {
swipeDirectionRef.current = SwipeDirection.Horizontal;
setIsActive(false);
const limit = windowWidth + SLIDES_GAP;
@ -512,7 +515,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
}
// If vertical shift is dominant we change only vertical position
if (swipeDirectionRef.current === SwipeDirection.Vertical
|| Math.abs(y) > threshold || absOffsetY / absOffsetX > tolerance) {
|| Math.abs(y) > SWIPE_DIRECTION_THRESHOLD || absOffsetY / absOffsetX > SWIPE_DIRECTION_TOLERANCE) {
swipeDirectionRef.current = SwipeDirection.Vertical;
const limit = windowHeight;
const y1 = clamp(dragOffsetY, -limit, limit);

View File

@ -5,7 +5,7 @@
position: relative;
}
.close {
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;

View File

@ -76,7 +76,7 @@ const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium } : StateP
round
color="translucent"
size="smaller"
className={styles.close}
className={styles.closeButton}
ariaLabel={lang('Close')}
onClick={handleClose}
>

View File

@ -497,11 +497,11 @@ function Story({
ripple={!isMobile}
size="tiny"
color="translucent-white"
className={isOpen ? 'active' : ''}
onClick={onTrigger}
className={buildClassName(styles.button, isOpen && 'active')}
ariaLabel={lang('AccDescrOpenMenu2')}
>
<i className={buildClassName('icon icon-more', styles.topIcon)} aria-hidden />
<i className={buildClassName('icon icon-more')} aria-hidden />
</Button>
);
};
@ -604,7 +604,7 @@ function Story({
{renderStoryPrivacyButton()}
{isVideo && (
<Button
className={buildClassName(styles.button, styles.buttonVolume)}
className={styles.button}
round
ripple={!isMobile}
size="tiny"
@ -617,14 +617,13 @@ function Story({
className={buildClassName(
'icon',
isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story',
styles.topIcon,
)}
aria-hidden
/>
</Button>
)}
<DropdownMenu
className={buildClassName(styles.button, styles.buttonMenu)}
className={styles.buttonMenu}
trigger={MenuButton}
positionX="right"
onOpen={handleDropdownMenuOpen}
@ -646,6 +645,16 @@ function Story({
{!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
{isOut && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
</DropdownMenu>
<Button
className={buildClassName(styles.button, styles.closeButton)}
round
size="tiny"
color="translucent-white"
ariaLabel={lang('Close')}
onClick={onClose}
>
<i className={buildClassName('icon icon-close')} aria-hidden />
</Button>
</div>
</div>
);
@ -727,7 +736,7 @@ function Story({
</>
)}
{isLoadedStory && fullMediaData && (
<MediaAreaOverlay story={story} className={styles.mediaAreaOverlay} isActive />
<MediaAreaOverlay story={story} isActive />
)}
</div>

View File

@ -4,20 +4,35 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
import type { RealTouchEvent } from '../../util/captureEvents';
import { ANIMATION_END_DELAY } from '../../config';
import { ANIMATION_END_DELAY, EDITABLE_STORY_INPUT_ID } from '../../config';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { getStoryKey } from '../../global/helpers';
import { selectIsStoryViewerOpen, selectPeer, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment';
import {
captureEvents,
IOS_SCREEN_EDGE_THRESHOLD,
SWIPE_DIRECTION_THRESHOLD,
SWIPE_DIRECTION_TOLERANCE,
} from '../../util/captureEvents';
import focusEditableElement from '../../util/focusEditableElement';
import { clamp } from '../../util/math';
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
import {
IS_FIREFOX, IS_IOS, IS_SAFARI,
} from '../../util/windowEnvironment';
import { calculateOffsetX } from './helpers/dimensions';
import useAppLayout from '../../hooks/useAppLayout';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useHistoryBack from '../../hooks/useHistoryBack';
import useLastCallback from '../../hooks/useLastCallback';
import usePrevious from '../../hooks/usePrevious';
import useSignal from '../../hooks/useSignal';
import useWindowSize from '../../hooks/useWindowSize';
import useSlideSizes from './hooks/useSlideSizes';
import Story from './Story';
@ -47,6 +62,13 @@ interface StateProps {
const ANIMATION_DURATION_MS = 350 + (IS_SAFARI || IS_FIREFOX ? ANIMATION_END_DELAY : 20);
const ACTIVE_SLIDE_VERTICAL_CORRECTION_REM = 1.75;
const SWIPE_Y_THRESHOLD = 50;
const SCROLL_RELEASE_DELAY = 1500;
enum SwipeDirection {
Horizontal,
Vertical,
}
function StorySlides({
peerIds,
@ -65,6 +87,8 @@ function StorySlides({
onReport,
}: OwnProps & StateProps) {
const { stopActiveReaction } = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const [renderingPeerId, setRenderingPeerId] = useState(currentPeerId);
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
const prevPeerId = usePrevious(currentPeerId);
@ -73,6 +97,11 @@ function StorySlides({
const renderingIsSinglePeer = useCurrentOrPrev(isSinglePeer, true);
const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true);
const slideSizes = useSlideSizes();
const { height: windowHeight, width: windowWidth } = useWindowSize();
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
const isReleasedRef = useRef(false);
const { isMobile } = useAppLayout();
const animationDuration = isMobile ? 0 : ANIMATION_DURATION_MS;
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
const [getIsAnimating, setIsAnimating] = useSignal(false);
@ -128,12 +157,12 @@ function StorySlides({
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setRenderingPeerId(currentPeerId);
}, ANIMATION_DURATION_MS);
}, animationDuration);
return () => {
window.clearTimeout(timeoutId);
};
}, [currentPeerId]);
}, [animationDuration, currentPeerId]);
useEffect(() => {
let timeOutId: number | undefined;
@ -141,7 +170,7 @@ function StorySlides({
if (renderingPeerId !== currentPeerId) {
timeOutId = window.setTimeout(() => {
setRenderingStoryId(currentStoryId);
}, ANIMATION_DURATION_MS);
}, animationDuration);
} else if (currentStoryId !== renderingStoryId) {
setRenderingStoryId(currentStoryId);
}
@ -149,7 +178,7 @@ function StorySlides({
return () => {
window.clearTimeout(timeOutId);
};
}, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId]);
}, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId, animationDuration]);
useEffect(() => {
let timeOutId: number | undefined;
@ -158,14 +187,14 @@ function StorySlides({
setIsAnimating(true);
timeOutId = window.setTimeout(() => {
setIsAnimating(false);
}, ANIMATION_DURATION_MS);
}, animationDuration);
}
return () => {
setIsAnimating(false);
window.clearTimeout(timeOutId);
};
}, [prevPeerId, currentPeerId, setIsAnimating]);
}, [prevPeerId, currentPeerId, setIsAnimating, animationDuration]);
useEffect(() => {
return () => {
@ -210,6 +239,91 @@ function StorySlides({
}, {});
});
useEffect(() => {
if (!containerRef.current || !isOpen) {
return undefined;
}
let offsetY = 0;
const getCurrentStoryRef = () => {
return renderingPeerId ? rendersRef.current[renderingPeerId]?.current : undefined;
};
const onRelease = (event: MouseEvent | TouchEvent | WheelEvent) => {
// This allows to prevent onRelease triggered by debounced wheel event
// after onRelease was triggered manually in onDrag
if (isReleasedRef.current) {
isReleasedRef.current = false;
return;
}
const current = getCurrentStoryRef();
if (!current) return;
if (offsetY < -SWIPE_Y_THRESHOLD) {
const composer = document.getElementById(EDITABLE_STORY_INPUT_ID);
if (composer) {
requestMutation(() => {
focusEditableElement(composer);
});
}
return;
}
if (offsetY > SWIPE_Y_THRESHOLD) {
onClose();
if (event.type === 'wheel') {
disableScrolling();
setTimeout(enableScrolling, SCROLL_RELEASE_DELAY);
}
} else {
requestMutation(() => {
current.style.setProperty('--slide-translate-y', '0px');
});
}
};
return captureEvents(containerRef.current, {
isNotPassive: true,
withNativeDrag: true,
excludedClosestSelector: '.Composer',
onDrag: (event, captureEvent, {
dragOffsetX, dragOffsetY,
}) => {
if (isReleasedRef.current) return;
// Avoid conflicts with swipe-to-back gestures
if (IS_IOS && captureEvent.type === 'touchstart') {
const { pageX } = (captureEvent as RealTouchEvent).touches[0];
if (pageX <= IOS_SCREEN_EDGE_THRESHOLD || pageX >= windowWidth - IOS_SCREEN_EDGE_THRESHOLD) {
return;
}
}
if (event.type === 'mousemove') return;
const absOffsetX = Math.abs(dragOffsetX);
const absOffsetY = Math.abs(dragOffsetY);
const current = getCurrentStoryRef();
if (!current) return;
// If vertical shift is dominant we change only vertical position
if (swipeDirectionRef.current === SwipeDirection.Vertical
|| Math.abs(absOffsetY) > SWIPE_DIRECTION_THRESHOLD || absOffsetY / absOffsetX > SWIPE_DIRECTION_TOLERANCE) {
swipeDirectionRef.current = SwipeDirection.Vertical;
const limit = windowHeight;
offsetY = clamp(dragOffsetY, -limit, limit);
if (offsetY > 0) {
requestMutation(() => {
current.style.setProperty('--slide-translate-y', `${-offsetY}px`);
});
}
if (event.type === 'wheel' && Math.abs(offsetY) > SWIPE_Y_THRESHOLD * 2) {
onRelease(event);
isReleasedRef.current = true;
}
}
},
onRelease,
});
}, [isOpen, renderingPeerId, onClose, windowWidth, windowHeight]);
useLayoutEffect(() => {
const transformX = calculateTransformX();
@ -225,25 +339,33 @@ function StorySlides({
return;
}
const scale = String(currentPeerId === peerId ? slideSizes.toActiveScale
: peerId === renderingPeerId ? slideSizes.fromActiveScale : 1);
const getScale = () => {
if (isMobile) return '1';
if (currentPeerId === peerId) {
return String(slideSizes.toActiveScale);
}
if (peerId === renderingPeerId) {
return String(slideSizes.fromActiveScale);
}
return '1';
};
let offsetY = 0;
if (peerId === renderingPeerId) {
offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * slideSizes.fromActiveScale;
if (!isMobile) offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * slideSizes.fromActiveScale;
current.classList.add(styles.slideAnimationFromActive);
}
if (peerId === currentPeerId) {
offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM;
if (!isMobile) offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM;
current.classList.add(styles.slideAnimationToActive);
}
current.classList.add(styles.slideAnimation);
current.style.setProperty('--slide-translate-x', `${transformX[peerId] || 0}px`);
current.style.setProperty('--slide-translate-y', `${offsetY}rem`);
current.style.setProperty('--slide-translate-scale', scale);
current.style.setProperty('--slide-translate-scale', getScale());
});
}, [currentPeerId, getIsAnimating, renderingPeerId, slideSizes]);
}, [currentPeerId, getIsAnimating, renderingPeerId, isMobile, slideSizes]);
function renderStoryPreview(peerId: string, index: number, position: number) {
const style = buildStyle(
@ -303,7 +425,11 @@ function StorySlides({
}
return (
<div className={styles.wrapper} style={`--story-viewer-scale: ${slideSizes.scale}`}>
<div
className={styles.wrapper}
ref={containerRef}
style={`--story-viewer-scale: ${slideSizes.scale}`}
>
<div className={styles.fullSize} onClick={onClose} />
{renderingPeerIds.length > 1 && (
<div className={styles.backdropNonInteractive} style={`height: ${slideSizes.slide.height}px`} />

View File

@ -98,7 +98,7 @@
border-top: 0.0625rem solid var(--color-borders);
}
.close {
.closeButton {
margin-block: 0.25rem;
}

View File

@ -67,9 +67,8 @@
right: 1rem;
top: 1rem;
z-index: 3;
@media (max-width: 600px) {
top: 1.25rem;
display: none;
}
}
@ -94,35 +93,31 @@
}
.slideAnimationToActive {
@media (min-width: 600.001px) {
--border-radius-default-small: 0.25rem;
--border-radius-default-small: 0.25rem;
&::before {
pointer-events: none;
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
height: 4.5rem;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
z-index: 1;
}
&::before {
pointer-events: none;
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
height: 4.5rem;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
z-index: 1;
}
.content {
opacity: 0;
}
.content {
opacity: 0;
}
}
.slideAnimationFromActive {
@media (min-width: 600.001px) {
.composer,
.caption,
.storyIndicators {
transition: opacity 250ms ease-in-out;
opacity: 0;
}
.composer,
.caption,
.storyIndicators {
transition: opacity 250ms ease-in-out;
opacity: 0;
}
}
@ -196,7 +191,7 @@
top: 0;
width: 100% !important;
height: 100%;
transform: none;
transform: translate3d(0, calc(var(--slide-translate-y, 0px) * -1), 0);
&::before {
display: none;
@ -243,9 +238,9 @@
overflow: hidden;
@media (max-width: 600px) {
width: 100% !important;
height: calc(100% - 4rem) !important; // Update `MOBILE_MEDIA_BOTTOM_MARGIN` in MediaAreaOverlay on change
border-radius: 0;
width: calc(100% - 1rem) !important; // Update `MOBILE_MEDIA_OFFSET_X` in MediaAreaOverlay on change
height: calc(100% - 4.5rem) !important; // Update `MOBILE_MEDIA_OFFSET_Y` in MediaAreaOverlay on change
margin: 0.5rem;
}
:global(body.ghost-animating) & {
@ -267,8 +262,6 @@
bottom: 0;
width: 100%;
height: 100%;
border-radius: 0;
object-fit: contain;
}
:global(body.ghost-animating) .activeSlide & {
@ -276,18 +269,6 @@
}
}
.mediaAreaOverlay {
@media (max-width: 600px) {
right: auto;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 100%;
}
}
.content {
position: absolute;
top: 50%;
@ -348,6 +329,13 @@
.storyHeader {
@include story-header;
@media (max-width: 600px) {
width: auto;
left: 0.5rem;
top: 0.5rem;
right: 0.5rem;
}
:global(body.ghost-animating) & {
background: none;
}
@ -425,8 +413,12 @@
}
}
.topIcon {
color: var(--color-white);
.closeButton {
display: none;
@media (max-width: 600px) {
display: flex;
}
}
.actions {
@ -434,12 +426,21 @@
display: flex;
align-items: center;
@media (max-width: 600px) {
position: relative;
right: 3.25rem;
:global(.Button) {
color: white;
@media (max-width: 600px) {
display: flex;
&:active,
&.active,
&:hover,
&:focus {
background: none !important;
}
}
}
}
.visibilityButton {
min-width: 1.5rem;
height: 1.5rem;
@ -451,6 +452,7 @@
color: #fff;
font-size: 0.75rem;
cursor: var(--custom-cursor, pointer);
margin: 0 0.375rem;
> :global(.icon + .icon) {
margin-left: 0.125rem;
@ -490,7 +492,9 @@
left: 0;
right: 0;
height: 7rem;
overflow: hidden;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
border-radius: 0 0 var(--border-radius-default-small) var(--border-radius-default-small);
pointer-events: none;
}
@ -590,6 +594,7 @@
left: 0;
margin-bottom: 0;
z-index: 3;
transition: transform var(--layer-transition);
&:global(.Composer) {
--base-height: 3rem;
@ -633,6 +638,10 @@
max-height: 8rem;
}
:global(.is-symbol-menu-open) & {
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height))), 0);
}
}
.navigate {
@ -673,6 +682,10 @@
height: 100%;
display: block;
object-fit: cover;
@media (max-width: 600px) {
border-radius: var(--border-radius-default-small);
}
}
.ghost {
@ -702,7 +715,4 @@
user-select: none;
-webkit-user-select: none;
object-fit: cover;
@media (max-width: 600px) {
object-fit: contain;
}
}

View File

@ -155,7 +155,7 @@ function StoryViewer({
ariaLabel={lang('Close')}
onClick={handleClose}
>
<i className={buildClassName('icon icon-close', styles.topIcon)} aria-hidden />
<i className={buildClassName('icon icon-close')} aria-hidden />
</Button>
<StorySlides

View File

@ -15,8 +15,8 @@ import storyRibbonStyles from '../StoryRibbon.module.scss';
import styles from '../StoryViewer.module.scss';
const ANIMATION_DURATION = 200;
const OFFSET_DESKTOP = 3.5 * REM;
const OFFSET_MOBILE = 4 * REM;
const OFFSET_BOTTOM = 3.5 * REM;
const MOBILE_OFFSET = 0.5 * REM;
const MOBILE_WIDTH = 600;
export function animateOpening(
@ -36,12 +36,12 @@ export function animateOpening(
const isMobile = windowWidth <= MOBILE_WIDTH;
if (isMobile) {
toWidth = windowWidth;
toHeight = windowHeight - OFFSET_MOBILE;
toWidth = windowWidth - 2 * MOBILE_OFFSET;
toHeight = windowHeight - OFFSET_BOTTOM - 2 * MOBILE_OFFSET;
}
const toLeft = isMobile ? 0 : (windowWidth - toWidth) / 2;
const toTop = isMobile ? 0 : (windowHeight - (toHeight + OFFSET_DESKTOP)) / 2;
const toLeft = isMobile ? MOBILE_OFFSET : (windowWidth - toWidth) / 2;
const toTop = isMobile ? MOBILE_OFFSET : (windowHeight - (toHeight + OFFSET_BOTTOM)) / 2;
const {
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,

View File

@ -332,11 +332,11 @@ function createGhost(sourceEl: HTMLElement) {
}
function getPeerId(el: HTMLElement) {
return el.getAttribute('data-peer-id');
return el?.getAttribute('data-peer-id');
}
function selectByPeerId(el: HTMLElement, id: string) {
return el.querySelector<HTMLElement>(`[data-peer-id="${id}"]`);
return el?.querySelector<HTMLElement>(`[data-peer-id="${id}"]`);
}
function createDelayedCallback(callback: NoneToVoidFunction, ms: number) {

View File

@ -5,8 +5,16 @@
right: 0;
bottom: 0;
width: var(--media-width, auto);
height: var(--media-height, auto);
aspect-ratio: 9 / 16;
pointer-events: none;
@media (max-width: 600px) {
right: auto;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.mediaArea {
@ -22,6 +30,7 @@
.shiny {
overflow: hidden;
border-radius: var(--border-radius-default-small);
}
.shiny::before {

View File

@ -22,7 +22,8 @@ type OwnProps = {
};
const STORY_ASPECT_RATIO = 9 / 16;
const MOBILE_MEDIA_BOTTOM_MARGIN = 4 * REM;
const MOBILE_MEDIA_OFFSET_X = Number(REM);
const MOBILE_MEDIA_OFFSET_Y = 4.5 * REM;
const MediaAreaOverlay = ({
story, isActive, className,
@ -41,17 +42,22 @@ const MediaAreaOverlay = ({
if (windowSize.width > MOBILE_SCREEN_MAX_WIDTH) {
requestMutation(() => {
element.style.removeProperty('--media-width');
element.style.removeProperty('--media-height');
});
return;
}
const adaptedHeight = windowSize.height - MOBILE_MEDIA_BOTTOM_MARGIN;
const adaptedHeight = windowSize.height - MOBILE_MEDIA_OFFSET_Y;
const adoptedWidth = windowSize.width - MOBILE_MEDIA_OFFSET_X;
const screenAspectRatio = windowSize.width / adaptedHeight;
const screenAspectRatio = windowSize.width / windowSize.height;
const width = screenAspectRatio < STORY_ASPECT_RATIO ? adaptedHeight * STORY_ASPECT_RATIO : adoptedWidth;
const height = screenAspectRatio < STORY_ASPECT_RATIO ? adaptedHeight : adoptedWidth / STORY_ASPECT_RATIO;
const width = screenAspectRatio > STORY_ASPECT_RATIO ? adaptedHeight * STORY_ASPECT_RATIO : windowSize.width;
requestMutation(() => {
element.style.setProperty('--media-width', `${width}px`);
element.style.setProperty('--media-height', `${height}px`);
});
}, [isActive, windowSize]);

View File

@ -273,7 +273,7 @@ $color-message-story-mention-to: #74bcff;
--z-sticky-date: 9;
--z-register-add-avatar: 5;
--z-media-viewer-head: 3;
--z-symbol-menu-mobile: 2;
--z-symbol-menu-mobile: calc(var(--z-story-viewer) + 1);
--z-resize-handle: 2;
--z-below: -1;

View File

@ -1,7 +1,7 @@
import { Lethargy } from './lethargy';
import { clamp, round } from './math';
import { debounce } from './schedulers';
import { IS_IOS } from './windowEnvironment';
import { IS_IOS, IS_WINDOWS } from './windowEnvironment';
import windowSize from './windowSize';
export enum SwipeDirection {
@ -68,6 +68,9 @@ type TSwipeAxis =
| undefined;
export const IOS_SCREEN_EDGE_THRESHOLD = 20;
export const SWIPE_DIRECTION_THRESHOLD = 10;
export const SWIPE_DIRECTION_TOLERANCE = 1.5;
const MOVED_THRESHOLD = 15;
const SWIPE_THRESHOLD = 50;
const RELEASE_WHEEL_ZOOM_DELAY = 150;
@ -89,7 +92,7 @@ let lastClickTime = 0;
const lethargy = new Lethargy({
stability: 5,
sensitivity: 25,
tolerance: 0.6,
tolerance: IS_WINDOWS ? 1 : 0.6, // Windows scrollDelta does not die down to 0
delay: 150,
});

View File

@ -93,7 +93,7 @@ export class Lethargy {
const newSum = lastDeltasNew.reduce((t, s) => t + s);
const oldAverage = oldSum / lastDeltasOld.length;
const newAverage = newSum / lastDeltasNew.length;
return Math.abs(oldAverage) < Math.abs(newAverage * this.tolerance)
return Math.abs(oldAverage) <= Math.abs(newAverage * this.tolerance)
&& this.sensitivity < Math.abs(newAverage);
}
}