diff --git a/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx b/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx index 7bb3b94e8..ad1f8fc84 100644 --- a/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx @@ -159,6 +159,7 @@ export const ChatPreview: React.FC<IChatPreviewProps> = (options: IChatPreviewPr flex="initial" cursor="pointer" className={options.readmode ? 'skeleton' : ''} + animation={theme.skeletonBG} > <Message theme={theme}> {options?.chatPreviewPayload?.chatMsg?.messageType === 'Image' || diff --git a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx index f51098d0b..05d23a046 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx @@ -25,7 +25,6 @@ import { ThemeContext } from '../theme/ThemeProvider'; // Assets // Interfaces & Types -import { IReactionsForChatMessages } from '../../../types'; import { Group, IChatViewListProps } from '../exportedTypes'; import { IChatTheme } from '../theme'; import { IChatInfoResponse } from '../types'; @@ -52,8 +51,6 @@ const CHAT_STATUS = { INVALID_CHAT: 'Invalid chatId', }; -const SCROLL_LIMIT = 25; - // Exported Interfaces & Types // Exported Functions @@ -71,16 +68,11 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis // const [chatStatusText, setChatStatusText] = useState<string>(''); const [messages, setMessages] = useState<IMessageIPFSWithCID[]>([]); - const [reactions, setReactions] = useState<IReactionsForChatMessages>({}); - const { historyMessages, historyLoading: messageLoading } = useFetchMessageUtilities(); - const scrollRef = useRef<HTMLDivElement>(null); + const listInnerRef = useRef<HTMLDivElement>(null); const [stopPagination, setStopPagination] = useState<boolean>(false); const { fetchChat } = useFetchChat(); - // keep tab on singular action id, useful to ensure only one action takes place - const [singularActionId, setSingularActionId] = useState<string | null | undefined>(null); - // for stream const { chatStream, @@ -95,7 +87,7 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis const dates = new Set(); // Primary Hook that fetches and sets ChatInfo which then fetches and sets UserInfo - // Which then calls await fetchChatMessages(); to fetch messages + // Which then calls await getMessagesCall(); to fetch messages useEffect(() => { (async () => { if (!user) return; @@ -141,143 +133,15 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis }; }, [chatId, user]); - // When loading is done - fetch chat messages + // When loading is done useEffect(() => { if (initialized.loading) return; (async function () { - await fetchChatMessages(); + await getMessagesCall(); })(); }, [initialized.loading]); - // when chat messages are changed or chat reactions are changed - useEffect(() => { - const checkForScrollAndFetchMessages = async () => { - if ( - !initialized.loading && - scrollRef && - scrollRef?.current && - scrollRef?.current?.parentElement && - !messageLoading && - !stopPagination - ) { - console.debug( - 'UIWeb::ChatViewList::useEffect[messages, reactions]::Checking if we need to load more chats::', - messages, - reactions, - scrollRef.current.clientHeight, - SCROLL_LIMIT, - scrollRef.current.parentElement.clientHeight, - scrollRef.current.clientHeight + SCROLL_LIMIT < scrollRef.current.parentElement.clientHeight - ); - - if (scrollRef.current.clientHeight + SCROLL_LIMIT < scrollRef.current.parentElement.clientHeight) { - await fetchChatMessages(); - } - } - }; - - // new messages are loaded, calculate new top and adjust since render is done - if (scrollRef.current) { - const content = scrollRef.current; - - const oldScrollHeight = parseInt(content.getAttribute('data-old-scroll-height') || '0', 10); // Old scroll height before messages are added - const newScrollHeight = content.scrollHeight; // New scroll height after messages are added - const scrollHeightDifference = newScrollHeight - oldScrollHeight; // Calculate how much the scroll height has increased plus some variance for spinner - - // Adjust the scroll position by the difference in scroll height to maintain the same view - content.scrollTop += scrollHeightDifference; - } - - // check and fetch messages - checkForScrollAndFetchMessages(); - }, [messages]); - - // Smart Scrolling - // Scroll to bottom if user hasn't scrolled or if scroll is at bottom - // Else leave the scroll as it is - // to get scroll lock - const onScroll = async () => { - if (scrollRef.current) { - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - - let scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; - const programmableScroll = scrollRef.current.getAttribute('data-programmable-scroll') === 'true' ? true : false; - const programmableScrollTop = scrollRef.current.getAttribute('data-programmable-scroll-top') || 0; - - // user has scrolled away so scroll should not be locked - if (programmableScroll === false) { - scrollLocked = false; - } - - // lock scroll if user is at bottom - if (scrollTop + clientHeight >= scrollHeight - 10) { - // add 10 for variability - scrollLocked = true; - } - - console.debug( - `UIWeb::ChatViewList::onScroll::scrollLocked ${new Date().toISOString()}`, - scrollRef.current.scrollTop, - scrollRef.current.clientHeight, - scrollRef.current.scrollHeight, - scrollLocked - ); - - // update scroll-locked attribute - scrollRef.current.setAttribute('data-scroll-locked', scrollLocked.toString()); - - if (scrollTop === 0) { - const content = scrollRef.current; - const oldScrollHeight = content.scrollHeight; // Capture the old scroll height before new messages are added - scrollRef.current.setAttribute('data-old-scroll-height', oldScrollHeight.toString()); - - await fetchChatMessages(); - } - } - }; - - // To enable smart scrolling when content height gets adjsuted - const chatContainerRef = useRef<HTMLDivElement>(null); - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const { height } = entry.contentRect; - - if (scrollRef.current && height !== 0) { - const scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; - - console.debug( - `UIWeb::ChatViewList::onScroll::scrollLocked Observer ${new Date().toISOString()}`, - scrollRef.current.scrollTop, - scrollRef.current.clientHeight, - scrollRef.current.scrollHeight, - scrollLocked - ); - - if (height !== 0 && scrollLocked) { - // update programmable-scroll attribute - scrollRef.current.setAttribute('data-programmable-scroll', 'true'); - scrollRef.current?.scrollTo(0, scrollRef.current?.scrollHeight); - - // update programmable-scroll attribute after timeout of 1000ms for previews to render - setTimeout(() => { - if (scrollRef.current) { - scrollRef.current.setAttribute('data-programmable-scroll', 'false'); - } - }, 1000); - } - } - } - }); - - if (chatContainerRef.current) { - resizeObserver.observe(chatContainerRef.current); - } - - return () => resizeObserver.disconnect(); // clean up - }, [chatContainerRef.current]); - // Change listtype to 'CHATS' and hidden to false when chatAcceptStream is received useEffect(() => { if (Object.keys(chatAcceptStream || {}).length > 0 && chatAcceptStream.constructor === Object) { @@ -293,7 +157,9 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis return () => clearTimeout(timer); } - return () => {}; + return () => { + // add comment + }; }, [chatAcceptStream, participantJoinStream]); // Change listtype to 'UINITIALIZED' and hidden to true when participantRemoveStream or participantLeaveStream is received @@ -310,12 +176,16 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis useEffect(() => { if (Object.keys(chatStream || {}).length > 0 && chatStream.constructor === Object) { transformSteamMessage(chatStream); + // setChatStatusText(''); + scrollToBottom(); } }, [chatStream]); useEffect(() => { if (Object.keys(chatRequestStream || {}).length > 0 && chatRequestStream.constructor === Object) { transformSteamMessage(chatRequestStream); + // setChatStatusText(''); + scrollToBottom(); } }, [chatRequestStream]); @@ -328,112 +198,97 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis const transformedMessage = transformStreamToIMessageIPFSWithCID(item); if (messages && messages.length) { const newChatViewList = appendUniqueMessages(messages, [transformedMessage], false); - filterChatMessages(newChatViewList); + setFilteredMessages(newChatViewList); } else { - filterChatMessages([transformedMessage]); + setFilteredMessages([transformedMessage]); } } }; - const fetchChatMessages = async () => { - if (user && !stopPagination && !messageLoading) { - const reference = messages && messages?.length ? messages[0].link : null; - const chatHistory = await historyMessages({ - limit: limit, - chatId: chatId, - reference, - }); + useEffect(() => { + if (messages && messages?.length && messages?.length <= limit) { + // setChatStatusText(''); + scrollToBottom(); + } + }, [messages]); - if (chatHistory && chatHistory?.length) { - const reversedChatHistory = chatHistory?.reverse(); - if (messages && messages?.length) { - const newChatViewList = appendUniqueMessages(messages, reversedChatHistory, true); - filterChatMessages(newChatViewList as IMessageIPFSWithCID[]); - } else { - filterChatMessages(reversedChatHistory as IMessageIPFSWithCID[]); - } + //methods + const scrollToBottom = () => { + requestAnimationFrame(() => { + if (listInnerRef.current) { + listInnerRef.current.scrollTop = listInnerRef.current.scrollHeight; } + }); + }; - // check and stop pagination if user is readmode and chatInfo visibility is false - if ( - (user && user.readmode() && initialized.chatInfo?.meta?.visibility === false) || - initialized.chatInfo?.meta?.group === false - ) { - // not a public group - setStopPagination(true); - } + const onScroll = async () => { + if (listInnerRef.current) { + const { scrollTop } = listInnerRef.current; + if (scrollTop === 0) { + const content = listInnerRef.current; + const curScrollPos = content.scrollTop; + const oldScroll = content.scrollHeight - content.clientHeight; + + await getMessagesCall(); - // check and stop pagination if all chats are fetched - if (!chatHistory || chatHistory?.length < limit) { - setStopPagination(true); + const newScroll = content.scrollHeight - content.clientHeight; + content.scrollTop = newScroll - oldScroll; } } }; - const processChatReactions = (messageList: Array<IMessageIPFSWithCID>) => { - const reactionMessages = reactions; - - for (const message of messageList) { - if (message.messageType === 'Reaction') { - const reaction = message as IMessageIPFSWithCID; + const getMessagesCall = async () => { + let reference = null; + let stopFetchingChats = false; + if (messages && messages?.length) { + reference = messages[0].link; + if (!reference) { + stopFetchingChats = true; + setStopPagination(stopFetchingChats); + } + } - // TODO: This should be present as an interface in the restapi package - const reference = (reaction as any).messageObj?.reference ?? ''; + if (user && !stopFetchingChats) { + const chatHistory = await historyMessages({ + limit: limit, + chatId: chatId, + reference, + }); - if (!reactionMessages[reference]) { - reactionMessages[reference] = []; + if (chatHistory?.length) { + const reversedChatHistory = chatHistory?.reverse(); + if (messages && messages?.length) { + const newChatViewList = appendUniqueMessages(messages, reversedChatHistory, true); + setFilteredMessages(newChatViewList as IMessageIPFSWithCID[]); + } else { + setFilteredMessages(reversedChatHistory as IMessageIPFSWithCID[]); } - // Push the reaction directly into the array - reactionMessages[reference].push(reaction); } } - - return reactionMessages; }; - const filterChatMessages = (messageList: Array<IMessageIPFSWithCID>) => { - // filter duplicates - const uniqueMessagesList = messageList.filter((msg) => !chatFilterList.includes(msg.cid)); + const setFilteredMessages = (messageList: Array<IMessageIPFSWithCID>) => { + const updatedMessageList = messageList.filter((msg) => !chatFilterList.includes(msg.cid)); - // remove reactions into reactions - const reactionMessages = processChatReactions(uniqueMessagesList); - - console.debug( - `UIWeb::ChatViewList::filterChatMessages::uniqueMessageList::${new Date().toISOString()}`, - uniqueMessagesList - ); - console.debug( - `UIWeb::ChatViewList::filterChatMessages::reactionMessages::${new Date().toISOString()}`, - reactionMessages - ); - - if (uniqueMessagesList && uniqueMessagesList.length) { - setMessages([...uniqueMessagesList]); - } - - if (reactionMessages && reactionMessages.length) { - // deep copy to update - setReactions(JSON.parse(JSON.stringify(reactionMessages))); + if (updatedMessageList && updatedMessageList.length) { + setMessages([...updatedMessageList]); } }; type RenderDataType = { chat: IMessageIPFS; dateNum: string; - uid: string; }; - const renderDate = ({ chat, dateNum, uid }: RenderDataType) => { + const renderDate = ({ chat, dateNum }: RenderDataType) => { const timestampDate = dateToFromNowDaily(chat.timestamp as number); dates.add(dateNum); return ( <Span - key={uid} margin="15px 0" fontSize={theme.fontSize?.timestamp} fontWeight={theme.fontWeight?.timestamp} color={theme.textColor?.timestamp} textAlign="center" - zIndex={uid} > {timestampDate} </Span> @@ -442,15 +297,12 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis return ( <ChatViewListCard - key={user?.uid} - data-scroll-locked="true" - data-programmable-scroll="false" blur={false} - overflow="auto" + overflow="hidden scroll" flexDirection="column" - ref={scrollRef} + ref={listInnerRef} width="100%" - height="auto" + height="100%" justifyContent="start" padding="0 2px" theme={theme} @@ -458,10 +310,6 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis e.stopPropagation(); if (!stopPagination) onScroll(); }} - onClick={() => { - // cancel any singular action - setSingularActionId(null); - }} > <Section margin="5px 0 10px 0" @@ -513,46 +361,30 @@ export const ChatViewList: React.FC<IChatViewListProps> = (options: IChatViewLis flexDirection="column" justifyContent="start" width="100%" - ref={chatContainerRef} blur={initialized.isHidden} > {messages && messages?.map((chat: IMessageIPFS, index: number) => { - // If message is a reaction, then skip it - if (chat?.messageType === 'Reaction') return null; - const dateNum = moment(chat.timestamp).format('L'); // TODO: This is a hack as chat.fromDID is converted with eip to match with user.account creating a bug for omnichain const position = pCAIP10ToWallet(chat.fromDID)?.toLowerCase() !== pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() ? 0 : 1; - - // define zIndex, really big number minus 1 - const uid = `${999999999 - index}`; - return ( <> - {dates.has(dateNum) ? null : renderDate({ chat, dateNum, uid: uid })} + {dates.has(dateNum) ? null : renderDate({ chat, dateNum })} <Section justifyContent={position ? 'end' : 'start'} - key={`section-${user?.uid}-${uid}-${index}`} - zIndex={uid} margin={ position ? theme.margin?.chatBubbleSenderMargin : theme.margin?.chatBubbleReceiverMargin } + key={index} > - {/* TODO: Remove decryptedMessagePayload in v2 component */} <ChatViewBubble - key={`chatbubble-${user?.uid}-${uid}-${index}`} decryptedMessagePayload={chat} - chatPayload={chat} - chatReactions={reactions[(chat as any).cid] || []} - showChatMeta={initialized.chatInfo?.meta?.group ?? false} - chatId={chatId} - actionId={(chat as any).cid} - singularActionId={singularActionId} - setSingularActionId={setSingularActionId} + key={index} + isGroup={initialized.chatInfo?.meta?.group ?? false} /> </Section> </> @@ -582,8 +414,9 @@ const ChatViewListCard = styled(Section)<IThemeProps>` } overscroll-behavior: contain; + scroll-behavior: smooth; `; const ChatViewListCardInner = styled(Section)<IThemeProps>` - filter: ${(props) => (props.blur ? 'blur(6px)' : 'none')}; + filter: ${(props) => (props.blur ? 'blur(12px)' : 'none')}; `; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewList/MessageEncryption.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/MessageEncryption.tsx index af70cfd80..3cc213f63 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/MessageEncryption.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/MessageEncryption.tsx @@ -68,6 +68,7 @@ export const EncryptionMessage = ({ id, className }: { id: EncryptionKeys; class fontWeight="400" textAlign="left" className={className} + animation={theme.skeletonBG} > {EncryptionMessageContent[id].text} </Span> diff --git a/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx b/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx index 795bc74fa..06caed88d 100644 --- a/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx +++ b/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx @@ -16,7 +16,7 @@ import { device } from '../../../config'; import { CopyIcon } from '../../../icons/PushIcons'; // Interfaces & Types -import { IChatTheme } from '../theme'; +import { IChatTheme, getCustomTheme } from '../theme'; type ProfileProps = { theme: IChatTheme; member: { @@ -62,6 +62,7 @@ export const ProfileContainer = ({ theme, member, copy, customStyle, loading }: } }, [member?.recipient, member?.icon]); + return ( <Section justifyContent="flex-start"> <Section @@ -69,6 +70,7 @@ export const ProfileContainer = ({ theme, member, copy, customStyle, loading }: width={customStyle?.imgHeight ?? '48px'} margin="0px 12px 0px 0px" position="relative" + animation={theme.skeletonBG} flex="none" borderRadius="100%" overflow="hidden" @@ -130,6 +132,7 @@ export const ProfileContainer = ({ theme, member, copy, customStyle, loading }: copyToClipboard(pCAIP10ToWallet(member?.recipient || '')); setCopyText('Copied'); }} + animation={theme.skeletonBG} className={loading ? 'skeleton' : ''} > <RecipientSpan diff --git a/packages/uiweb/src/lib/components/chat/theme/index.ts b/packages/uiweb/src/lib/components/chat/theme/index.ts index a45dbaef3..274a28356 100644 --- a/packages/uiweb/src/lib/components/chat/theme/index.ts +++ b/packages/uiweb/src/lib/components/chat/theme/index.ts @@ -2,6 +2,7 @@ * @file theme file: all the predefined themes are defined here */ import { CHAT_THEME_OPTIONS } from '../exportedTypes'; +import styled, { keyframes, css } from 'styled-components'; // bgColorPrimary: "#fff", // bgColorSecondary: "#D53A94", // textColorPrimary: "#1e1e1e", @@ -173,6 +174,7 @@ interface IIconColor { primaryColor?: string; subtleColor?: string; } + export interface IChatTheme { borderRadius?: IBorderRadius; padding?: IPadding; @@ -191,10 +193,38 @@ export interface IChatTheme { textColor?: ITextColor; backdropFilter?: string; scrollbarColor?: string; - + skeletonBG?: any; spinnerColor?: string; } +const lightSkeletonLoading = keyframes` + 0% { + background-color: hsl(200, 20%, 80%); + } + 100% { + background-color: hsl(200, 20%, 95%); + } +`; + +const darkSkeletonLoading = keyframes` + 0% { + background-color: #575D73; + } + 100% { + background-color: #6E748B; + } +`; + +const animation = () => + css` + ${lightSkeletonLoading} 1s linear infinite alternate; +`; + +const darkAnimation = () => + css` + ${darkSkeletonLoading} 1s linear infinite alternate; +`; + //dark theme object export const lightChatTheme: IChatTheme = { borderRadius: { @@ -368,6 +398,7 @@ export const lightChatTheme: IChatTheme = { backdropFilter: 'none', spinnerColor: 'rgb(202, 89, 155)', scrollbarColor: 'rgb(202, 89, 155)', + skeletonBG: animation }; export const darkChatTheme: IChatTheme = { @@ -541,4 +572,5 @@ export const darkChatTheme: IChatTheme = { backdropFilter: 'none', spinnerColor: 'rgb(202, 89, 155)', scrollbarColor: 'rgb(202, 89, 155)', + skeletonBG: darkAnimation }; diff --git a/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx b/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx index 806fb13b3..ad35db531 100644 --- a/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx +++ b/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx @@ -1,14 +1,5 @@ -import styled, { keyframes } from 'styled-components'; +import styled from 'styled-components'; -// Define keyframes -const skeletonLoading = keyframes` - 0% { - background-color: hsl(200, 20%, 80%); - } - 100% { - background-color: hsl(200, 20%, 95%); - } -`; // Define types and export components type SectionStyleProps = { @@ -44,6 +35,7 @@ type SectionStyleProps = { whiteSpace?: string; visibility?: string; zIndex?: string; + animation?: string; fontSize?: string; }; @@ -84,7 +76,7 @@ export const Section = styled.div<SectionStyleProps>` &.skeleton { > * { - visibility: ${(props) => (props.visibility || skeletonLoading ? 'hidden' : 'visible')}; + visibility: ${(props) => (props.visibility || props.animation ? 'hidden' : 'visible')}; } &:after { @@ -95,7 +87,7 @@ export const Section = styled.div<SectionStyleProps>` left: 0; right: 0; z-index: 1; - animation: ${skeletonLoading} 1s linear infinite alternate; + animation: ${(props) => props.animation}; border-radius: 8px; } } @@ -112,6 +104,7 @@ type DivStyleProps = { borderRadius?: string; textAlign?: string; visibility?: string; + animation?: string; }; export const Div = styled.div<DivStyleProps>` height: ${(props) => props.height || 'auto'}; @@ -126,7 +119,7 @@ export const Div = styled.div<DivStyleProps>` &.skeleton { > * { - visibility: ${(props) => (props.visibility || skeletonLoading ? 'hidden' : 'visible')}; + visibility: ${(props) => (props.visibility || props.animation ? 'hidden' : 'visible')}; } &:after { @@ -138,7 +131,7 @@ export const Div = styled.div<DivStyleProps>` right: 0; opacity: 1; z-index: 1; - animation: ${skeletonLoading} 1s linear infinite alternate; + animation: ${(props) => props.animation}; border-radius: 8px; } } @@ -169,6 +162,7 @@ type SpanStyleProps = { cursor?: string; whiteSpace?: string; visibility?: string; + animation?: string; textWrap?: string; }; @@ -201,7 +195,7 @@ export const Span = styled.span<SpanStyleProps>` &.skeleton { > * { - visibility: ${(props) => (props.visibility || skeletonLoading ? 'hidden' : 'visible')}; + visibility: ${(props) => (props.visibility || props.animation ? 'hidden' : 'visible')}; } &:after { @@ -213,7 +207,7 @@ export const Span = styled.span<SpanStyleProps>` right: 0; opacity: 1; z-index: 1; - animation: ${skeletonLoading} 1s linear infinite alternate; + animation: ${(props) => props.animation}; border-radius: 8px; } }