diff --git a/apps/desktop/src/components/AccountDrawer/AccountDrawerDisplay.test.tsx b/apps/desktop/src/components/AccountDrawer/AccountDrawerDisplay.test.tsx
index b6a26271d6..75a4d8f0d1 100644
--- a/apps/desktop/src/components/AccountDrawer/AccountDrawerDisplay.test.tsx
+++ b/apps/desktop/src/components/AccountDrawer/AccountDrawerDisplay.test.tsx
@@ -51,6 +51,7 @@ beforeEach(() => {
isFirstLoad: false,
isLoading: false,
loadMore: jest.fn(),
+ triggerRef: { current: null },
});
store.dispatch(networksActions.setCurrent(MAINNET));
addTestAccount(store, selectedAccount);
@@ -300,6 +301,7 @@ describe("", () => {
isFirstLoad: false,
isLoading: false,
loadMore: jest.fn(),
+ triggerRef: { current: null },
});
multisigsFixture.forEach(account => addTestAccount(store, account));
});
diff --git a/apps/desktop/src/setupTests.tsx b/apps/desktop/src/setupTests.tsx
index ba167fbc8b..0471f70d91 100644
--- a/apps/desktop/src/setupTests.tsx
+++ b/apps/desktop/src/setupTests.tsx
@@ -24,6 +24,22 @@ import {
MockDate.set("2023-03-27T14:15:09.760Z");
+const mockIntersectionObserver = class MockIntersectionObserver {
+ callback: jest.Mock;
+ options: jest.Mock;
+ observe: jest.Mock;
+ unobserve: jest.Mock;
+ disconnect: jest.Mock;
+
+ constructor(callback: jest.Mock, options: jest.Mock) {
+ this.callback = callback;
+ this.options = options;
+ this.observe = jest.fn();
+ this.unobserve = jest.fn();
+ this.disconnect = jest.fn();
+ }
+};
+
jest.mock("./env", () => ({ IS_DEV: false }));
beforeEach(() => {
@@ -32,6 +48,7 @@ beforeEach(() => {
crypto: { value: webcrypto, writable: true },
TextDecoder: { value: TextDecoder, writable: true },
TextEncoder: { value: TextEncoder, writable: true },
+ IntersectionObserver: { value: mockIntersectionObserver, writable: true, configurable: true },
scrollTo: { value: jest.fn(), writable: true },
// taken from https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
diff --git a/apps/desktop/src/views/operations/OperationsView.tsx b/apps/desktop/src/views/operations/OperationsView.tsx
index 9306bebbe6..f718125f66 100644
--- a/apps/desktop/src/views/operations/OperationsView.tsx
+++ b/apps/desktop/src/views/operations/OperationsView.tsx
@@ -1,6 +1,5 @@
import { Box, Center, Divider, Flex, Image } from "@chakra-ui/react";
import { useGetOperations } from "@umami/data-polling";
-import { type UIEvent, useRef } from "react";
import { NoOperations } from "../../components/NoItems";
import { OperationTile, OperationTileContext } from "../../components/OperationTile";
@@ -10,26 +9,7 @@ import colors from "../../style/colors";
export const OperationsView = () => {
const { accountsFilter, selectedAccounts } = useAccountsFilter();
- const { operations, loadMore, hasMore, isLoading, isFirstLoad } =
- useGetOperations(selectedAccounts);
- // used to run loadMore only once when the user scrolls to the bottom
- // otherwise it might be called multiple times which would trigger multiple fetches
- const skipLoadMore = useRef(false);
-
- const onScroll = (e: UIEvent) => {
- if (skipLoadMore.current || !hasMore || isLoading) {
- return;
- }
- const element = e.target as HTMLDivElement;
-
- // start loading earlier than we reached the end of the list
- if (element.scrollHeight - element.scrollTop - element.clientHeight < 100) {
- skipLoadMore.current = true;
- return loadMore().finally(() => {
- skipLoadMore.current = false;
- });
- }
- };
+ const { operations, isLoading, isFirstLoad, triggerRef } = useGetOperations(selectedAccounts);
return (
@@ -52,7 +32,6 @@ export const OperationsView = () => {
marginBottom="20px"
background={colors.gray[900]}
borderRadius="8px"
- onScroll={onScroll}
paddingX="20px"
>
@@ -75,6 +54,7 @@ export const OperationsView = () => {
);
})}
+
diff --git a/apps/web/src/setupTests.ts b/apps/web/src/setupTests.ts
index 564d727959..d5fffcd127 100644
--- a/apps/web/src/setupTests.ts
+++ b/apps/web/src/setupTests.ts
@@ -6,10 +6,21 @@ import { mockToast } from "@umami/state";
import { mockLocalStorage } from "@umami/test-utils";
import { setupJestCanvasMock } from "jest-canvas-mock";
-const intersectionObserverMock = () => ({
- observe: jest.fn(),
- unobserve: jest.fn(),
-});
+const mockIntersectionObserver = class MockIntersectionObserver {
+ callback: jest.Mock;
+ options: jest.Mock;
+ observe: jest.Mock;
+ unobserve: jest.Mock;
+ disconnect: jest.Mock;
+
+ constructor(callback: jest.Mock, options: jest.Mock) {
+ this.callback = callback;
+ this.options = options;
+ this.observe = jest.fn();
+ this.unobserve = jest.fn();
+ this.disconnect = jest.fn();
+ }
+};
jest.mock("./env", () => ({ IS_DEV: false }));
@@ -23,7 +34,7 @@ Object.defineProperties(global, {
crypto: { value: webcrypto, writable: true },
TextDecoder: { value: TextDecoder, writable: true },
TextEncoder: { value: TextEncoder, writable: true },
- IntersectionObserver: { value: intersectionObserverMock, writable: true, configurable: true },
+ IntersectionObserver: { value: mockIntersectionObserver, writable: true, configurable: true },
});
jest.mock("./utils/persistor", () => ({
diff --git a/apps/web/src/views/Activity/Activity.tsx b/apps/web/src/views/Activity/Activity.tsx
index a0193ba092..ba6824010d 100644
--- a/apps/web/src/views/Activity/Activity.tsx
+++ b/apps/web/src/views/Activity/Activity.tsx
@@ -2,7 +2,6 @@ import { Box, Center, Divider, Flex, Image, Spinner } from "@chakra-ui/react";
import { type Account } from "@umami/core";
import { useGetOperations } from "@umami/data-polling";
import { useCurrentAccount } from "@umami/state";
-import { type UIEvent, useRef } from "react";
import loadingDots from "../../assets/loading-dots.gif";
import { EmptyMessage } from "../../components/EmptyMessage";
@@ -17,34 +16,15 @@ export const Activity = () => {
const color = useColor();
const currentAccount = useCurrentAccount();
- const { operations, loadMore, hasMore, isLoading, isFirstLoad } = useGetOperations(
+ const { operations, isLoading, isFirstLoad, triggerRef } = useGetOperations(
[currentAccount ?? ({} as Account)],
isVerified
);
const buyTezUrl = `https://widget.wert.io/default/widget/?commodity=XTZ&address=${currentAccount?.address.pkh}&network=tezos&commodity_id=xtz.simple.tezos`;
- // used to run loadMore only once when the user scrolls to the bottom
- // otherwise it might be called multiple times which would trigger multiple fetches
- const skipLoadMore = useRef(false);
-
const isEmpty = operations.length === 0 && !isLoading;
- const onScroll = (e: UIEvent) => {
- if (skipLoadMore.current || !hasMore || isLoading) {
- return;
- }
- const element = e.target as HTMLDivElement;
-
- // start loading earlier than we reached the end of the list
- if (element.scrollHeight - element.scrollTop - element.clientHeight < 100) {
- skipLoadMore.current = true;
- return loadMore().finally(() => {
- skipLoadMore.current = false;
- });
- }
- };
-
return (
<>
@@ -68,7 +48,7 @@ export const Activity = () => {
)}
{operations.length > 0 && (
-
+
{operations.map((operation, i) => {
const isFirst = i === 0;
const isLast = i === operations.length - 1;
@@ -84,6 +64,8 @@ export const Activity = () => {
/>
);
})}
+ {/* trigger for loading more operations */}
+
diff --git a/packages/data-polling/src/useGetOperations.tsx b/packages/data-polling/src/useGetOperations.tsx
index bfc070ef9d..eac71f07bd 100644
--- a/packages/data-polling/src/useGetOperations.tsx
+++ b/packages/data-polling/src/useGetOperations.tsx
@@ -16,7 +16,7 @@ import {
getRelatedTokenTransfers,
} from "@umami/tzkt";
import { maxBy } from "lodash";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import { useReactQueryErrorHandler } from "./useReactQueryErrorHandler";
import { useRefetchTrigger } from "./useRefetchTrigger";
@@ -34,6 +34,7 @@ type UseGetOperationsResult = {
isFirstLoad: boolean;
isLoading: boolean;
hasMore: boolean;
+ triggerRef: React.RefObject;
loadMore: InfiniteQueryObserverBaseResult["fetchNextPage"];
};
@@ -63,6 +64,8 @@ export const useGetOperations = (accounts: Account[], isEnabled = true): UseGetO
const refetchTrigger = useRefetchTrigger();
const handleError = useReactQueryErrorHandler();
+ const triggerRef = useRef(null);
+
const {
isFetching,
data: operations,
@@ -128,12 +131,36 @@ export const useGetOperations = (accounts: Account[], isEnabled = true): UseGetO
void fetchPreviousPage();
}, [refetchTrigger, fetchPreviousPage, isEnabled]);
+ useEffect(() => {
+ if (!isEnabled || !triggerRef.current) {
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ entries => {
+ if (entries[0].isIntersecting && hasNextPage && !isLoading) {
+ void fetchNextPage();
+ }
+ },
+ {
+ root: null,
+ rootMargin: "100px",
+ threshold: 0,
+ }
+ );
+
+ observer.observe(triggerRef.current);
+
+ return () => observer.disconnect();
+ }, [hasNextPage, isLoading, fetchNextPage, isEnabled]);
+
return {
+ hasMore: hasNextPage,
operations: operations || [],
isFirstLoad: isLoading,
isLoading: isFetching,
- hasMore: hasNextPage,
loadMore: fetchNextPage,
+ triggerRef,
};
};