From 0e4d7ec21a13f6f2cd5dbaf6062e833ac876917a Mon Sep 17 00:00:00 2001 From: Audric Ackermann <audric@loki.network> Date: Fri, 16 Jul 2021 09:34:32 +1000 Subject: [PATCH] WIP --- package.json | 1 + .../conversation/SessionMessagesList.tsx | 177 +++++++++--------- ts/state/ducks/conversations.ts | 25 +++ ts/state/selectors/conversations.ts | 6 + yarn.lock | 5 + 5 files changed, 123 insertions(+), 91 deletions(-) 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 ( + <GroupUpdateItem + key={messageProps.propsForMessage.id} + groupNotificationProps={groupNotificationProps} + messageId={messageProps.propsForMessage.id} + showUnreadIndicator={showUnreadIndicator} + /> + ); + } + + if (propsForGroupInvitation) { + return ( + <GroupInvitationItem + key={messageProps.propsForMessage.id} + propsForGroupInvitation={propsForGroupInvitation} + messageId={messageProps.propsForMessage.id} + showUnreadIndicator={showUnreadIndicator} + /> + ); + } + + if (propsForDataExtractionNotification) { + return ( + <DataExtractionNotificationItem + key={messageProps.propsForMessage.id} + propsForDataExtractionNotification={propsForDataExtractionNotification} + messageId={messageProps.propsForMessage.id} + showUnreadIndicator={showUnreadIndicator} + /> + ); + } + + if (timerProps) { + return ( + <TimerNotificationItem + key={messageProps.propsForMessage.id} + timerProps={timerProps} + messageId={messageProps.propsForMessage.id} + showUnreadIndicator={showUnreadIndicator} + /> + ); + } + + 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 ( + <GenericMessageItem + key={messageProps.propsForMessage.id} + playableMessageIndex={playableMessageIndex} + messageId={messageProps.propsForMessage.id} + messageProps={messageProps} + showUnreadIndicator={showUnreadIndicator} + /> + ); + })} + </> + ); +}; + class SessionMessagesListInner extends React.Component<Props, State> { private scrollOffsetBottomPx: number = Number.MAX_VALUE; private ignoreScrollEvents: boolean; @@ -264,7 +345,7 @@ class SessionMessagesListInner extends React.Component<Props, State> { key="typing-bubble" /> - {this.renderMessages()} + <MessageList /> <SessionScrollButton show={showScrollButton} @@ -275,92 +356,6 @@ class SessionMessagesListInner extends React.Component<Props, State> { ); } - 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 ( - <GroupUpdateItem - key={messageProps.propsForMessage.id} - groupNotificationProps={groupNotificationProps} - messageId={messageProps.propsForMessage.id} - showUnreadIndicator={showUnreadIndicator} - /> - ); - } - - if (propsForGroupInvitation) { - return ( - <GroupInvitationItem - key={messageProps.propsForMessage.id} - propsForGroupInvitation={propsForGroupInvitation} - messageId={messageProps.propsForMessage.id} - showUnreadIndicator={showUnreadIndicator} - /> - ); - } - - if (propsForDataExtractionNotification) { - return ( - <DataExtractionNotificationItem - key={messageProps.propsForMessage.id} - propsForDataExtractionNotification={propsForDataExtractionNotification} - messageId={messageProps.propsForMessage.id} - showUnreadIndicator={showUnreadIndicator} - /> - ); - } - - if (timerProps) { - return ( - <TimerNotificationItem - key={messageProps.propsForMessage.id} - timerProps={timerProps} - messageId={messageProps.propsForMessage.id} - showUnreadIndicator={showUnreadIndicator} - /> - ); - } - - 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 ( - <GenericMessageItem - key={messageProps.propsForMessage.id} - playableMessageIndex={playableMessageIndex} - messageId={messageProps.propsForMessage.id} - messageProps={messageProps} - showUnreadIndicator={showUnreadIndicator} - /> - ); - })} - </> - ); - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~ 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<string>; 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"