From b1580d753f403d4b088cf4579d29c1976da20605 Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Thu, 7 Jan 2021 11:24:41 +0100
Subject: [PATCH] feat: drop traditional pagination in favor of auto window
 expansion

---
 .env                                |   2 +-
 src/actions/posts.js                |   2 -
 src/components/onboarding/index.jsx |  17 +----
 src/components/posts/Post.jsx       |  14 ++--
 src/components/posts/PostList.jsx   | 100 +++++++++++++++++-----------
 src/containers/PostFilters.jsx      |  51 +-------------
 src/containers/PostsContainer.jsx   |  10 +--
 src/pages/Home.jsx                  |   4 +-
 src/stores.js                       |   2 -
 typings/cf2021.d.ts                 |   2 -
 10 files changed, 79 insertions(+), 125 deletions(-)

diff --git a/.env b/.env
index 2283bc6..613ffb1 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,4 @@
-REACT_APP_STYLEGUIDE_URL=https://styleguide.pir-test.eu/2.3.0
+REACT_APP_STYLEGUIDE_URL=https://styleguide.pir-test.eu/2.3.1
 REACT_APP_API_BASE_URL=https://cf2021.pir-test.eu/api
 REACT_APP_MATOMO_ID=135
 REACT_APP_SENTRY_DSN=https://aa80453ff4d54b9a9c1b49e79060498a@sentry.pir-test.eu/14
diff --git a/src/actions/posts.js b/src/actions/posts.js
index eab9e29..238fdf2 100644
--- a/src/actions/posts.js
+++ b/src/actions/posts.js
@@ -36,8 +36,6 @@ export const loadPosts = createAsyncAction(
           state.window = {
             items: filteredPosts.map(property("id")),
             itemCount: filteredPosts.length,
-            page: 1,
-            perPage: 20,
           };
         });
       }
diff --git a/src/components/onboarding/index.jsx b/src/components/onboarding/index.jsx
index 6f60ffd..44462d5 100644
--- a/src/components/onboarding/index.jsx
+++ b/src/components/onboarding/index.jsx
@@ -113,7 +113,7 @@ export const steps = [
         </div>
       </>
     ),
-    placement: "right",
+    placement: "center",
   },
   {
     target: ".joyride-filters",
@@ -132,21 +132,6 @@ export const steps = [
     ),
     placement: "bottom",
   },
