diff --git a/package.json b/package.json index eba0d1e02..2e2869638 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "react-toastify": "^6.0.9", "react-use": "^17.2.1", "react-virtualized": "9.22.3", + "react-window-infinite-loader": "^1.0.7", "read-last-lines": "1.3.0", "redux": "4.0.1", "redux-logger": "3.0.6", diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 23e77590c..a23816317 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -7,7 +7,6 @@ import { SessionScrollButton } from '../SessionScrollButton'; import { Constants } from '../../../session'; import _ from 'lodash'; import { contextMenu } from 'react-contexify'; -import { AttachmentType, AttachmentTypeWithPath } from '../../../types/Attachment'; import { GroupNotification } from '../../conversation/GroupNotification'; import { GroupInvitation } from '../../conversation/GroupInvitation'; import { @@ -15,7 +14,6 @@ import { PropsForExpirationTimer, PropsForGroupInvitation, PropsForGroupUpdate, - PropsForMessage, ReduxConversationType, SortedMessageModelProps, } from '../../../state/ducks/conversations'; @@ -39,10 +37,8 @@ import { getMessagesOfSelectedConversation, getSelectedConversation, getSelectedConversationKey, - getSelectedMessageIds, isMessageSelectionMode, } from '../../../state/selectors/conversations'; -import { saveAttachmentToDisk } from '../../../util/attachmentsUtil'; interface State { showScrollButton: boolean; @@ -164,6 +160,91 @@ const GenericMessageItem = (props: { ); }; +const MessageList = ({ hasNextPage: boolean, isNextPageLoading, list, loadNextPage }) => { + const messagesProps = useSelector(getMessagesOfSelectedConversation); + let playableMessageIndex = 0; + + return ( + <> + {messagesProps.map((messageProps: SortedMessageModelProps) => { + const timerProps = messageProps.propsForTimerNotification; + const propsForGroupInvitation = messageProps.propsForGroupInvitation; + const propsForDataExtractionNotification = messageProps.propsForDataExtractionNotification; + + const groupNotificationProps = messageProps.propsForGroupNotification; + + // IF we found the first unread message + // AND we are not scrolled all the way to the bottom + // THEN, show the unread banner for the current message + const showUnreadIndicator = Boolean(messageProps.firstUnread); + console.warn('&& this.getScrollOffsetBottomPx() !== 0'); + + if (groupNotificationProps) { + return ( + + ); + } + + if (propsForGroupInvitation) { + return ( + + ); + } + + if (propsForDataExtractionNotification) { + return ( + + ); + } + + if (timerProps) { + return ( + + ); + } + + if (!messageProps) { + return; + } + + playableMessageIndex++; + + // firstMessageOfSeries tells us to render the avatar only for the first message + // in a series of messages from the same user + return ( + + ); + })} + + ); +}; + class SessionMessagesListInner extends React.Component { private scrollOffsetBottomPx: number = Number.MAX_VALUE; private ignoreScrollEvents: boolean; @@ -264,7 +345,7 @@ class SessionMessagesListInner extends React.Component { key="typing-bubble" /> - {this.renderMessages()} + { ); } - private renderMessages() { - const { messagesProps } = this.props; - let playableMessageIndex = 0; - - return ( - <> - {messagesProps.map((messageProps: SortedMessageModelProps) => { - const timerProps = messageProps.propsForTimerNotification; - const propsForGroupInvitation = messageProps.propsForGroupInvitation; - const propsForDataExtractionNotification = - messageProps.propsForDataExtractionNotification; - - const groupNotificationProps = messageProps.propsForGroupNotification; - - // IF we found the first unread message - // AND we are not scrolled all the way to the bottom - // THEN, show the unread banner for the current message - const showUnreadIndicator = - Boolean(messageProps.firstUnread) && this.getScrollOffsetBottomPx() !== 0; - - if (groupNotificationProps) { - return ( - - ); - } - - if (propsForGroupInvitation) { - return ( - - ); - } - - if (propsForDataExtractionNotification) { - return ( - - ); - } - - if (timerProps) { - return ( - - ); - } - - if (!messageProps) { - return; - } - - playableMessageIndex++; - - // firstMessageOfSeries tells us to render the avatar only for the first message - // in a series of messages from the same user - return ( - - ); - })} - - ); - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 962989bfe..98c2b828d 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -238,6 +238,7 @@ export type ConversationsStateType = { selectedMessageIds: Array; lightBox?: LightBoxOptions; quotedMessage?: ReplyingToMessageProps; + areMoreMessagesBeingFetched: boolean; }; async function getMessages( @@ -392,6 +393,7 @@ function getEmptyState(): ConversationsStateType { messageDetailProps: undefined, showRightPanel: false, selectedMessageIds: [], + areMoreMessagesBeingFetched: false, }; } @@ -742,11 +744,34 @@ const conversationsSlice = createSlice({ return { ...state, messages: messagesProps, + areMoreMessagesBeingFetched: false, }; } return state; } ); + builder.addCase( + fetchMessagesForConversation.fulfilled, + (state: ConversationsStateType, action: any) => { + // this is called once the messages are loaded from the db for the currently selected conversation + const { messagesProps, conversationKey } = action.payload as FetchedMessageResults; + // double check that this update is for the shown convo + if (conversationKey === state.selectedConversation) { + return { + ...state, + messages: messagesProps, + areMoreMessagesBeingFetched: false, + }; + } + return state; + } + ); + builder.addCase(fetchMessagesForConversation.pending, (state: ConversationsStateType) => { + state.areMoreMessagesBeingFetched = true; + }); + builder.addCase(fetchMessagesForConversation.rejected, (state: ConversationsStateType) => { + state.areMoreMessagesBeingFetched = false; + }); }, }); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 3b04be7b8..c130815da 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -19,6 +19,7 @@ import { } from '../../components/conversation/ConversationHeader'; import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox'; +import { createSlice } from '@reduxjs/toolkit'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -295,3 +296,8 @@ export const getQuotedMessage = createSelector( getConversations, (state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage ); + + +export const areMoreMessagesLoading = createSlice(getConversations, + (state: ConversationsStateType): boolean => state. +); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 456a3404b..7f71618c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7931,6 +7931,11 @@ react-virtualized@9.22.3: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-window-infinite-loader@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/react-window-infinite-loader/-/react-window-infinite-loader-1.0.7.tgz#958ef1a689d20dce122ef377583acd987760aee8" + integrity sha512-wg3LWkUpG21lhv+cZvNy+p0+vtclZw+9nP2vO6T9PKT50EN1cUq37Dq6FzcM38h/c2domE0gsUhb6jHXtGogAA== + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"