Adding cursor pagination. Fixes #2155 (#2173)

* Adding cursor pagination. Fixes #2155

* Addressing PR comments.

---------

Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>
This commit is contained in:
Dessalines 2023-10-06 09:08:55 -04:00 committed by GitHub
parent ae4c37ed44
commit 2c1f4538be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 76 deletions

View file

@ -20,21 +20,26 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"]
server.use(express.json());
server.use(express.urlencoded({ extended: false }));
server.use(
getStaticDir(),
express.static(path.resolve("./dist"), {
maxAge: 24 * 60 * 60 * 1000, // 1 day
immutable: true,
}),
);
server.use(setCacheControl);
const serverPath = path.resolve("./dist");
if (
!process.env["LEMMY_UI_DISABLE_CSP"] &&
!process.env["LEMMY_UI_DEBUG"] &&
process.env["NODE_ENV"] !== "development"
) {
server.use(
getStaticDir(),
express.static(serverPath, {
maxAge: 24 * 60 * 60 * 1000, // 1 day
immutable: true,
}),
);
server.use(setDefaultCsp);
server.use(setCacheControl);
} else {
// In debug mode, don't use the maxAge and immutable, or it breaks live reload for dev
server.use(getStaticDir(), express.static(serverPath));
}
server.get("/.well-known/security.txt", SecurityHandler);

View file

@ -0,0 +1,46 @@
import { Component, linkEvent } from "inferno";
import { I18NextService } from "../../services";
import { PaginationCursor } from "lemmy-js-client";
interface PaginatorCursorProps {
prevPage?: PaginationCursor;
nextPage?: PaginationCursor;
onNext(val: PaginationCursor): void;
onPrev(): void;
}
function handlePrev(i: PaginatorCursor) {
i.props.onPrev();
}
function handleNext(i: PaginatorCursor) {
if (i.props.nextPage) {
i.props.onNext(i.props.nextPage);
}
}
export class PaginatorCursor extends Component<PaginatorCursorProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<div className="paginator my-2">
<button
className="btn btn-secondary me-2"
disabled={!this.props.prevPage}
onClick={linkEvent(this, handlePrev)}
>
{I18NextService.i18n.t("prev")}
</button>
<button
className="btn btn-secondary"
onClick={linkEvent(this, handleNext)}
disabled={!this.props.nextPage}
>
{I18NextService.i18n.t("next")}
</button>
</div>
);
}
}

View file