-  {
-    target: ".joyride-pagination",
-    content: (
-      <>
-        <h1 className="head-alt-sm mb-4">Stránkování příspěvků</h1>
-        <div className="leading-snug text-base space-y-2">
-          <p>
-            Příspěvky jsou stránkované <strong>po dvaceti</strong>. Stránky
-            můžeš přepínat těmito šipkami.
-          </p>
-        </div>
-      </>
-    ),
-    placement: "bottom",
-  },
   {
     target: ".joyride-announcements",
     content: (
diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx
index 763e221..f6262e8 100644
--- a/src/components/posts/Post.jsx
+++ b/src/components/posts/Post.jsx
@@ -23,6 +23,7 @@ const Post = ({
   currentUser,
   supportThreshold,
   canThumb,
+  reportSeen = true,
   onLike,
   onDislike,
   onHide,
@@ -36,20 +37,21 @@ const Post = ({
   onEdit,
   onArchive,
   onSeen,
+  ...props
 }) => {
   const { ref, inView } = useInView({
     threshold: 0.8,
     trackVisibility: true,
-    delay: 1500,
-    skip: seen,
+    delay: 1000,
+    skip: !reportSeen,
     triggerOnce: true,
   });
 
   useEffect(() => {
-    if (!seen && inView && onSeen) {
+    if (inView && onSeen) {
       onSeen();
     }
-  });
+  }, [inView, onSeen]);
 
   const wrapperClassName = classNames(
     "flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2 transition duration-500",
@@ -193,7 +195,7 @@ const Post = ({
   const thumbsVisible = !archived && (type === "post" || state === "announced");
 
   return (
-    <div className={wrapperClassName} ref={ref}>
+    <div className={wrapperClassName} ref={ref} {...props}>
       <img
         src={`https://a.pirati.cz/piratar/200/${author.username}.jpg`}
         className="w-8 h-8 lg:w-14 lg:h-14 mr-3 rounded object-cover"
@@ -327,7 +329,7 @@ const Post = ({
         </div>
         <div className="overflow-hidden">
           <div
-            className="text-sm lg:text-base text-black leading-normal content-block overflow-x-scroll"
+            className="text-sm lg:text-base text-black leading-normal content-block overflow-x-scroll overflow-y-hidden"
             dangerouslySetInnerHTML={htmlContent}
           ></div>
         </div>
diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx
index 607cc01..e80a6fd 100644
--- a/src/components/posts/PostList.jsx
+++ b/src/components/posts/PostList.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 import classNames from "classnames";
 
 import Post from "./Post";
@@ -30,6 +30,9 @@ const PostList = ({
     responderFn(post);
   };
 
+  const windowSize = 20;
+  const [window, setWindow] = useState(windowSize);
+
   const onPostLike = buildHandler(onLike);
   const onPostDislike = buildHandler(onDislike);
   const onPostEdit = buildHandler(onEdit);
@@ -47,48 +50,65 @@ const PostList = ({
     onRejectProcedureProposalByChairman
   );
 
-  const onPostSeen = (post) => () => {
-    onSeen(post);
-  };
+  // Reset window when items change (possibly due to filtering).
+  useEffect(() => {
+    setWindow(windowSize);
+  }, [items, setWindow]);
+
+  const onPostSeen = useCallback(
+    (post) => () => {
+      if (!post.seen) {
+        onSeen(post);
+      }
+
+      // Once last post in window is reached, attempt show more.
+      if (items.indexOf(post) === window - 1) {
+        setWindow(window + windowSize);
+      }
+    },
+    [items, onSeen, window]
+  );
+
+  const windowItems = useMemo(() => {
+    return items.slice(0, window).filter((item) => !item.hidden);
+  }, [items, window]);
 
   return (
     <div className={classNames("space-y-px", className)}>
-      {items
-        .filter((item) => !item.hidden)
-        .map((item) => (
-          <Post
-            key={item.id}
-            datetime={item.datetime}
-            author={item.author}
-            type={item.type}
-            state={item.state}
-            content={item.contentHtml}
-            ranking={item.ranking}
-            historyLog={item.historyLog}
-            modified={item.modified}
-            seen={item.seen}
-            archived={item.archived}
-            dimIfArchived={dimArchived}
-            currentUser={currentUser}
-            supportThreshold={supportThreshold}
-            canThumb={canThumb}
-            onLike={onPostLike(item)}
-            onDislike={onPostDislike(item)}
-            onHide={onPostHide(item)}
-            onBanUser={onPostBanUser(item)}
-            onUnbanUser={onPostUnbanUser(item)}
-            onInviteUser={onPostInviteUser(item)}
-            onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)}
-            onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)}
-            onRejectProcedureProposal={onPostRejectProcedureProposal(item)}
-            onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
-              item
-            )}
-            onEdit={onPostEdit(item)}
-            onArchive={onPostArchive(item)}
-            onSeen={onPostSeen(item)}
-          />
-        ))}
+      {windowItems.map((item, idx) => (
+        <Post
+          key={item.id}
+          datetime={item.datetime}
+          author={item.author}
+          type={item.type}
+          state={item.state}
+          content={item.contentHtml}
+          ranking={item.ranking}
+          modified={item.modified}
+          seen={item.seen}
+          reportSeen={!item.seen || idx === window - 1}
+          archived={item.archived}
+          dimIfArchived={dimArchived}
+          currentUser={currentUser}
+          supportThreshold={supportThreshold}
+          canThumb={canThumb}
+          onLike={onPostLike(item)}
+          onDislike={onPostDislike(item)}
+          onHide={onPostHide(item)}
+          onBanUser={onPostBanUser(item)}
+          onUnbanUser={onPostUnbanUser(item)}
+          onInviteUser={onPostInviteUser(item)}
+          onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)}
+          onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)}
+          onRejectProcedureProposal={onPostRejectProcedureProposal(item)}
+          onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
+            item
+          )}
+          onEdit={onPostEdit(item)}
+          onArchive={onPostArchive(item)}
+          onSeen={onPostSeen(item)}
+        />
+      ))}
       {showAddPostCta && !items.length && (
         <p className="p-4 lg:p-0 lg:py-3 leading-snug text-sm md:text-base">
           Nikdo zatím žádný odpovídající příspěvek do rozpravy nepřidal. Budeš
diff --git a/src/containers/PostFilters.jsx b/src/containers/PostFilters.jsx
index e1ccec4..5ade1dd 100644
--- a/src/containers/PostFilters.jsx
+++ b/src/containers/PostFilters.jsx
@@ -1,15 +1,11 @@
-import React, { useCallback } from "react";
-import pick from "lodash/pick";
+import React from "react";
 
-import Chip from "components/Chip";
 import Dropdown from "components/Dropdown";
 import { PostStore } from "stores";
 import { updateWindowPosts } from "utils";
 
 const PostFilters = () => {
-  const { window, filters } = PostStore.useState((state) =>
-    pick(state, ["window", "filters", "items"])
-  );
+  const filters = PostStore.useState((state) => state.filters);
 
   const flagsOptions = [
     { title: "Vše", value: "all" },
@@ -25,8 +21,6 @@ const PostFilters = () => {
     { title: "Jen návrhy", value: "proposalsOnly" },
     { title: "Jen příspěvky", value: "discussionOnly" },
   ];
-  const hasNextPage = window.page * window.perPage < window.itemCount;
-  const hasPrevPage = window.page > 1;
 
   const setFilter = (prop, newValue, resetPage = true) => {
     PostStore.update((state) => {
@@ -45,24 +39,6 @@ const PostFilters = () => {
   const onSortChange = (newValue) => setFilter("sort", newValue, false);
   const onTypeChange = (newValue) => setFilter("type", newValue);
 
-  const onNextPage = useCallback(() => {
-    if (hasNextPage) {
-      PostStore.update((state) => {
-        state.window.page = state.window.page + 1;
-      });
-    }
-  }, [hasNextPage]);
-  const onPrevPage = useCallback(() => {
-    if (hasPrevPage) {
-      PostStore.update((state) => {
-        state.window.page = state.window.page - 1;
-      });
-    }
-  }, [hasPrevPage]);
-
-  const enabledPaginatorClass = "cursor-pointer text-xs";
-  const disabledPaginatorClass = "opacity-25 cursor-not-allowed text-xs";
-
   return (
     <div className="flex flex-col space-y-2 xl:space-y-0 xl:space-x-8 xl:flex-row xl:items-center">
       <div className="-mx-1 joyride-filters">
@@ -85,29 +61,6 @@ const PostFilters = () => {
           className="text-xs ml-1 mt-2 xl:mt-0"
         />
       </div>
-
-      <div className="joyride-pagination">
-        <Chip
-          color="grey-125"
-          className={
-            hasPrevPage ? enabledPaginatorClass : disabledPaginatorClass
-          }
-          hoveractive
-          onClick={onPrevPage}
-        >
-          <span className="ico--chevron-left"></span>
-        </Chip>
-        <Chip
-          color="grey-125"
-          className={
-            hasNextPage ? enabledPaginatorClass : disabledPaginatorClass
-          }
-          hoveractive
-          onClick={onNextPage}
-        >
-          <span className="ico--chevron-right"></span>
-        </Chip>
-      </div>
     </div>
   );
 };
diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx
index 3fd62b1..500f083 100644
--- a/src/containers/PostsContainer.jsx
+++ b/src/containers/PostsContainer.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useMemo } from "react";
 import pick from "lodash/pick";
 
 import {
@@ -191,9 +191,9 @@ const PostsContainer = ({ className, showAddPostCta }) => {
     [setUserToInvite]
   );
 
-  const sliceStart = (window.page - 1) * window.perPage;
-  const sliceEnd = window.page * window.perPage;
-  const windowItems = window.items.map((postId) => items[postId]);
+  const windowItems = useMemo(() => {
+    return window.items.map((postId) => items[postId]);
+  }, [items, window.items]);
 
   return (
     <>
@@ -203,7 +203,7 @@ const PostsContainer = ({ className, showAddPostCta }) => {
         </ErrorMessage>
       )}
       <PostList
-        items={windowItems.slice(sliceStart, sliceEnd)}
+        items={windowItems}
         showAddPostCta={showAddPostCta}
         canThumb={isAuthenticated}
         onLike={like.run}
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index fdf8f2c..8918b1e 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -213,10 +213,10 @@ const Home = () => {
           <div className="pl-4 pt-1 lg:pt-5">
             <div className="space-x-4 inline-flex items-center">
               <button
-                className="ico--question text-grey-200 hover:text-black text-lg"
+                className="ico--question text-grey-200 hidden lg:block hover:text-black text-lg"
                 aria-label="Potřebuješ pomoc? Spusť si znovu nápovědu jak tuhle aplikaci používat."
                 data-tip="Potřebuješ pomoc? Spusť si znovu nápovědu jak tuhle aplikaci používat."
-                data-tip-at="left"
+                data-tip-at="top"
                 onClick={showTutorial}
               />
               {displayActions && (
diff --git a/src/stores.js b/src/stores.js
index e98a90e..6be388b 100644
--- a/src/stores.js
+++ b/src/stores.js
@@ -48,8 +48,6 @@ const postStoreInitial = {
   window: {
     items: [],
     itemCount: 0,
-    page: 1,
-    perPage: 20,
   },
   filters: {
     flags: "active",
diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts
index e73fbbf..b699e18 100644
--- a/typings/cf2021.d.ts
+++ b/typings/cf2021.d.ts
@@ -158,8 +158,6 @@ declare namespace CF2021 {
     window: {
       items: string[];
       itemCount: number;
-      page: number;
-      perPage: number;
     };
     filters: PostStoreFilters;
   }
-- 
GitLab