You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			195 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			195 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
| import { debounce, noop } from 'lodash';
 | |
| import React, { useCallback, useContext, useLayoutEffect, useState } from 'react';
 | |
| import { InView } from 'react-intersection-observer';
 | |
| import { useDispatch, useSelector } from 'react-redux';
 | |
| import { Data } from '../../../../data/data';
 | |
| import { getConversationController } from '../../../../session/conversations';
 | |
| import {
 | |
|   fetchBottomMessagesForConversation,
 | |
|   fetchTopMessagesForConversation,
 | |
|   markConversationFullyRead,
 | |
|   showScrollToBottomButton,
 | |
| } from '../../../../state/ducks/conversations';
 | |
| import {
 | |
|   areMoreMessagesBeingFetched,
 | |
|   getConversationHasUnread,
 | |
|   getLoadedMessagesLength,
 | |
|   getMostRecentMessageId,
 | |
|   getOldestMessageId,
 | |
|   getQuotedMessageToAnimate,
 | |
|   getSelectedConversationKey,
 | |
|   getShowScrollButton,
 | |
|   getYoungestMessageId,
 | |
| } from '../../../../state/selectors/conversations';
 | |
| import { getIsAppFocused } from '../../../../state/selectors/section';
 | |
| import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer';
 | |
| 
 | |
| type ReadableMessageProps = {
 | |
|   children: React.ReactNode;
 | |
|   messageId: string;
 | |
|   className?: string;
 | |
|   receivedAt: number | undefined;
 | |
|   isUnread: boolean;
 | |
|   onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
 | |
| };
 | |
| 
 | |
| const debouncedTriggerLoadMoreTop = debounce(
 | |
|   (selectedConversationKey: string, oldestMessageId: string) => {
 | |
|     (window.inboxStore?.dispatch as any)(
 | |
|       fetchTopMessagesForConversation({
 | |
|         conversationKey: selectedConversationKey,
 | |
|         oldTopMessageId: oldestMessageId,
 | |
|       })
 | |
|     );
 | |
|   },
 | |
|   100
 | |
| );
 | |
| 
 | |
| const debouncedTriggerLoadMoreBottom = debounce(
 | |
|   (selectedConversationKey: string, youngestMessageId: string) => {
 | |
|     (window.inboxStore?.dispatch as any)(
 | |
|       fetchBottomMessagesForConversation({
 | |
|         conversationKey: selectedConversationKey,
 | |
|         oldBottomMessageId: youngestMessageId,
 | |
|       })
 | |
|     );
 | |
|   },
 | |
|   100
 | |
| );
 | |
| 
 | |
| export const ReadableMessage = (props: ReadableMessageProps) => {
 | |
|   const { messageId, onContextMenu, className, receivedAt, isUnread } = props;
 | |
| 
 | |
|   const isAppFocused = useSelector(getIsAppFocused);
 | |
|   const dispatch = useDispatch();
 | |
| 
 | |
|   const selectedConversationKey = useSelector(getSelectedConversationKey);
 | |
|   const loadedMessagesLength = useSelector(getLoadedMessagesLength);
 | |
|   const mostRecentMessageId = useSelector(getMostRecentMessageId);
 | |
|   const oldestMessageId = useSelector(getOldestMessageId);
 | |
|   const youngestMessageId = useSelector(getYoungestMessageId);
 | |
|   const fetchingMoreInProgress = useSelector(areMoreMessagesBeingFetched);
 | |
|   const conversationHasUnread = useSelector(getConversationHasUnread);
 | |
|   const scrollButtonVisible = useSelector(getShowScrollButton);
 | |
|   const shouldMarkReadWhenVisible = isUnread;
 | |
| 
 | |
|   const [didScroll, setDidScroll] = useState(false);
 | |
|   const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
 | |
| 
 | |
|   const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext);
 | |
| 
 | |
|   // if this unread-indicator is rendered,
 | |
|   // we want to scroll here only if the conversation was not opened to a specific message
 | |
| 
 | |
