diff --git a/src/components/ScrollThreshold.jsx b/src/components/ScrollThreshold.jsx
new file mode 100644
index 00000000..be8272b5
--- /dev/null
+++ b/src/components/ScrollThreshold.jsx
@@ -0,0 +1,41 @@
+import React, {
+ useEffect,
+ useRef,
+} from 'react';
+import PropTypes from 'prop-types';
+
+function ScrollThreshold({ onScroll }) {
+ const elementRef = useRef(null);
+
+ useEffect(() => {
+ if (!elementRef.current) {
+ return undefined;
+ }
+
+ // create the observer
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ onScroll();
+ }
+ },
+ );
+
+ observer.observe(elementRef.current);
+
+ // cleanup callback
+ return () => {
+ observer.disconnect();
+ };
+ }, [elementRef]);
+
+ return (
+
+ );
+}
+
+ScrollThreshold.propTypes = {
+ onScroll: PropTypes.func.isRequired,
+};
+
+export default ScrollThreshold;
diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx
index b6eb369a..0ad2730c 100644
--- a/src/discussions/posts/PostsView.jsx
+++ b/src/discussions/posts/PostsView.jsx
@@ -7,10 +7,12 @@ import { useParams } from 'react-router';
import { AppContext } from '@edx/frontend-platform/react';
import { Spinner } from '@edx/paragon';
+import ScrollThreshold from '../../components/ScrollThreshold';
import { RequestStatus } from '../../data/constants';
import {
selectAllThreads,
selectThreadFilters,
+ selectThreadNextPage,
selectThreadSorting,
selectTopicThreads,
selectUserThreads,
@@ -30,6 +32,7 @@ function PostsView({ showOwnPosts }) {
const { authenticatedUser } = useContext(AppContext);
const orderBy = useSelector(selectThreadSorting());
const filters = useSelector(selectThreadFilters());
+ const nextPage = useSelector(selectThreadNextPage());
const loadingStatus = useSelector(threadsLoadingStatus());
let posts = [];
@@ -48,19 +51,33 @@ function PostsView({ showOwnPosts }) {
}));
}, [courseId, orderBy, filters]);
+ const loadMorePosts = async () => {
+ if (nextPage) {
+ dispatch(fetchThreads(courseId, {
+ orderBy,
+ filters,
+ page: nextPage,
+ }));
+ }
+ };
+
return (
- {posts && posts.length > 0 && (
-
- {posts.map(post => (
))}
-
- )}
- {loadingStatus === RequestStatus.IN_PROGRESS && (
-
-
-
- )}
+
+ {posts && posts.map(post => (
+
+ ))}
+ {loadingStatus === RequestStatus.IN_PROGRESS ? (
+
+
+
+ ) : (
+ nextPage && (
+
+ )
+ )}
+
);
}
diff --git a/src/discussions/posts/data/redux.test.js b/src/discussions/posts/data/redux.test.js
index 0e1d7148..39738764 100644
--- a/src/discussions/posts/data/redux.test.js
+++ b/src/discussions/posts/data/redux.test.js
@@ -58,6 +58,35 @@ describe('Threads/Posts data layer tests', () => {
.toEqual('test-topic');
});
+ test('successfully processes threads pagination', async () => {
+ const mockPage = page => axiosMock
+ .onGet(threadsApiUrl)
+ .reply(200, Factory.build('threadsResult', null, {
+ page,
+ count: 5,
+ pageSize: 3,
+ }));
+
+ mockPage(1);
+ await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
+ expect(store.getState().threads.pages)
+ .toEqual([
+ ['thread-1', 'thread-2', 'thread-3'],
+ ]);
+ expect(store.getState().threads.nextPage)
+ .toEqual(2);
+
+ mockPage(2);
+ await executeThunk(fetchThreads(courseId, { page: 2 }), store.dispatch, store.getState);
+ expect(store.getState().threads.pages)
+ .toEqual([
+ ['thread-1', 'thread-2', 'thread-3'],
+ ['thread-4', 'thread-5'],
+ ]);
+ expect(store.getState().threads.nextPage)
+ .toBeNull();
+ });
+
test('successfully processes single thread', async () => {
const threadId = 'thread-1';
axiosMock.onGet(`${threadsApiUrl}${threadId}/`)
diff --git a/src/discussions/posts/data/selectors.js b/src/discussions/posts/data/selectors.js
index 2f8098c6..ed110d9f 100644
--- a/src/discussions/posts/data/selectors.js
+++ b/src/discussions/posts/data/selectors.js
@@ -47,6 +47,8 @@ export const selectThreadSorting = () => state => state.threads.sortedBy;
export const selectThreadFilters = () => state => state.threads.filters;
+export const selectThreadNextPage = () => state => state.threads.nextPage;
+
export const selectAuthorAvatars = author => state => (
state.threads.avatars?.[author]?.profile.image
);
diff --git a/src/discussions/posts/data/slices.js b/src/discussions/posts/data/slices.js
index 8f402d53..1d1a3bef 100644
--- a/src/discussions/posts/data/slices.js
+++ b/src/discussions/posts/data/slices.js
@@ -24,6 +24,7 @@ const threadsSlice = createSlice({
},
pages: [],
threadDraft: null,
+ nextPage: null,
totalPages: null,
totalThreads: null,
postStatus: RequestStatus.SUCCESSFUL,
@@ -47,6 +48,7 @@ const threadsSlice = createSlice({
state.threadsById = { ...state.threadsById, ...payload.threadsById };
state.threadsInTopic = { ...state.threadsInTopic, ...payload.threadsInTopic };
state.avatars = { ...state.avatars, ...payload.avatars };
+ state.nextPage = (payload.page < payload.pagination.numPages) ? payload.page + 1 : null;
state.totalPages = payload.pagination.numPages;
state.totalThreads = payload.pagination.count;
},