diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 6b086e0edf..0abf8d3382 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -11,7 +11,7 @@ import { useDispatch, useSelector } from "react-redux"; import { useDebounce, useMeasure } from "react-use"; import classNames from "classnames"; import isHotkey from "is-hotkey"; -import { delay, omit } from "lodash"; +import { debounce, delay, omit } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { ChatService, DiscussionMessageService, FileService } from "@/services"; @@ -59,6 +59,7 @@ import { selectFilesPreview, FileInfo, } from "@/store/states"; +import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { ChatContent, ChatContentRef, @@ -181,6 +182,15 @@ export default function ChatComponent({ const chatContentRef = useRef(null); const chatWrapperId = useMemo(() => `chat-wrapper-${uuidv4()}`, []); const chatInputWrapperRef = useRef(null); + const chatContainerRef = useRef(null); + const [isScrolling, setScrolling] = useState(false); + const chatContentContextValue: ChatContentData = useMemo( + () => ({ + isScrolling, + chatContentRect: chatContainerRef.current?.getBoundingClientRect(), + }), + [isScrolling], + ); const [message, setMessage] = useState( parseStringToTextEditorValue(), @@ -591,6 +601,23 @@ export default function ChatComponent({ }; }, []); + useEffect(() => { + const deactivateScrollingFlag = debounce(() => { + setScrolling(false); + }, 300); + + function handleScroll() { + setScrolling(true); + deactivateScrollingFlag(); + } + + chatContainerRef.current?.addEventListener("scroll", handleScroll); + + return () => { + chatContainerRef.current?.removeEventListener("scroll", handleScroll); + }; + }, []); + const renderChatInput = (): ReactNode => { const shouldHideChatInput = !isChatChannel && (!hasAccess || isHidden); @@ -666,32 +693,35 @@ export default function ChatComponent({ [styles.emptyChat]: !dateList.length, })} id={chatWrapperId} + ref={chatContainerRef} > - + + +
diff --git a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx index 17c3d6c60a..e047d3c936 100644 --- a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx +++ b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx @@ -127,20 +127,6 @@ const ChatContent: ForwardRefRenderFunction< [chatWrapperId], ); - const scrollMore = useCallback( - (toY: number) => - setTimeout( - () => - animateScroll.scrollMore(toY, { - containerId: chatWrapperId, - smooth: true, - delay: 0, - }), - 0, - ), - [chatWrapperId], - ); - const dateListReverse = useMemo(() => [...dateList].reverse(), [dateList]); useEffect(() => { @@ -241,17 +227,6 @@ const ChatContent: ForwardRefRenderFunction< scrollToRepliedMessage={scrollToRepliedMessage} highlighted={message.id === highlightedMessageId} hasPermissionToHide={hasPermissionToHide} - onMessageDropdownOpen={(isOpen, messageTopPosition = 0) => { - const dropdownHeight = 240; - const visibleDropdownHeight = - window.innerHeight - messageTopPosition; - const hasEnoughSpaceForMenu = - visibleDropdownHeight >= dropdownHeight; - - if (isOpen && !hasEnoughSpaceForMenu) { - scrollMore(dropdownHeight - visibleDropdownHeight + 20); - } - }} users={users} feedItemId={feedItemId} commonMember={commonMember} diff --git a/src/pages/common/components/CommonContent/context.ts b/src/pages/common/components/CommonContent/context.ts new file mode 100644 index 0000000000..8d8716534c --- /dev/null +++ b/src/pages/common/components/CommonContent/context.ts @@ -0,0 +1,13 @@ +import React, { useContext } from "react"; + +export interface ChatContentData { + isScrolling: boolean; + chatContentRect?: DOMRect; +} + +export const ChatContentContext = React.createContext({ + isScrolling: false, +}); + +export const useChatContentContext = (): ChatContentData => + useContext(ChatContentContext); diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index c7a7274694..6467e98d3a 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState, - useRef, useMemo, } from "react"; import classNames from "classnames"; @@ -53,10 +52,6 @@ interface ChatMessageProps { chatType: ChatType; highlighted?: boolean; className?: string; - onMessageDropdownOpen?: ( - isOpen: boolean, - messageTopPosition?: number, - ) => void; user: User | null; scrollToRepliedMessage: (messageId: string) => void; hasPermissionToHide: boolean; @@ -87,7 +82,6 @@ export default function ChatMessage({ chatType, highlighted = false, className, - onMessageDropdownOpen, user, scrollToRepliedMessage, hasPermissionToHide, @@ -101,7 +95,6 @@ export default function ChatMessage({ onFeedItemClick, onInternalLinkClick, }: ChatMessageProps) { - const messageRef = useRef(null); const { getCommonPagePath, getCommonPageAboutTabPath } = useRoutesContext(); const [isEditMode, setEditMode] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -141,15 +134,6 @@ export default function ChatMessage({ } }; - const handleMessageDropdownOpen = - onMessageDropdownOpen && - ((isOpen: boolean) => { - onMessageDropdownOpen( - isOpen, - messageRef.current?.getBoundingClientRect().top, - ); - }); - useEffect(() => { (async () => { if (!discussionMessage.text) { @@ -217,14 +201,6 @@ export default function ChatMessage({ onUserClick, ]); - const handleMenuToggle = (isOpen: boolean) => { - setIsMenuOpen(isOpen); - - if (handleMessageDropdownOpen) { - handleMessageDropdownOpen(isOpen); - } - }; - const handleLongPress = () => { setIsMenuOpen(true); }; @@ -383,7 +359,6 @@ export default function ChatMessage({ ) : ( <>
, - menuRef: HTMLUListElement | null, -): CSSProperties | undefined => { - if (!ref.current || !menuRef) { - return; - } - - const { top, left, height } = ref.current.getBoundingClientRect(); - const menuRect = menuRef.getBoundingClientRect(); - const bottom = top + height + menuRect.height; - const styles: CSSProperties = { - left, - top: top + height, - }; - - if (window.innerHeight < bottom) { - styles.top = top - menuRect.height; - } - if (styles.top && styles.top < 0) { - styles.top = 0; - styles.bottom = 0; - styles.maxHeight = "100%"; - } - - return styles; -}; - -const getMenuStyles = ( - ref: RefObject, - menuRef: HTMLUListElement | null, - shouldBeFixed?: boolean, -): CSSProperties | undefined => { - if (!menuRef) { - return; - } - if (shouldBeFixed) { - return getFixedMenuStyles(ref, menuRef); - } - - const { right } = menuRef.getBoundingClientRect(); - - if (window.innerWidth < right) { - return { right: 0 }; - } -}; - const Dropdown: ForwardRefRenderFunction = ( props, dropdownRef, @@ -153,6 +107,8 @@ const Dropdown: ForwardRefRenderFunction = ( const [isOpen, setIsOpen] = useState(false); const selectedOption = options.find((option) => option.value === value); const dropdownId = useMemo(() => `dropdown-${uuidv4()}`, []); + const { isScrolling: isChatScrolling, chatContentRect } = + useChatContentContext(); const handleSelection: MenuWrapperProps["onSelection"] = ( value, @@ -179,9 +135,16 @@ const Dropdown: ForwardRefRenderFunction = ( } }; + useEffect(() => { + if (isMenuOpen && isChatScrolling) { + handleMenuToggle({ isOpen: false }); + closeMenu(dropdownId); + } + }, [isMenuOpen, isChatScrolling]); + const menuStyles = useMemo( - () => getMenuStyles(menuButtonRef, menuRef, shouldBeFixed), - [menuRef, shouldBeFixed], + () => getMenuStyles(menuButtonRef, menuRef, chatContentRect, shouldBeFixed), + [menuRef, shouldBeFixed, chatContentRect], ); useImperativeHandle( diff --git a/src/shared/components/Dropdown/helpers.ts b/src/shared/components/Dropdown/helpers.ts new file mode 100644 index 0000000000..a0fe50ee04 --- /dev/null +++ b/src/shared/components/Dropdown/helpers.ts @@ -0,0 +1,62 @@ +import { CSSProperties, RefObject } from "react"; + +const getFixedMenuStyles = ( + ref: RefObject, + menuRef: HTMLUListElement | null, +): CSSProperties | undefined => { + if (!ref.current || !menuRef) { + return; + } + + const { top, left, height } = ref.current.getBoundingClientRect(); + const menuRect = menuRef.getBoundingClientRect(); + const bottom = top + height + menuRect.height; + const styles: CSSProperties = { + left, + top: top + height, + }; + + if (window.innerHeight < bottom) { + styles.top = top - menuRect.height; + } + if (styles.top && Number(styles.top) < 0) { + styles.top = 0; + styles.bottom = 0; + styles.maxHeight = "100%"; + } + + return styles; +}; + +export const getMenuStyles = ( + ref: RefObject, + menuRef: HTMLUListElement | null, + chatContentRect?: DOMRect, + shouldBeFixed?: boolean, +): CSSProperties | undefined => { + if (!menuRef) { + return; + } + if (shouldBeFixed) { + return getFixedMenuStyles(ref, menuRef); + } + + const styles: CSSProperties = {}; + const { right, height } = menuRef.getBoundingClientRect(); + + if (window.innerWidth < right) { + styles.right = 0; + } + + if (ref.current && chatContentRect) { + const { bottom: menuButtonBottom, height: menuButtonHeight } = + ref.current.getBoundingClientRect(); + const menuBottom = menuButtonBottom + height; + + if (chatContentRect.bottom < menuBottom) { + styles.bottom = menuButtonHeight; + } + } + + return styles; +}; diff --git a/src/shared/icons/copy.icon.tsx b/src/shared/icons/copy.icon.tsx index 4db0c41a84..6d07a03261 100644 --- a/src/shared/icons/copy.icon.tsx +++ b/src/shared/icons/copy.icon.tsx @@ -17,16 +17,16 @@ export default function CopyIcon({ className }: CopyIconProps): ReactElement { ); diff --git a/src/shared/icons/hide.icon.tsx b/src/shared/icons/hide.icon.tsx index 32d65942b9..d9078dd53a 100644 --- a/src/shared/icons/hide.icon.tsx +++ b/src/shared/icons/hide.icon.tsx @@ -17,23 +17,23 @@ export default function HideIcon({ className }: HideIconProps): ReactElement { );