diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index 6c0b90ee3..ddf1741d2 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -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 = ({ 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 = ({ // 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 = ({ } // 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); diff --git a/src/components/story/StealthModeModal.module.scss b/src/components/story/StealthModeModal.module.scss index b1c4a7a44..3a68bf4fd 100644 --- a/src/components/story/StealthModeModal.module.scss +++ b/src/components/story/StealthModeModal.module.scss @@ -5,7 +5,7 @@ position: relative; } -.close { +.closeButton { position: absolute; top: 0.5rem; left: 0.5rem; diff --git a/src/components/story/StealthModeModal.tsx b/src/components/story/StealthModeModal.tsx index f528a23cd..d82860155 100644 --- a/src/components/story/StealthModeModal.tsx +++ b/src/components/story/StealthModeModal.tsx @@ -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} > diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 8e55212d1..57513d961 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -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')} > - + ); }; @@ -604,7 +604,7 @@ function Story({ {renderStoryPrivacyButton()} {isVideo && ( )} {lang('lng_report_story')}} {isOut && {lang('Delete')}} + ); @@ -727,7 +736,7 @@ function Story({ )} {isLoadedStory && fullMediaData && ( - + )} diff --git a/src/components/story/StorySlides.tsx b/src/components/story/StorySlides.tsx index b729b68c5..0a829b287 100644 --- a/src/components/story/StorySlides.tsx +++ b/src/components/story/StorySlides.tsx @@ -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(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(undefined); + const isReleasedRef = useRef(false); + const { isMobile } = useAppLayout(); + const animationDuration = isMobile ? 0 : ANIMATION_DURATION_MS; const rendersRef = useRef>({}); 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 ( -
+
{renderingPeerIds.length > 1 && (
diff --git a/src/components/story/StoryViewModal.module.scss b/src/components/story/StoryViewModal.module.scss index 13c03dab4..7a517f920 100644 --- a/src/components/story/StoryViewModal.module.scss +++ b/src/components/story/StoryViewModal.module.scss @@ -98,7 +98,7 @@ border-top: 0.0625rem solid var(--color-borders); } -.close { +.closeButton { margin-block: 0.25rem; } diff --git a/src/components/story/StoryViewer.module.scss b/src/components/story/StoryViewer.module.scss index d525082a0..be1c7e161 100644 --- a/src/components/story/StoryViewer.module.scss +++ b/src/components/story/StoryViewer.module.scss @@ -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; - } } diff --git a/src/components/story/StoryViewer.tsx b/src/components/story/StoryViewer.tsx index 677bfe2a4..0811a9345 100644 --- a/src/components/story/StoryViewer.tsx +++ b/src/components/story/StoryViewer.tsx @@ -155,7 +155,7 @@ function StoryViewer({ ariaLabel={lang('Close')} onClick={handleClose} > - + (`[data-peer-id="${id}"]`); + return el?.querySelector(`[data-peer-id="${id}"]`); } function createDelayedCallback(callback: NoneToVoidFunction, ms: number) { diff --git a/src/components/story/mediaArea/MediaArea.module.scss b/src/components/story/mediaArea/MediaArea.module.scss index b2ef2fe43..7e57b1e00 100644 --- a/src/components/story/mediaArea/MediaArea.module.scss +++ b/src/components/story/mediaArea/MediaArea.module.scss @@ -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 { diff --git a/src/components/story/mediaArea/MediaAreaOverlay.tsx b/src/components/story/mediaArea/MediaAreaOverlay.tsx index 403296398..0bf40348b 100644 --- a/src/components/story/mediaArea/MediaAreaOverlay.tsx +++ b/src/components/story/mediaArea/MediaAreaOverlay.tsx @@ -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]); diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index f974ae5dc..9286bd5b2 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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; diff --git a/src/util/captureEvents.ts b/src/util/captureEvents.ts index fa9e1de1b..e3d434499 100644 --- a/src/util/captureEvents.ts +++ b/src/util/captureEvents.ts @@ -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, }); diff --git a/src/util/lethargy.ts b/src/util/lethargy.ts index c50198953..d98fb1683 100644 --- a/src/util/lethargy.ts +++ b/src/util/lethargy.ts @@ -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); } }