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"