|   useLayoutEffect(() => {
 | |
|     if (
 | |
|       props.messageId === youngestMessageId &&
 | |
|       !quotedMessageToAnimate &&
 | |
|       !scrollButtonVisible &&
 | |
|       !didScroll &&
 | |
|       !conversationHasUnread
 | |
|     ) {
 | |
|       scrollToLoadedMessage(props.messageId, 'go-to-bottom');
 | |
|       setDidScroll(true);
 | |
|     } else if (quotedMessageToAnimate) {
 | |
|       setDidScroll(true);
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const onVisible = useCallback(
 | |
|     // tslint:disable-next-line: cyclomatic-complexity
 | |
|     async (inView: boolean | Object) => {
 | |
|       // we are the most recent message
 | |
|       if (mostRecentMessageId === messageId && selectedConversationKey) {
 | |
|         // make sure the app is focused, because we mark message as read here
 | |
|         if (inView === true && isAppFocused) {
 | |
|           dispatch(showScrollToBottomButton(false));
 | |
|           getConversationController()
 | |
|             .get(selectedConversationKey)
 | |
|             ?.markRead(receivedAt || 0);
 | |
| 
 | |
|           dispatch(markConversationFullyRead(selectedConversationKey));
 | |
|         } else if (inView === false) {
 | |
|           dispatch(showScrollToBottomButton(true));
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         inView === true &&
 | |
|         isAppFocused &&
 | |
|         oldestMessageId === messageId &&
 | |
|         !fetchingMoreInProgress &&
 | |
|         selectedConversationKey
 | |
|       ) {
 | |
|         debouncedTriggerLoadMoreTop(selectedConversationKey, oldestMessageId);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         inView === true &&
 | |
|         isAppFocused &&
 | |
|         youngestMessageId === messageId &&
 | |
|         !fetchingMoreInProgress &&
 | |
|         selectedConversationKey
 | |
|       ) {
 | |
|         debouncedTriggerLoadMoreBottom(selectedConversationKey, youngestMessageId);
 | |
|       }
 | |
| 
 | |
|       // this part is just handling the marking of the message as read if needed
 | |
|       if (
 | |
|         (inView === true ||
 | |
|           ((inView as any).type === 'focus' && (inView as any).returnValue === true)) &&
 | |
|         isAppFocused
 | |
|       ) {
 | |
|         if (shouldMarkReadWhenVisible) {
 | |
|           const found = await Data.getMessageById(messageId);
 | |
| 
 | |
|           if (found && Boolean(found.get('unread'))) {
 | |
|             const foundSentAt = found.get('sent_at');
 | |
|             // mark the message as read.
 | |
|             // this will trigger the expire timer.
 | |
|             await found.markRead(Date.now());
 | |
| 
 | |
|             // we should stack those and send them in a single message once every 5secs or something.
 | |
|             // this would be part of an redesign of the sending pipeline
 | |
|             if (foundSentAt && selectedConversationKey) {
 | |
|               void getConversationController()
 | |
|                 .get(selectedConversationKey)
 | |
|                 ?.sendReadReceiptsIfNeeded([foundSentAt]);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     },
 | |
|     [
 | |
|       selectedConversationKey,
 | |
|       mostRecentMessageId,
 | |
|       oldestMessageId,
 | |
|       fetchingMoreInProgress,
 | |
|       isAppFocused,
 | |
|       loadedMessagesLength,
 | |
|       receivedAt,
 | |
|       shouldMarkReadWhenVisible,
 | |
|       messageId,
 | |
|     ]
 | |
|   );
 | |
| 
 | |
|   return (
 | |
|     // tslint:disable-next-line: use-simple-attributes
 | |
|     <InView
 | |
|       id={`msg-${messageId}`}
 | |
|       onContextMenu={onContextMenu}
 | |
|       className={className}
 | |
|       as="div"
 | |
|       threshold={0.8}
 | |
|       delay={isAppFocused ? 100 : 200}
 | |
|       onChange={isAppFocused ? onVisible : noop}
 | |
|       triggerOnce={false}
 | |
|       trackVisibility={true}
 | |
|       key={`inview-msg-${messageId}`}
 | |
|       data-testid="readable-message"
 | |
|     >
 | |
|       {props.children}
 | |
|     </InView>
 | |
|   );
 | |
| };
 |