From 7c0379ba514e1e26e87ca9a0f6e412ca44d4fd30 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:16:57 -0400 Subject: [PATCH] fix: Allow Mobile Scroll During Message Stream (#984) * fix(Icon/types): pick types from TMessage and TConversation * refactor: make abortScroll a global recoil state and change props/types for useScrollToRef * refactor(Message): invoke abort setter onTouchMove and onWheel, refactor(Messages): remove redundancy, reset abortScroll when scroll button is clicked --- client/src/common/types.ts | 21 ++++----- client/src/components/Endpoints/Icon.tsx | 4 +- client/src/components/Messages/Message.tsx | 14 +++--- client/src/components/Messages/Messages.tsx | 46 +++++++++---------- client/src/hooks/useScrollToRef.ts | 10 +++- client/src/store/index.ts | 4 +- .../store/{optionSettings.ts => settings.ts} | 6 +++ 7 files changed, 55 insertions(+), 50 deletions(-) rename client/src/store/{optionSettings.ts => settings.ts} (87%) diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 2fc9164b71..030095fcdf 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -179,16 +179,11 @@ export type TAuthConfig = { loginRedirect: string; }; -export type IconProps = { - size?: number; - isCreatedByUser?: boolean; - button?: boolean; - model?: string; - message?: boolean; - className?: string; - endpoint?: string | null; - error?: boolean; - chatGptLabel?: string; - modelLabel?: string; - jailbreak?: boolean; -}; +export type IconProps = Pick & + Pick & { + size?: number; + button?: boolean; + message?: boolean; + className?: string; + endpoint?: string | null; + }; diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index a561812655..4c7a655da2 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -19,7 +19,7 @@ const Icon: React.FC = (props) => { width: size, height: size, }} - className={`relative flex items-center justify-center ${props.className || ''}`} + className={`relative flex items-center justify-center ${props.className ?? ''}`} > = (props) => { default: { icon: , bg: 'grey', name: 'UNKNOWN' }, }; - const { icon, bg, name } = endpointIcons[endpoint] || endpointIcons.default; + const { icon, bg, name } = endpointIcons[endpoint ?? ''] ?? endpointIcons.default; return (
setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId); - const handleWheel = () => { + const handleScroll = () => { if (blinker) { - setAbort(true); + setAbortScroll(true); } else { - setAbort(false); + setAbortScroll(false); } }; @@ -133,7 +133,7 @@ export default function Message({ return ( <> -
+
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? ( diff --git a/client/src/components/Messages/Messages.tsx b/client/src/components/Messages/Messages.tsx index bf454ea47f..19f4b1e7b3 100644 --- a/client/src/components/Messages/Messages.tsx +++ b/client/src/components/Messages/Messages.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { CSSTransition } from 'react-transition-group'; -import { useRecoilValue } from 'recoil'; import ScrollToBottom from './ScrollToBottom'; import MessageHeader from './MessageHeader'; @@ -18,6 +18,7 @@ export default function Messages({ isSearchView = false }) { const messagesTree = useRecoilValue(store.messagesTree); const showPopover = useRecoilValue(store.showPopover); + const setAbortScroll = useSetRecoilState(store.abortScroll); const searchResultMessagesTree = useRecoilValue(store.searchResultMessagesTree); const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree; @@ -27,50 +28,47 @@ export default function Messages({ isSearchView = false }) { const { screenshotTargetRef } = useScreenshot(); - const handleScroll = () => { + const checkIfAtBottom = useCallback(() => { if (!scrollableRef.current) { return; } + const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; const diff = Math.abs(scrollHeight - scrollTop); const percent = Math.abs(clientHeight - diff) / clientHeight; - if (percent <= 0.2) { - setShowScrollButton(false); - } else { - setShowScrollButton(true); - } - }; + const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15; + setShowScrollButton(hasScrollbar); + }, [scrollableRef]); useEffect(() => { const timeoutId = setTimeout(() => { - if (!scrollableRef.current) { - return; - } - const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; - const diff = Math.abs(scrollHeight - scrollTop); - const percent = Math.abs(clientHeight - diff) / clientHeight; - const hasScrollbar = scrollHeight > clientHeight && percent > 0.2; - setShowScrollButton(hasScrollbar); + checkIfAtBottom(); }, 650); // Add a listener on the window object - window.addEventListener('scroll', handleScroll); + window.addEventListener('scroll', checkIfAtBottom); return () => { clearTimeout(timeoutId); - window.removeEventListener('scroll', handleScroll); + window.removeEventListener('scroll', checkIfAtBottom); }; - }, [_messagesTree]); + }, [_messagesTree, checkIfAtBottom]); let timeoutId: ReturnType | undefined; const debouncedHandleScroll = () => { clearTimeout(timeoutId); - timeoutId = setTimeout(handleScroll, 100); + timeoutId = setTimeout(checkIfAtBottom, 100); }; - const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef(messagesEndRef, () => - setShowScrollButton(false), - ); + const scrollCallback = () => setShowScrollButton(false); + const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({ + targetRef: messagesEndRef, + callback: scrollCallback, + smoothCallback: () => { + scrollCallback(); + setAbortScroll(false); + }, + }); return (
, callback: () => void) { +type TUseScrollToRef = { + targetRef: RefObject; + callback: () => void; + smoothCallback: () => void; +}; + +export default function useScrollToRef({ targetRef, callback, smoothCallback }: TUseScrollToRef) { // eslint-disable-next-line react-hooks/exhaustive-deps const scrollToRef = useCallback( throttle( @@ -20,7 +26,7 @@ export default function useScrollToRef(targetRef: RefObject, cal throttle( () => { targetRef.current?.scrollIntoView({ behavior: 'smooth' }); - callback(); + smoothCallback(); }, 750, { leading: true }, diff --git a/client/src/store/index.ts b/client/src/store/index.ts index 2fe0d1f1f5..86f2144c00 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -8,7 +8,7 @@ import submission from './submission'; import search from './search'; import preset from './preset'; import lang from './language'; -import optionSettings from './optionSettings'; +import settings from './settings'; export default { ...conversation, @@ -21,5 +21,5 @@ export default { ...search, ...preset, ...lang, - ...optionSettings, + ...settings, }; diff --git a/client/src/store/optionSettings.ts b/client/src/store/settings.ts similarity index 87% rename from client/src/store/optionSettings.ts rename to client/src/store/settings.ts index d2956f9070..139be45cbb 100644 --- a/client/src/store/optionSettings.ts +++ b/client/src/store/settings.ts @@ -5,6 +5,11 @@ type TOptionSettings = { isCodeChat?: boolean; }; +const abortScroll = atom({ + key: 'abortScroll', + default: false, +}); + const optionSettings = atom({ key: 'optionSettings', default: {}, @@ -31,6 +36,7 @@ const showPopover = atom({ }); export default { + abortScroll, optionSettings, showPluginStoreDialog, showAgentSettings,