From 7dfe6a8347224413bba37216f898073fd4ce166d Mon Sep 17 00:00:00 2001 From: VanyaMate Date: Tue, 6 Aug 2024 10:26:02 +0300 Subject: [PATCH] nu kak to tak --- src/pages/DialoguesPage/ui/DialoguesPage.tsx | 6 +- .../hooks/useVirtualItems/useVirtualItems.ts | 15 - .../hooks/useVirtualList/useVirtualList.ts | 38 +++ .../useVirtualScroll/useVirtualScroll.ts | 129 ++++++++ .../lib/calculateAnimationScrollPosition.ts | 27 ++ ...calculateScrollPositionForVirtualBottom.ts | 31 ++ .../calculateScrollPositionForVirtualTop.ts | 31 ++ .../lib/getInitialIndex/getInitialIndex.ts | 19 ++ .../getScrollTargetScrollPosition.ts | 47 +++ .../lib/getVirtualList/getVirtualList.ts | 13 + .../isNextScrollPosition.ts | 37 +++ .../isPreviousScrollPosition.ts | 42 +++ .../ui-kit/box/Virtual/lib/isTop/isTop.ts | 2 +- src/shared/ui-kit/box/Virtual/types/types.ts | 37 +++ .../ui-kit/box/Virtual/ui/Virtual.module.scss | 2 +- src/shared/ui-kit/box/Virtual/ui/Virtual.tsx | 284 +++++++++++++++++- ...rivateMessagesInfinityVirtualContainer.tsx | 20 +- .../ui/GlobalNotifications.tsx | 6 +- 18 files changed, 744 insertions(+), 42 deletions(-) delete mode 100644 src/shared/ui-kit/box/Virtual/hooks/useVirtualItems/useVirtualItems.ts create mode 100644 src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts create mode 100644 src/shared/ui-kit/box/Virtual/hooks/useVirtualScroll/useVirtualScroll.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/calculateAnimationScrollPosition.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualBottom/calculateScrollPositionForVirtualBottom.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualTop/calculateScrollPositionForVirtualTop.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/getInitialIndex/getInitialIndex.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/getScrollTargetScrollPosition/getScrollTargetScrollPosition.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/getVirtualList/getVirtualList.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/isNextScrollPosition/isNextScrollPosition.ts create mode 100644 src/shared/ui-kit/box/Virtual/lib/isPreviousScrollPosition/isPreviousScrollPosition.ts create mode 100644 src/shared/ui-kit/box/Virtual/types/types.ts diff --git a/src/pages/DialoguesPage/ui/DialoguesPage.tsx b/src/pages/DialoguesPage/ui/DialoguesPage.tsx index b768455a..cc545828 100644 --- a/src/pages/DialoguesPage/ui/DialoguesPage.tsx +++ b/src/pages/DialoguesPage/ui/DialoguesPage.tsx @@ -36,9 +36,9 @@ import { DomainPrivateDialogueFull, } from 'product-types/dist/private-dialogue/DomainPrivateDialogueFull'; import { - Virtual, VirtualRenderMethod, - VirtualType, -} from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; + VirtualRenderMethod, VirtualType, +} from '@/shared/ui-kit/box/Virtual/types/types.ts'; +import { Virtual } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; export type DialoguesPageProps = diff --git a/src/shared/ui-kit/box/Virtual/hooks/useVirtualItems/useVirtualItems.ts b/src/shared/ui-kit/box/Virtual/hooks/useVirtualItems/useVirtualItems.ts deleted file mode 100644 index c685931e..00000000 --- a/src/shared/ui-kit/box/Virtual/hooks/useVirtualItems/useVirtualItems.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - VirtualList, - VirtualType, -} from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; - - -export type UseVirtualItemsProps = { - type: VirtualType; - list: VirtualList; - showAmount: number; -} - -export const useVirtualItems = function () { - -}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts b/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts new file mode 100644 index 00000000..735382e5 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts @@ -0,0 +1,38 @@ +import { useCallback, useRef, useState } from 'react'; +import { + getInitialIndex, +} from '@/shared/ui-kit/box/Virtual/lib/getInitialIndex/getInitialIndex.ts'; +import { + getVirtualList, +} from '@/shared/ui-kit/box/Virtual/lib/getVirtualList/getVirtualList.ts'; +import { + VirtualIndexSetter, VirtualList, + VirtualType, +} from '@/shared/ui-kit/box/Virtual/types/types.ts'; + + +export type UseVirtualListProps = { + type: VirtualType; + list: VirtualList; + showAmount: number; +} + +export const useVirtualList = function (props: UseVirtualListProps) { + const { type, list, showAmount } = props; + + const currentIndex = useRef( + getInitialIndex({ type, showAmount, listLength: list.length }), + ); + const [ virtualList, setVirtualList ] = useState( + getVirtualList({ index: currentIndex.current, list, showAmount }), + ); + + const setIndex = useCallback((index: number) => { + currentIndex.current = index; + setVirtualList( + getVirtualList({ index, list, showAmount }), + ); + }, [ list, showAmount ]); + + return { currentIndex, virtualList, setIndex }; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/hooks/useVirtualScroll/useVirtualScroll.ts b/src/shared/ui-kit/box/Virtual/hooks/useVirtualScroll/useVirtualScroll.ts new file mode 100644 index 00000000..9cc6eda6 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/hooks/useVirtualScroll/useVirtualScroll.ts @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; +import { VirtualType } from '@/shared/ui-kit/box/Virtual/types/types.ts'; + + +export type UseVirtualScrollProps = { + animationMs: number; + scrollDistance: number; + type: VirtualType; +} + +export enum VirtualScrollDirection { + TOP = 'top', + BOTTOM = 'bottom', + NONE = 'none' +} + +export const useVirtualScroll = function (props: UseVirtualScrollProps) { + const { + animationMs, + type, + scrollDistance, + } = props; + const containerRef = useRef(null); + const targetScrollPosition = useRef(0); + const startTimeRef = useRef(0); + const startPositionRef = useRef(0); + + const previousScrollHeight = useRef(0); + + const scrollAnimation = useCallback((timestamp?: number) => { + if (startTimeRef.current === 0) { + startTimeRef.current = timestamp; + } + const progress = timestamp - startTimeRef.current; + const easeInOutQuad = (t: number) => t < 0.5 + ? 2 * t * t + : -1 + (4 - 2 * t) * t; + + const elapsedTime = Math.min(progress / animationMs, 1); + const ease = easeInOutQuad(elapsedTime); + const currentScrollPosition = startPositionRef.current + ease * (targetScrollPosition.current - startPositionRef.current); + + containerRef.current.scrollTop = currentScrollPosition; + + if (elapsedTime < 1) { + requestAnimationFrame(scrollAnimation); + } + }, [ animationMs ]); + + const scrollHandler = useCallback((side: VirtualScrollDirection) => { + const { scrollTop, scrollHeight, offsetHeight } = containerRef.current; + + if (type === VirtualType.TOP) { + if (side === VirtualScrollDirection.TOP) { + if (targetScrollPosition.current === 0) { + return; + } + targetScrollPosition.current = Math.max(0, targetScrollPosition.current + scrollDistance); + } else { + if (targetScrollPosition.current === scrollHeight - offsetHeight) { + return; + } + targetScrollPosition.current = Math.min(scrollHeight - offsetHeight, targetScrollPosition.current - scrollDistance); + } + } else { + if (side === VirtualScrollDirection.TOP) { + if (targetScrollPosition.current === -(scrollHeight - offsetHeight)) { + return; + } + targetScrollPosition.current = Math.max(-(scrollHeight - offsetHeight), targetScrollPosition.current - scrollDistance); + } else { + if (targetScrollPosition.current === 0) { + return; + } + targetScrollPosition.current = Math.min(0, targetScrollPosition.current + scrollDistance); + } + } + + startPositionRef.current = scrollTop; + startTimeRef.current = 0; + requestAnimationFrame(scrollAnimation); + }, [ scrollAnimation, scrollDistance, type ]); + + const scrollTo = useCallback((options: ScrollToOptions) => { + targetScrollPosition.current = options.top; + startTimeRef.current = 0; + + if (options.behavior === 'smooth') { + startPositionRef.current = containerRef.current.scrollTop; + requestAnimationFrame(scrollAnimation); + } else { + containerRef.current.scrollTop = options.top; + } + }, [ scrollAnimation ]); + + useEffect(() => { + const ref = containerRef.current; + if (ref) { + previousScrollHeight.current = ref.scrollHeight; + + return () => { + console.log('[RETURN] RefContainerHeight', ref.scrollHeight); + + }; + } + }); + + useLayoutEffect(() => { + const ref = containerRef.current; + + if (ref) { + const onWheelHandler = function (event: WheelEvent) { + if (event.deltaY > 0) { + scrollHandler(VirtualScrollDirection.BOTTOM); + } else { + scrollHandler(VirtualScrollDirection.TOP); + } + }; + + ref.addEventListener('wheel', onWheelHandler); + + return () => { + ref.removeEventListener('wheel', onWheelHandler); + }; + } + }, [ animationMs, scrollDistance, scrollHandler, type ]); + + return { containerRef, scrollTo }; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/calculateAnimationScrollPosition.ts b/src/shared/ui-kit/box/Virtual/lib/calculateAnimationScrollPosition.ts new file mode 100644 index 00000000..405855a1 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/calculateAnimationScrollPosition.ts @@ -0,0 +1,27 @@ +export type CalculateAnimationScrollPositionProps = { + timestamp: number; + startAnimationTime: number; + startAnimationPosition: number; + targetScrollPosition: number; + animationMs: number; +} + +export const calculateAnimationScrollPosition = function (props: CalculateAnimationScrollPositionProps): number { + const { + startAnimationPosition, + startAnimationTime, + animationMs, + targetScrollPosition, + timestamp, + } = props; + + const progress = timestamp - startAnimationTime; + const easeInOutQuad = (t: number) => t < 0.5 + ? 2 * t * t + : -1 + (4 - 2 * t) * t; + + const elapsedTime = Math.min(progress / animationMs, 1); + const ease = easeInOutQuad(elapsedTime); + + return startAnimationPosition + ease * (targetScrollPosition - startAnimationPosition); +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualBottom/calculateScrollPositionForVirtualBottom.ts b/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualBottom/calculateScrollPositionForVirtualBottom.ts new file mode 100644 index 00000000..6ab1d1ad --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualBottom/calculateScrollPositionForVirtualBottom.ts @@ -0,0 +1,31 @@ +export type CalculateScrollPositionForVirtualBottomProps = { + offset: number; + currentTarget: number; + scrollDistance: number; + offsetHeight: number; + scrollHeight: number; +} + +export const calculateScrollPositionForVirtualBottom = function (props: CalculateScrollPositionForVirtualBottomProps): number | null { + const { + offset, + scrollHeight, + offsetHeight, + currentTarget, + scrollDistance, + } = props; + + const isScrollToTop = offset < 0; + + if (isScrollToTop) { + return Math.max(-(scrollHeight - offsetHeight), currentTarget - scrollDistance); + } + + const isScrollToBottom = offset > 0; + + if (isScrollToBottom) { + return Math.min(0, currentTarget + scrollDistance); + } + + return null; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualTop/calculateScrollPositionForVirtualTop.ts b/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualTop/calculateScrollPositionForVirtualTop.ts new file mode 100644 index 00000000..69d3c827 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualTop/calculateScrollPositionForVirtualTop.ts @@ -0,0 +1,31 @@ +export type CalculateScrollPositionForVirtualTopProps = { + offset: number; + currentTarget: number; + scrollDistance: number; + offsetHeight: number; + scrollHeight: number; +} + +export const calculateScrollPositionForVirtualTop = function (props: CalculateScrollPositionForVirtualTopProps): number | null { + const { + offset, + scrollHeight, + offsetHeight, + currentTarget, + scrollDistance, + } = props; + + const isScrollToTop = offset < 0; + + if (isScrollToTop) { + return Math.max(0, currentTarget + scrollDistance); + } + + const isScrollToBottom = offset > 0; + + if (isScrollToBottom) { + return Math.min(scrollHeight - offsetHeight, currentTarget + scrollDistance); + } + + return null; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/getInitialIndex/getInitialIndex.ts b/src/shared/ui-kit/box/Virtual/lib/getInitialIndex/getInitialIndex.ts new file mode 100644 index 00000000..bd950a38 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/getInitialIndex/getInitialIndex.ts @@ -0,0 +1,19 @@ +import { VirtualType } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; +import { isTop } from '@/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts'; + + +export type GetInitialIndexProps = { + type: VirtualType; + listLength: number; + showAmount: number; +} + +export const getInitialIndex = function (props: GetInitialIndexProps): number { + const { type, showAmount, listLength } = props; + + if (isTop(type)) { + return 0; + } + + return Math.max(0, listLength - showAmount); +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/getScrollTargetScrollPosition/getScrollTargetScrollPosition.ts b/src/shared/ui-kit/box/Virtual/lib/getScrollTargetScrollPosition/getScrollTargetScrollPosition.ts new file mode 100644 index 00000000..8bf3b30b --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/getScrollTargetScrollPosition/getScrollTargetScrollPosition.ts @@ -0,0 +1,47 @@ +import { VirtualType } from '@/shared/ui-kit/box/Virtual/types/types.ts'; +import { isTop } from '@/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts'; +import { + calculateScrollPositionForVirtualTop, +} from '@/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualTop/calculateScrollPositionForVirtualTop.ts'; +import { + calculateScrollPositionForVirtualBottom, +} from '@/shared/ui-kit/box/Virtual/lib/calculateScrollPositionForVirtualBottom/calculateScrollPositionForVirtualBottom.ts'; + + +export type GetScrollTargetScrollPositionProps = { + type: VirtualType; + offset: number; + currentTarget: number; + scrollHeight: number; + offsetHeight: number; + scrollDistance: number; +} + +export const getScrollTargetScrollPosition = function (props: GetScrollTargetScrollPositionProps): number | null { + const { + type, + currentTarget, + offset, + offsetHeight, + scrollHeight, + scrollDistance, + } = props; + + if (isTop(type)) { + return calculateScrollPositionForVirtualTop({ + scrollDistance, + scrollHeight, + offsetHeight, + currentTarget, + offset, + }); + } else { + return calculateScrollPositionForVirtualBottom({ + scrollDistance, + scrollHeight, + offsetHeight, + currentTarget, + offset, + }); + } +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/getVirtualList/getVirtualList.ts b/src/shared/ui-kit/box/Virtual/lib/getVirtualList/getVirtualList.ts new file mode 100644 index 00000000..407f0458 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/getVirtualList/getVirtualList.ts @@ -0,0 +1,13 @@ +import { VirtualList } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; + + +export type GetVirtualItemsProps = { + index: number; + list: VirtualList; + showAmount: number; +} + +export const getVirtualList = function (props: GetVirtualItemsProps): VirtualList { + const { list, index, showAmount } = props; + return list.slice(index, showAmount + index); +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/isNextScrollPosition/isNextScrollPosition.ts b/src/shared/ui-kit/box/Virtual/lib/isNextScrollPosition/isNextScrollPosition.ts new file mode 100644 index 00000000..869f0648 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/isNextScrollPosition/isNextScrollPosition.ts @@ -0,0 +1,37 @@ +import { VirtualType } from '@/shared/ui-kit/box/Virtual/types/types.ts'; +import { isTop } from '@/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts'; + + +export type IsNextScrollPosition = { + type: VirtualType; + scrollTop: number; + previousScrollTop: number; + distanceToTrigger: number; +} + +export const isNextScrollPosition = function (props: IsNextScrollPosition): boolean { + const { + type, + distanceToTrigger, + previousScrollTop, + scrollTop, + } = props; + + if (isTop(type)) { + const isTopScroll: boolean = scrollTop < previousScrollTop; + + if (isTopScroll) { + return scrollTop <= distanceToTrigger; + } + + return false; + } + + const isBottomScroll: boolean = scrollTop > previousScrollTop; + + if (isBottomScroll) { + return Math.abs(scrollTop) <= distanceToTrigger; + } + + return false; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/isPreviousScrollPosition/isPreviousScrollPosition.ts b/src/shared/ui-kit/box/Virtual/lib/isPreviousScrollPosition/isPreviousScrollPosition.ts new file mode 100644 index 00000000..8eb325b9 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/lib/isPreviousScrollPosition/isPreviousScrollPosition.ts @@ -0,0 +1,42 @@ +import { isTop } from '@/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts'; +import { VirtualType } from '@/shared/ui-kit/box/Virtual/types/types.ts'; + + +export type IsPreviousScrollPosition = { + type: VirtualType; + scrollTop: number; + previousScrollTop: number; + scrollHeight: number; + offsetHeight: number; + distanceToTrigger: number; +} + + +export const isPreviousScrollPosition = function (props: IsPreviousScrollPosition): boolean { + const { + type, + distanceToTrigger, + previousScrollTop, + scrollHeight, + scrollTop, + offsetHeight, + } = props; + + if (isTop(type)) { + const isBottomScroll: boolean = scrollTop > previousScrollTop; + + if (isBottomScroll) { + return scrollHeight - offsetHeight - scrollTop <= distanceToTrigger; + } + + return false; + } + + const isTopScroll: boolean = scrollTop < previousScrollTop; + + if (isTopScroll) { + return scrollHeight - offsetHeight - Math.abs(scrollTop) <= distanceToTrigger; + } + + return false; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts b/src/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts index 121bb610..ef965235 100644 --- a/src/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts +++ b/src/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts @@ -1,4 +1,4 @@ -import { VirtualType } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; +import { VirtualType } from '@/shared/ui-kit/box/Virtual/types/types.ts'; export const isTop = function (type: VirtualType) { diff --git a/src/shared/ui-kit/box/Virtual/types/types.ts b/src/shared/ui-kit/box/Virtual/types/types.ts new file mode 100644 index 00000000..ca9db284 --- /dev/null +++ b/src/shared/ui-kit/box/Virtual/types/types.ts @@ -0,0 +1,37 @@ +import { MutableRefObject, ReactNode } from 'react'; + + +export type VirtualHandler = () => void; + +export type UseVirtualAction = { + onNextHandler: VirtualHandler; + onPreviousHandler: VirtualHandler; + onOtherHandler: VirtualHandler; + action: MutableRefObject; +} + +export enum VirtualAction { + AUTOSCROLL_NEXT = 'autoscroll_next', + AUTOSCROLL_PREVIOUS = 'autoscroll_previous', + TOGGLE_NEXT = 'toggle_next', + TOGGLE_PREVIOUS = 'toggle_previous', + NONE = 'none' +} + +export enum VirtualScrollDirection { + TOP = 'top', + BOTTOM = 'bottom', + NONE = 'none' +} + + +export type VirtualIndexSetter = (index: number) => void; + +export enum VirtualType { + TOP, + BOTTOM +} + +export type VirtualList = Array; +export type VirtualUploadMethod = () => Promise; +export type VirtualRenderMethod = (item: unknown, index: number) => ReactNode; diff --git a/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss b/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss index f3281281..74ea999d 100644 --- a/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss +++ b/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss @@ -1,5 +1,5 @@ .container { - overflow : auto; + overflow : hidden; width : 100%; height : 100%; display : flex; diff --git a/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx b/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx index 99b10161..ac926f8f 100644 --- a/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx +++ b/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx @@ -1,26 +1,49 @@ /* eslint-disable */ -import { ComponentPropsWithoutRef, FC, memo, ReactNode } from 'react'; +import { + ComponentPropsWithoutRef, + FC, + memo, + ReactNode, + useEffect, + useLayoutEffect, + useRef, +} from 'react'; import classNames from 'classnames'; import css from './Virtual.module.scss'; import { isTop } from '@/shared/ui-kit/box/Virtual/lib/isTop/isTop.ts'; +import { + useVirtualList, +} from '@/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts'; +import { + VirtualAction, + VirtualList, + VirtualRenderMethod, + VirtualType, + VirtualUploadMethod, +} from '@/shared/ui-kit/box/Virtual/types/types.ts'; +import { + getScrollTargetScrollPosition, +} from '@/shared/ui-kit/box/Virtual/lib/getScrollTargetScrollPosition/getScrollTargetScrollPosition.ts'; +import { + calculateAnimationScrollPosition, +} from '@/shared/ui-kit/box/Virtual/lib/calculateAnimationScrollPosition.ts'; +import { + isNextScrollPosition, +} from '@/shared/ui-kit/box/Virtual/lib/isNextScrollPosition/isNextScrollPosition.ts'; +import { + isPreviousScrollPosition, +} from '@/shared/ui-kit/box/Virtual/lib/isPreviousScrollPosition/isPreviousScrollPosition.ts'; -export enum VirtualType { - TOP, - BOTTOM -} - -export type VirtualList = Array; -export type VirtualUploadMethod = () => Promise; -export type VirtualRenderMethod = (item: unknown, index: number) => ReactNode; - export type VirtualProps = { list: VirtualList; render: VirtualRenderMethod; type?: VirtualType; smoothAutoscroll?: boolean; + animationMs?: number; + scrollDistance?: number; showAmount?: number; distanceToTrigger?: number; autoscrollNext?: boolean; @@ -47,6 +70,8 @@ export const Virtual: FC = memo(function Virtual (props) { render, type = VirtualType.TOP, smoothAutoscroll = true, + animationMs = 100, + scrollDistance = 100, showAmount = 20, distanceToTrigger = 100, autoscrollNext = true, @@ -64,15 +89,252 @@ export const Virtual: FC = memo(function Virtual (props) { ...other } = props; + /******************************************************************* + * Items + ******************************************************************/ + + const { virtualList, setIndex, currentIndex } = useVirtualList({ + type, + list, + showAmount, + }); + + + /******************************************************************* + * Scroll + ******************************************************************/ + + const containerRef = useRef(null); + const targetScrollPosition = useRef(0); + const startAnimationTime = useRef(0); + const startAnimationPosition = useRef(0); + + useLayoutEffect(() => { + const ref = containerRef.current; + + if (ref) { + const scrollAnimation = function (timestamp: number) { + if (startAnimationTime.current === 0) { + startAnimationTime.current = timestamp; + } + + const scrollPosition = calculateAnimationScrollPosition({ + animationMs, + timestamp, + startAnimationPosition: startAnimationPosition.current, + targetScrollPosition : targetScrollPosition.current, + startAnimationTime : startAnimationTime.current, + }); + + ref.scrollTop = scrollPosition; + + if (scrollPosition !== targetScrollPosition.current) { + requestAnimationFrame(scrollAnimation); + } + }; + + const onWheelHandler = function (event: WheelEvent) { + const { scrollTop, scrollHeight, offsetHeight } = ref; + + const targetPosition = getScrollTargetScrollPosition({ + scrollDistance, + scrollHeight, + type, + offsetHeight, + currentTarget: targetScrollPosition.current, + offset : event.deltaY, + }); + + + if (targetPosition !== null) { + targetScrollPosition.current = targetPosition; + startAnimationPosition.current = scrollTop; + startAnimationTime.current = 0; + requestAnimationFrame(scrollAnimation); + } + }; + + ref.addEventListener('wheel', onWheelHandler); + return () => { + ref.removeEventListener('wheel', onWheelHandler); + }; + } + }, []); + + + /******************************************************************* + * Toggle list by scroll position + ******************************************************************/ + + const previousScrollTop = useRef(0); + const previousScrollAction = useRef(VirtualAction.AUTOSCROLL_NEXT); + const disableScrollHandler = useRef(false); + + useLayoutEffect(() => { + const ref = containerRef.current; + const content = contentRef.current; + + if (ref && content) { + const onScrollHandler = function () { + const { scrollTop, scrollHeight, offsetHeight } = ref; + + if (disableScrollHandler.current) { + return; + } + + if (isNextScrollPosition({ + scrollTop, + previousScrollTop: previousScrollTop.current, + distanceToTrigger, + type, + }) && previousScrollAction.current !== VirtualAction.TOGGLE_NEXT) { + previousScrollAction.current = VirtualAction.TOGGLE_NEXT; + previousContentHeight.current = content.scrollHeight; + if (isTop(type)) { + const firstElement = content.firstElementChild as HTMLElement; + previousFirstElementPosition.current = firstElement.offsetTop; + previousFirstElement.current = firstElement; + disableScrollHandler.current = true; + setIndex(Math.max(0, currentIndex.current - 10)); + } else { + const lastElement = content.lastElementChild as HTMLElement; + previousLastElementPosition.current = lastElement.offsetTop; + previousLastElement.current = lastElement; + disableScrollHandler.current = true; + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + 10)); + } + } else if (isPreviousScrollPosition({ + scrollTop, + scrollHeight, + offsetHeight, + previousScrollTop: previousScrollTop.current, + distanceToTrigger, + type, + }) && previousScrollAction.current !== VirtualAction.TOGGLE_PREVIOUS) { + previousScrollAction.current = VirtualAction.TOGGLE_PREVIOUS; + previousContentHeight.current = content.scrollHeight; + if (isTop(type)) { + const lastElement = content.lastElementChild as HTMLElement; + previousLastElementPosition.current = lastElement.offsetTop; + previousLastElement.current = lastElement; + disableScrollHandler.current = true; + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + 10)); + } else { + const firstElement = content.firstElementChild as HTMLElement; + previousFirstElementPosition.current = firstElement.offsetTop; + previousFirstElement.current = firstElement; + disableScrollHandler.current = true; + setIndex(Math.max(0, currentIndex.current - 10)); + } + } else { + previousScrollAction.current = VirtualAction.NONE; + } + + previousScrollTop.current = scrollTop; + }; + + ref.addEventListener('scroll', onScrollHandler); + return () => { + ref.removeEventListener('scroll', onScrollHandler); + }; + } + }, [ setIndex, list ]); + + + /******************************************************************* + * Scroll after update virtual list + ******************************************************************/ + + const contentRef = useRef(null); + const previousFirstElement = useRef(null); + const previousFirstElementPosition = useRef(0); + const previousLastElement = useRef(null); + const previousLastElementPosition = useRef(0); + const previousContentHeight = useRef(null); + + useEffect(() => { + const ref = containerRef.current; + const content = contentRef.current; + + if (ref && content) { + switch (previousScrollAction.current) { + case VirtualAction.TOGGLE_NEXT: + if (isTop(type)) { + const firstElement = content.firstElementChild as HTMLElement; + const previousElement = previousFirstElement.current; + + if (firstElement !== previousElement) { + const currentPosition = previousElement.offsetTop; + const positionDelta = currentPosition - previousFirstElementPosition.current; + const heightDelta = content.scrollHeight - previousContentHeight.current; + ref.scrollTop = previousScrollTop.current + positionDelta - heightDelta; + targetScrollPosition.current = targetScrollPosition.current + positionDelta; + startAnimationPosition.current = startAnimationPosition.current + positionDelta; + } + } else { + const lastElement = content.lastElementChild as HTMLElement; + const previousElement = previousLastElement.current; + + if (lastElement !== previousElement) { + const currentPosition = previousElement.offsetTop; + const positionDelta = currentPosition - previousLastElementPosition.current; + ref.scrollTop = previousScrollTop.current + positionDelta; + targetScrollPosition.current = targetScrollPosition.current + positionDelta; + startAnimationPosition.current = startAnimationPosition.current + positionDelta; + } + } + disableScrollHandler.current = false; + break; + case VirtualAction.TOGGLE_PREVIOUS: + if (isTop(type)) { + const lastElement = content.lastElementChild as HTMLElement; + const previousElement = previousLastElement.current; + + if (lastElement !== previousElement) { + const currentPosition = previousElement.offsetTop; + const positionDelta = currentPosition - previousLastElementPosition.current; + const heightDelta = content.scrollHeight - previousContentHeight.current; + ref.scrollTop = previousScrollTop.current + positionDelta - heightDelta; + targetScrollPosition.current = targetScrollPosition.current + positionDelta; + startAnimationPosition.current = startAnimationPosition.current + positionDelta; + } + } else { + const firstElement = content.firstElementChild as HTMLElement; + const previousElement = previousFirstElement.current; + + if (firstElement !== previousElement) { + const currentPosition = previousElement.offsetTop; + const positionDelta = currentPosition - previousFirstElementPosition.current; + ref.scrollTop = previousScrollTop.current + positionDelta; + targetScrollPosition.current = targetScrollPosition.current + positionDelta; + startAnimationPosition.current = startAnimationPosition.current + positionDelta; + } + } + disableScrollHandler.current = false; + break; + case VirtualAction.AUTOSCROLL_NEXT: + break; + case VirtualAction.AUTOSCROLL_PREVIOUS: + break; + default: + disableScrollHandler.current = false; + break; + + } + } + }, [ virtualList ]); + return (
- // + { virtualList.map(render) }
); diff --git a/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx b/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx index 585d9d37..9ad06c9c 100644 --- a/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx +++ b/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx @@ -8,7 +8,7 @@ import { import { useStore } from '@vanyamate/sec-react'; import { $privateMessages, - $privateMessagesHasMore, + $privateMessagesHasMore, $privateMessagesIsPending, getPrivateMessagesByCursorEffect, readPrivateMessageEffect, } from '@/app/model/private-messages/private-messages.model.ts'; @@ -19,13 +19,14 @@ import { import { $authUser } from '@/app/model/auth/auth.model.ts'; import css from './PrivateMessagesInfinityVirtualContainer.module.scss'; import classNames from 'classnames'; -import { - Virtual, VirtualRenderMethod, - VirtualType, -} from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; import { EmptyDialogue, } from '@/entities/dialogue/EmptyDialogue/ui/EmptyDialogue.tsx'; +import { + VirtualRenderMethod, + VirtualType, +} from '@/shared/ui-kit/box/Virtual/types/types.ts'; +import { Virtual } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; export type PrivateMessagesInfinityVirtualContainerProps = @@ -38,6 +39,7 @@ export const PrivateMessagesInfinityVirtualContainer: FC { @@ -45,7 +47,7 @@ export const PrivateMessagesInfinityVirtualContainer: FC