diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss index dbc13c9..f523ed6 100644 --- a/src/app/molecules/message/Message.scss +++ b/src/app/molecules/message/Message.scss @@ -26,6 +26,7 @@ & button { cursor: pointer; + display: flex; } [dir=rtl] & { diff --git a/src/app/organisms/room/RoomView.jsx b/src/app/organisms/room/RoomView.jsx index edb427d..867073a 100644 --- a/src/app/organisms/room/RoomView.jsx +++ b/src/app/organisms/room/RoomView.jsx @@ -5,6 +5,7 @@ import './RoomView.scss'; import EventEmitter from 'events'; import RoomTimeline from '../../../client/state/RoomTimeline'; +import { Debounce, getScrollInfo } from '../../../util/common'; import ScrollView from '../../atoms/scroll/ScrollView'; @@ -14,98 +15,125 @@ import RoomViewFloating from './RoomViewFloating'; import RoomViewInput from './RoomViewInput'; import RoomViewCmdBar from './RoomViewCmdBar'; -import { scrollToBottom, isAtBottom, autoScrollToBottom } from './common'; - const viewEvent = new EventEmitter(); -let lastScrollTop = 0; -let lastScrollHeight = 0; -let isReachedBottom = true; -let isReachedTop = false; function RoomView({ roomId }) { const [roomTimeline, updateRoomTimeline] = useState(null); + const [debounce] = useState(new Debounce()); const timelineSVRef = useRef(null); useEffect(() => { roomTimeline?.removeInternalListeners(); updateRoomTimeline(new RoomTimeline(roomId)); - isReachedBottom = true; - isReachedTop = false; }, [roomId]); const timelineScroll = { reachBottom() { - scrollToBottom(timelineSVRef); + timelineScroll.isOngoing = true; + const target = timelineSVRef?.current; + if (!target) return; + const maxScrollTop = target.scrollHeight - target.offsetHeight; + target.scrollTop = maxScrollTop; + timelineScroll.position = 'BOTTOM'; + timelineScroll.isScrollable = maxScrollTop > 0; + timelineScroll.isInTopHalf = false; + timelineScroll.lastTopMsg = null; + timelineScroll.lastBottomMsg = null; }, autoReachBottom() { - autoScrollToBottom(timelineSVRef); + if (timelineScroll.position === 'BOTTOM') timelineScroll.reachBottom(); }, tryRestoringScroll() { + timelineScroll.isOngoing = true; const sv = timelineSVRef.current; - const { scrollHeight } = sv; + const { + lastTopMsg, lastBottomMsg, + diff, isInTopHalf, lastTop, + } = timelineScroll; - if (lastScrollHeight === scrollHeight) return; - - if (lastScrollHeight < scrollHeight) { - sv.scrollTop = lastScrollTop + (scrollHeight - lastScrollHeight); - } else { - timelineScroll.reachBottom(); + if (lastTopMsg === null) { + sv.scrollTop = sv.scrollHeight; + return; } + + const ot = isInTopHalf ? lastTopMsg?.offsetTop : lastBottomMsg?.offsetTop; + if (!ot) sv.scrollTop = lastTop; + else sv.scrollTop = ot - diff; }, - enableSmoothScroll() { - timelineSVRef.current.style.scrollBehavior = 'smooth'; - }, - disableSmoothScroll() { - timelineSVRef.current.style.scrollBehavior = 'auto'; - }, - isScrollable() { - const oHeight = timelineSVRef.current.offsetHeight; - const sHeight = timelineSVRef.current.scrollHeight; - if (sHeight > oHeight) return true; - return false; - }, + position: 'BOTTOM', + isScrollable: false, + isInTopHalf: false, + maxEvents: 50, + lastTop: 0, + lastHeight: 0, + lastViewHeight: 0, + lastTopMsg: null, + lastBottomMsg: null, + diff: 0, + isOngoing: false, }; - function onTimelineScroll(e) { - const { scrollTop, scrollHeight, offsetHeight } = e.target; - const scrollBottom = scrollTop + offsetHeight; - lastScrollTop = scrollTop; - lastScrollHeight = scrollHeight; - - const PLACEHOLDER_HEIGHT = 96; - const PLACEHOLDER_COUNT = 3; - - const topPagKeyPoint = PLACEHOLDER_COUNT * PLACEHOLDER_HEIGHT; - const bottomPagKeyPoint = scrollHeight - (offsetHeight / 2); - - if (!isReachedBottom && isAtBottom(timelineSVRef)) { - isReachedBottom = true; - viewEvent.emit('toggle-reached-bottom', true); - } - if (isReachedBottom && !isAtBottom(timelineSVRef)) { - isReachedBottom = false; - viewEvent.emit('toggle-reached-bottom', false); - } - // TOP of timeline - if (scrollTop < topPagKeyPoint && isReachedTop === false) { - isReachedTop = true; - viewEvent.emit('reached-top'); + const calcScroll = (target) => { + if (timelineScroll.isOngoing) { + timelineScroll.isOngoing = false; return; } - isReachedTop = false; + const PLACEHOLDER_COUNT = 2; + const PLACEHOLDER_HEIGHT = 96 * PLACEHOLDER_COUNT; + const SMALLEST_MSG_HEIGHT = 32; + const scroll = getScrollInfo(target); - // BOTTOM of timeline - if (scrollBottom > bottomPagKeyPoint) { - // TODO: + const isPaginateBack = scroll.top < PLACEHOLDER_HEIGHT; + const isPaginateForward = scroll.bottom > (scroll.height - PLACEHOLDER_HEIGHT); + timelineScroll.isInTopHalf = scroll.top + (scroll.viewHeight / 2) < scroll.height / 2; + + if (timelineScroll.lastViewHeight !== scroll.viewHeight) { + timelineScroll.maxEvents = Math.round(scroll.viewHeight / SMALLEST_MSG_HEIGHT) * 3; + timelineScroll.lastViewHeight = scroll.viewHeight; } - } + timelineScroll.isScrollable = scroll.isScrollable; + timelineScroll.lastTop = scroll.top; + timelineScroll.lastHeight = scroll.height; + const tChildren = target.lastElementChild.lastElementChild.children; + const lCIndex = tChildren.length - 1; + + timelineScroll.lastTopMsg = tChildren[0]?.className === 'ph-msg' + ? tChildren[PLACEHOLDER_COUNT] + : tChildren[0]; + timelineScroll.lastBottomMsg = tChildren[lCIndex]?.className === 'ph-msg' + ? tChildren[lCIndex - PLACEHOLDER_COUNT] + : tChildren[lCIndex]; + + if (timelineScroll.isInTopHalf && timelineScroll.lastBottomMsg) { + timelineScroll.diff = timelineScroll.lastTopMsg.offsetTop - scroll.top; + } else { + timelineScroll.diff = timelineScroll.lastBottomMsg.offsetTop - scroll.top; + } + + if (isPaginateBack) { + timelineScroll.position = 'TOP'; + viewEvent.emit('timeline-scroll', timelineScroll.position); + } else if (isPaginateForward) { + timelineScroll.position = 'BOTTOM'; + viewEvent.emit('timeline-scroll', timelineScroll.position); + } else { + timelineScroll.position = 'BETWEEN'; + viewEvent.emit('timeline-scroll', timelineScroll.position); + } + }; + + const handleTimelineScroll = (event) => { + const { target } = event; + if (!target) return; + debounce._(calcScroll, 200)(target); + }; return (