@ -14,11 +14,7 @@ import {
updateCommunityBlock,
updatePersonBlock,
} from "@utils/app";
import {
getPageFromString,
getQueryParams,
getQueryString,
} from "@utils/helpers";
import { getQueryParams, getQueryString } from "@utils/helpers";
import type { QueryParams } from "@utils/types";
import { RouteDataResponse } from "@utils/types";
import { Component, RefObject, createRef, linkEvent } from "inferno";
@ -62,6 +58,7 @@ import {
MarkCommentReplyAsRead,
MarkPersonMentionAsRead,
MarkPostAsRead,
PaginationCursor,
PostResponse,
PurgeComment,
PurgeCommunity,
@ -96,12 +93,12 @@ import { BannerIconHeader } from "../common/banner-icon-header";
import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select";
import { Sidebar } from "../community/sidebar";
import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link";
import { PaginatorCursor } from "../common/paginator-cursor";
type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse;
@ -122,13 +119,13 @@ interface State {
interface CommunityProps {
dataType: DataType;
sort: SortType;
page: number;
pageCursor?: PaginationCursor;
}
function getCommunityQueryParams() {
return getQueryParams<CommunityProps>({
dataType: getDataTypeFromQuery,
page: getPageFromString,
pageCursor: cursor => cursor,
sort: getSortTypeFromQuery,
});
}
@ -165,7 +162,8 @@ export class Community extends Component<
this.handleSortChange = this.handleSortChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.handlePageNext = this.handlePageNext.bind(this);
this.handlePagePrev = this.handlePagePrev.bind(this);
// All of the action binds
this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this);
@ -236,7 +234,7 @@ export class Community extends Component<
static async fetchInitialData({
client,
path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
query: { dataType: urlDataType, pageCursor, sort: urlSort },
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
Promise<CommunityData>
> {
@ -251,15 +249,13 @@ export class Community extends Component<
const sort = getSortTypeFromQuery(urlSort);
const page = getPageFromString(urlPage);
let postsResponse: RequestState<GetPostsResponse> = EMPTY_REQUEST;
let commentsResponse: RequestState<GetCommentsResponse> = EMPTY_REQUEST;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
community_name: communityName,
page,
page_cursor: pageCursor,
limit: fetchLimit,
sort,
type_: "All",
@ -270,7 +266,6 @@ export class Community extends Component<
} else {
const getCommentsForm: GetComments = {
community_name: communityName,
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
type_: "All",
@ -287,6 +282,12 @@ export class Community extends Component<
};
}
get getNextPage(): PaginationCursor | undefined {
return this.state.postsRes.state === "success"
? this.state.postsRes.data.next_page
: undefined;
}
get documentTitle(): string {
const cRes = this.state.communityRes;
return cRes.state === "success"
@ -295,6 +296,7 @@ export class Community extends Component<
}
renderCommunity() {
const { pageCursor } = getCommunityQueryParams();
switch (this.state.communityRes.state) {
case "loading":
return (
@ -304,7 +306,6 @@ export class Community extends Component<
);
case "success": {
const res = this.state.communityRes.data;
const { page } = getCommunityQueryParams();
return (
<>
@ -341,13 +342,11 @@ export class Community extends Component<
</div>
{this.selects(res)}
{this.listings(res)}
<Paginator
page={page}
onChange={this.handlePageChange}
nextDisabled={
this.state.postsRes.state !== "success" ||
fetchLimit > this.state.postsRes.data.posts.length
}
<PaginatorCursor
prevPage={pageCursor}
nextPage={this.getNextPage}
onNext={this.handlePageNext}
onPrev={this.handlePagePrev}
/>
</main>
<aside className="d-none d-md-block col-md-4 col-lg-3">
@ -542,18 +541,22 @@ export class Community extends Component<
);
}
handlePageChange(page: number) {
this.updateUrl({ page });
handlePagePrev() {
this.props.history.back();
}
handlePageNext(nextPage: PaginationCursor) {
this.updateUrl({ pageCursor: nextPage });
window.scrollTo(0, 0);
}
handleSortChange(sort: SortType) {
this.updateUrl({ sort, page: 1 });
this.updateUrl({ sort, pageCursor: undefined });
window.scrollTo(0, 0);
}
handleDataTypeChange(dataType: DataType) {
this.updateUrl({ dataType, page: 1 });
this.updateUrl({ dataType, pageCursor: undefined });
window.scrollTo(0, 0);
}
@ -563,16 +566,12 @@ export class Community extends Component<
}));
}
async updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
const {
dataType: urlDataType,
page: urlPage,
sort: urlSort,
} = getCommunityQueryParams();
async updateUrl({ dataType, pageCursor, sort }: Partial<CommunityProps>) {
const { dataType: urlDataType, sort: urlSort } = getCommunityQueryParams();
const queryParams: QueryParams<CommunityProps> = {
dataType: getDataTypeString(dataType ?? urlDataType),
page: (page ?? urlPage).toString(),
pageCursor: pageCursor,
sort: sort ?? urlSort,
};
@ -584,14 +583,14 @@ export class Community extends Component<
}
async fetchData() {
const { dataType, page, sort } = getCommunityQueryParams();
const { dataType, pageCursor, sort } = getCommunityQueryParams();
const { name } = this.props.match.params;
if (dataType === DataType.Post) {
this.setState({ postsRes: LOADING_REQUEST });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
page_cursor: pageCursor,
limit: fetchLimit,
sort,
type_: "All",
@ -603,7 +602,6 @@ export class Community extends Component<
this.setState({ commentsRes: LOADING_REQUEST });
this.setState({
commentsRes: await HttpService.client.getComments({
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
type_: "All",

View file

@ -14,7 +14,6 @@ import {
updatePersonBlock,
} from "@utils/app";
import {
getPageFromString,
getQueryParams,
getQueryString,
getRandomFromList,
@ -60,6 +59,7 @@ import {
MarkCommentReplyAsRead,
MarkPersonMentionAsRead,
MarkPostAsRead,
PaginationCursor,
PostResponse,
PurgeComment,
PurgeItemResponse,
@ -98,11 +98,11 @@ import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { ListingTypeSelect } from "../common/listing-type-select";
import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "../community/community-link";
import { PostListings } from "../post/post-listings";
import { SiteSidebar } from "./site-sidebar";
import { PaginatorCursor } from "../common/paginator-cursor";
interface HomeState {
postsRes: RequestState<GetPostsResponse>;
@ -123,7 +123,7 @@ interface HomeProps {
listingType?: ListingType;
dataType: DataType;
sort: SortType;
page: number;
pageCursor?: PaginationCursor;
}
type HomeData = RouteDataResponse<{
@ -185,13 +185,14 @@ function getSortTypeFromQuery(type?: string): SortType {
return (type ? (type as SortType) : mySortType) ?? "Active";
}
const getHomeQueryParams = () =>
getQueryParams<HomeProps>({
function getHomeQueryParams() {
return getQueryParams<HomeProps>({
sort: getSortTypeFromQuery,
listingType: getListingTypeFromQuery,
page: getPageFromString,
pageCursor: cursor => cursor,
dataType: getDataTypeFromQuery,
});
}
const MobileButton = ({
textKey,
@ -245,7 +246,8 @@ export class Home extends Component<any, HomeState> {
this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.handlePageNext = this.handlePageNext.bind(this);
this.handlePagePrev = this.handlePagePrev.bind(this);
this.handleCreateComment = this.handleCreateComment.bind(this);
this.handleEditComment = this.handleEditComment.bind(this);
@ -315,7 +317,7 @@ export class Home extends Component<any, HomeState> {
static async fetchInitialData({
client,
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
query: { dataType: urlDataType, listingType, pageCursor, sort: urlSort },
site,
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
const dataType = getDataTypeFromQuery(urlDataType);
@ -324,15 +326,13 @@ export class Home extends Component<any, HomeState> {
site.site_view.local_site.default_post_listing_type;
const sort = getSortTypeFromQuery(urlSort);
const page = urlPage ? Number(urlPage) : 1;
let postsRes: RequestState<GetPostsResponse> = EMPTY_REQUEST;
let commentsRes: RequestState<GetCommentsResponse> = EMPTY_REQUEST;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
type_,
page,
page_cursor: pageCursor,
limit: fetchLimit,
sort,
saved_only: false,
@ -341,7 +341,6 @@ export class Home extends Component<any, HomeState> {
postsRes = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
type_,
@ -616,18 +615,22 @@ export class Home extends Component<any, HomeState> {
);
}
async updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
async updateUrl({
dataType,
listingType,
pageCursor,
sort,
}: Partial<HomeProps>) {
const {
dataType: urlDataType,
listingType: urlListingType,
page: urlPage,
sort: urlSort,
} = getHomeQueryParams();
const queryParams: QueryParams<HomeProps> = {
dataType: getDataTypeString(dataType ?? urlDataType),
listingType: listingType ?? urlListingType,
page: (page ?? urlPage).toString(),
pageCursor: pageCursor,
sort: sort ?? urlSort,
};
@ -645,26 +648,30 @@ export class Home extends Component<any, HomeState> {
}
get posts() {
const { page } = getHomeQueryParams();
const { pageCursor } = getHomeQueryParams();
return (
<div className="main-content-wrapper">
<div>
{this.selects}
{this.listings}
<Paginator
page={page}
onChange={this.handlePageChange}
nextDisabled={
this.state.postsRes?.state !== "success" ||
fetchLimit > this.state.postsRes.data.posts.length
}
<PaginatorCursor
prevPage={pageCursor}
nextPage={this.getNextPage}
onNext={this.handlePageNext}
onPrev={this.handlePagePrev}
/>
</div>
</div>
);
}
get getNextPage(): PaginationCursor | undefined {
return this.state.postsRes.state === "success"
? this.state.postsRes.data.next_page
: undefined;
}
get listings() {
const { dataType } = getHomeQueryParams();
const siteRes = this.state.siteRes;
@ -804,7 +811,7 @@ export class Home extends Component<any, HomeState> {
}
async fetchData() {
const { dataType, page, listingType, sort } = getHomeQueryParams();
const { dataType, pageCursor, listingType, sort } = getHomeQueryParams();
if (dataType === DataType.Post) {
if (HomeCacheService.active) {
@ -814,13 +821,12 @@ export class Home extends Component<any, HomeState> {
window.scrollTo({
left: 0,
top: scrollY,
behavior: "instant",
});
} else {
this.setState({ postsRes: LOADING_REQUEST });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
page_cursor: pageCursor,
limit: fetchLimit,
sort,
saved_only: false,
@ -834,7 +840,6 @@ export class Home extends Component<any, HomeState> {
this.setState({ commentsRes: LOADING_REQUEST });
this.setState({
commentsRes: await HttpService.client.getComments({
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
saved_only: false,
@ -862,24 +867,32 @@ export class Home extends Component<any, HomeState> {
i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
}
handlePageChange(page: number) {
handlePagePrev() {
this.props.history.back();
// A hack to scroll to top
setTimeout(() => {
window.scrollTo(0, 0);
}, 50);
}
handlePageNext(nextPage: PaginationCursor) {
this.setState({ scrolled: false });
this.updateUrl({ page });
this.updateUrl({ pageCursor: nextPage });
}
handleSortChange(val: SortType) {
this.setState({ scrolled: false });
this.updateUrl({ sort: val, page: 1 });
this.updateUrl({ sort: val, pageCursor: undefined });
}
handleListingTypeChange(val: ListingType) {
this.setState({ scrolled: false });
this.updateUrl({ listingType: val, page: 1 });
this.updateUrl({ listingType: val, pageCursor: undefined });
}
handleDataTypeChange(val: DataType) {
this.setState({ scrolled: false });
this.updateUrl({ dataType: val, page: 1 });
this.updateUrl({ dataType: val, pageCursor: undefined });
}
async handleAddModToCommunity(form: AddModToCommunity) {