]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/home/home.tsx
Fix home page not using site-level listing type #1612 (#1778)
[lemmy-ui.git] / src / shared / components / home / home.tsx
index 9c7cd753defe55c61f92226fe1617ef68c9c2608..741dfa57aa7576eb0138bf1d687ab2773ae4a70d 100644 (file)
@@ -1,5 +1,29 @@
+import {
+  commentsToFlatNodes,
+  editComment,
+  editPost,
+  editWith,
+  enableDownvotes,
+  enableNsfw,
+  getCommentParentId,
+  getDataTypeString,
+  myAuth,
+  postToCommentSortType,
+  setIsoData,
+  showLocal,
+  updatePersonBlock,
+} from "@utils/app";
+import {
+  getPageFromString,
+  getQueryParams,
+  getQueryString,
+  getRandomFromList,
+} from "@utils/helpers";
+import { canCreateCommunity } from "@utils/roles";
+import type { QueryParams } from "@utils/types";
+import { RouteDataResponse } from "@utils/types";
 import { NoOptionI18nKeys } from "i18next";
-import { Component, linkEvent, MouseEventHandler } from "inferno";
+import { Component, MouseEventHandler, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { Link } from "inferno-router";
 import {
@@ -47,44 +71,22 @@ import {
   SortType,
   TransferCommunity,
 } from "lemmy-js-client";
-import { i18n } from "../../i18next";
+import { fetchLimit, relTags, trendingFetchLimit } from "../../config";
 import {
   CommentViewType,
   DataType,
   InitialFetchRequest,
 } from "../../interfaces";
-import { UserService } from "../../services";
-import { FirstLoadService } from "../../services/FirstLoadService";
-import { HttpService, RequestState } from "../../services/HttpService";
+import { mdToHtml } from "../../markdown";
 import {
-  canCreateCommunity,
-  commentsToFlatNodes,
-  editComment,
-  editPost,
-  editWith,
-  enableDownvotes,
-  enableNsfw,
-  fetchLimit,
-  getCommentParentId,
-  getDataTypeString,
-  getPageFromString,
-  getQueryParams,
-  getQueryString,
-  getRandomFromList,
-  mdToHtml,
-  myAuth,
-  postToCommentSortType,
-  QueryParams,
-  relTags,
-  restoreScrollPosition,
-  saveScrollPosition,
-  setIsoData,
-  setupTippy,
-  showLocal,
-  toast,
-  trendingFetchLimit,
-  updatePersonBlock,
-} from "../../utils";
+  FirstLoadService,
+  HomeCacheService,
+  I18NextService,
+  UserService,
+} from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
+import { setupTippy } from "../../tippy";
+import { toast } from "../../toast";
 import { CommentNodes } from "../comment/comment-nodes";
 import { DataTypeSelect } from "../common/data-type-select";
 import { HtmlTags } from "../common/html-tags";
@@ -104,6 +106,7 @@ interface HomeState {
   showTrendingMobile: boolean;
   showSidebarMobile: boolean;
   subscribedCollapsed: boolean;
+  scrolled: boolean;
   tagline?: string;
   siteRes: GetSiteResponse;
   finished: Map<CommentId, boolean | undefined>;
@@ -111,22 +114,61 @@ interface HomeState {
 }
 
 interface HomeProps {
-  listingType: ListingType;
+  listingType?: ListingType;
   dataType: DataType;
   sort: SortType;
   page: number;
 }
 
+type HomeData = RouteDataResponse<{
+  postsRes: GetPostsResponse;
+  commentsRes: GetCommentsResponse;
+  trendingCommunitiesRes: ListCommunitiesResponse;
+}>;
+
+function getRss(listingType: ListingType) {
+  const { sort } = getHomeQueryParams();
+  const auth = myAuth();
+
+  let rss: string | undefined = undefined;
+
+  switch (listingType) {
+    case "All": {
+      rss = `/feeds/all.xml?sort=${sort}`;
+      break;
+    }
+    case "Local": {
+      rss = `/feeds/local.xml?sort=${sort}`;
+      break;
+    }
+    case "Subscribed": {
+      rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
+      break;
+    }
+  }
+
+  return (
+    rss && (
+      <>
+        <a href={rss} rel={relTags} title="RSS">
+          <Icon icon="rss" classes="text-muted small" />
+        </a>
+        <link rel="alternate" type="application/atom+xml" href={rss} />
+      </>
+    )
+  );
+}
+
 function getDataTypeFromQuery(type?: string): DataType {
   return type ? DataType[type] : DataType.Post;
 }
 
-function getListingTypeFromQuery(type?: string): ListingType {
+function getListingTypeFromQuery(type?: string): ListingType | undefined {
   const myListingType =
     UserService.Instance.myUserInfo?.local_user_view?.local_user
       ?.default_listing_type;
 
-  return (type ? (type as ListingType) : myListingType) ?? "Local";
+  return type ? (type as ListingType) : myListingType;
 }
 
 function getSortTypeFromQuery(type?: string): SortType {
@@ -155,10 +197,10 @@ const MobileButton = ({
   onClick: MouseEventHandler<HTMLButtonElement>;
 }) => (
   <button
-    className="btn btn-secondary d-inline-block mb-2 mr-3"
+    className="btn btn-secondary d-inline-block mb-2 me-3"
     onClick={onClick}
   >
-    {i18n.t(textKey)}{" "}
+    {I18NextService.i18n.t(textKey)}{" "}
     <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
   </button>
 );
@@ -170,17 +212,18 @@ const LinkButton = ({
   path: string;
   translationKey: NoOptionI18nKeys;
 }) => (
-  <Link className="btn btn-secondary btn-block" to={path}>
-    {i18n.t(translationKey)}
+  <Link className="btn btn-secondary d-block" to={path}>
+    {I18NextService.i18n.t(translationKey)}
   </Link>
 );
 
 export class Home extends Component<any, HomeState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData<HomeData>(this.context);
   state: HomeState = {
     postsRes: { state: "empty" },
     commentsRes: { state: "empty" },
     trendingCommunitiesRes: { state: "empty" },
+    scrolled: true,
     siteRes: this.isoData.site_res,
     showSubscribedMobile: false,
     showTrendingMobile: false,
@@ -228,49 +271,60 @@ export class Home extends Component<any, HomeState> {
 
     // Only fetch the data if coming from another route
     if (FirstLoadService.isFirstLoad) {
-      const [postsRes, commentsRes, trendingCommunitiesRes] =
+      const { trendingCommunitiesRes, commentsRes, postsRes } =
         this.isoData.routeData;
 
       this.state = {
         ...this.state,
-        postsRes,
-        commentsRes,
         trendingCommunitiesRes,
-        tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
-          ?.content,
+        commentsRes,
+        postsRes,
         isIsomorphic: true,
       };
+
+      HomeCacheService.postsRes = postsRes;
     }
+
+    this.state.tagline = getRandomFromList(
+      this.state?.siteRes?.taglines ?? []
+    )?.content;
+  }
+
+  componentWillUnmount() {
+    HomeCacheService.activate();
   }
 
   async componentDidMount() {
-    if (!this.state.isIsomorphic || !this.isoData.routeData.length) {
+    if (
+      !this.state.isIsomorphic ||
+      !Object.values(this.isoData.routeData).some(
+        res => res.state === "success" || res.state === "failed"
+      )
+    ) {
       await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
     }
 
     setupTippy();
   }
 
-  componentWillUnmount() {
-    saveScrollPosition(this.context);
-  }
-
-  static fetchInitialData({
+  static async fetchInitialData({
     client,
     auth,
     query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
-  }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<
-    RequestState<any>
-  >[] {
+    site,
+  }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
     const dataType = getDataTypeFromQuery(urlDataType);
-
-    // TODO figure out auth default_listingType, default_sort_type
-    const type_ = getListingTypeFromQuery(listingType);
+    const type_ =
+      getListingTypeFromQuery(listingType) ??
+      site.site_view.local_site.default_post_listing_type;
     const sort = getSortTypeFromQuery(urlSort);
 
     const page = urlPage ? Number(urlPage) : 1;
 
-    const promises: Promise<RequestState<any>>[] = [];
+    let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
+    let commentsRes: RequestState<GetCommentsResponse> = {
+      state: "empty",
+    };
 
     if (dataType === DataType.Post) {
       const getPostsForm: GetPosts = {
@@ -282,8 +336,7 @@ export class Home extends Component<any, HomeState> {
         auth,
       };
 
-      promises.push(client.getPosts(getPostsForm));
-      promises.push(Promise.resolve({ state: "empty" }));
+      postsRes = await client.getPosts(getPostsForm);
     } else {
       const getCommentsForm: GetComments = {
         page,
@@ -293,8 +346,8 @@ export class Home extends Component<any, HomeState> {
         saved_only: false,
         auth,
       };
-      promises.push(Promise.resolve({ state: "empty" }));
-      promises.push(client.getComments(getCommentsForm));
+
+      commentsRes = await client.getComments(getCommentsForm);
     }
 
     const trendingCommunitiesForm: ListCommunities = {
@@ -303,9 +356,14 @@ export class Home extends Component<any, HomeState> {
       limit: trendingFetchLimit,
       auth,
     };
-    promises.push(client.listCommunities(trendingCommunitiesForm));
 
-    return promises;
+    return {
+      trendingCommunitiesRes: await client.listCommunities(
+        trendingCommunitiesForm
+      ),
+      commentsRes,
+      postsRes,
+    };
   }
 
   get documentTitle(): string {
@@ -325,14 +383,14 @@ export class Home extends Component<any, HomeState> {
     } = this.state;
 
     return (
-      <div className="container-lg">
+      <div className="home container-lg">
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
         />
         {site_setup && (
           <div className="row">
-            <main role="main" className="col-12 col-md-8">
+            <main role="main" className="col-12 col-md-8 col-lg-9">
               {tagline && (
                 <div
                   id="tagline"
@@ -340,9 +398,9 @@ export class Home extends Component<any, HomeState> {
                 ></div>
               )}
               <div className="d-block d-md-none">{this.mobileView}</div>
-              {this.posts()}
+              {this.posts}
             </main>
-            <aside className="d-none d-md-block col-md-4">
+            <aside className="d-none d-md-block col-md-4 col-lg-3">
               {this.mySidebar}
             </aside>
           </div>
@@ -361,7 +419,6 @@ export class Home extends Component<any, HomeState> {
       siteRes: {
         site_view: { counts, site },
         admins,
-        online,
       },
       showSubscribedMobile,
       showTrendingMobile,
@@ -393,18 +450,18 @@ export class Home extends Component<any, HomeState> {
               site={site}
               admins={admins}
               counts={counts}
-              online={online}
               showLocal={showLocal(this.isoData)}
+              isMobile={true}
             />
           )}
           {showTrendingMobile && (
-            <div className="col-12 card border-secondary mb-3">
-              <div className="card-body">{this.trendingCommunities(true)}</div>
+            <div className="card border-secondary mb-3">
+              {this.trendingCommunities()}
             </div>
           )}
           {showSubscribedMobile && (
-            <div className="col-12 card border-secondary mb-3">
-              <div className="card-body">{this.subscribedCommunities}</div>
+            <div className="card border-secondary mb-3">
+              {this.subscribedCommunities(true)}
             </div>
           )}
         </div>
@@ -417,46 +474,35 @@ export class Home extends Component<any, HomeState> {
       siteRes: {
         site_view: { counts, site },
         admins,
-        online,
       },
     } = this.state;
 
     return (
-      <div>
-        <div id="sidebarContainer">
-          <div id="sidebarMain" className="card border-secondary mb-3">
-            <div className="card-body">
-              {this.trendingCommunities()}
-              {canCreateCommunity(this.state.siteRes) && (
-                <LinkButton
-                  path="/create_community"
-                  translationKey="create_a_community"
-                />
-              )}
-              <LinkButton
-                path="/communities"
-                translationKey="explore_communities"
-              />
-            </div>
+      <div id="sidebarContainer">
+        <section id="sidebarMain" className="card border-secondary mb-3">
+          {this.trendingCommunities()}
+        </section>
+        <SiteSidebar
+          site={site}
+          admins={admins}
+          counts={counts}
+          showLocal={showLocal(this.isoData)}
+        />
+        {this.hasFollows && (
+          <div className="accordion">
+            <section
+              id="sidebarSubscribed"
+              className="card border-secondary mb-3"
+            >
+              {this.subscribedCommunities(false)}
+            </section>
           </div>
-          <SiteSidebar
-            site={site}
-            admins={admins}
-            counts={counts}
-            online={online}
-            showLocal={showLocal(this.isoData)}
-          />
-          {this.hasFollows && (
-            <div id="sidebarSubscribed" className="card border-secondary mb-3">
-              <div className="card-body">{this.subscribedCommunities}</div>
-            </div>
-          )}
-        </div>
+        )}
       </div>
     );
   }
 
-  trendingCommunities(isMobile = false) {
+  trendingCommunities() {
     switch (this.state.trendingCommunitiesRes?.state) {
       case "loading":
         return (
@@ -467,68 +513,106 @@ export class Home extends Component<any, HomeState> {
       case "success": {
         const trending = this.state.trendingCommunitiesRes.data.communities;
         return (
-          <div className={!isMobile ? "mb-2" : ""}>
-            <h5>
-              <T i18nKey="trending_communities">
-                #
-                <Link className="text-body" to="/communities">
+          <>
+            <header className="card-header d-flex align-items-center">
+              <h5 className="mb-0">
+                <T i18nKey="trending_communities">
                   #
-                </Link>
-              </T>
-            </h5>
-            <ul className="list-inline mb-0">
-              {trending.map(cv => (
-                <li
-                  key={cv.community.id}
-                  className="list-inline-item d-inline-block"
-                >
-                  <CommunityLink community={cv.community} />
-                </li>
-              ))}
-            </ul>
-          </div>
+                  <Link className="text-body" to="/communities">
+                    #
+                  </Link>
+                </T>
+              </h5>
+            </header>
+            <div className="card-body">
+              {trending.length > 0 && (
+                <ul className="list-inline">
+                  {trending.map(cv => (
+                    <li key={cv.community.id} className="list-inline-item">
+                      <CommunityLink community={cv.community} />
+                    </li>
+                  ))}
+                </ul>
+              )}
+              {canCreateCommunity(this.state.siteRes) && (
+                <LinkButton
+                  path="/create_community"
+                  translationKey="create_a_community"
+                />
+              )}
+              <LinkButton
+                path="/communities"
+                translationKey="explore_communities"
+              />
+            </div>
+          </>
         );
       }
     }
   }
 
-  get subscribedCommunities() {
+  subscribedCommunities(isMobile = false) {
     const { subscribedCollapsed } = this.state;
 
     return (
-      <div>
-        <h5>
-          <T class="d-inline" i18nKey="subscribed_to_communities">
-            #
-            <Link className="text-body" to="/communities">
+      <>
+        <header
+          className="card-header d-flex align-items-center"
+          id="sidebarSubscribedHeader"
+        >
+          <h5 className="mb-0 d-inline">
+            <T class="d-inline" i18nKey="subscribed_to_communities">
               #
-            </Link>
-          </T>
-          <button
-            className="btn btn-sm text-muted"
-            onClick={linkEvent(this, this.handleCollapseSubscribe)}
-            aria-label={i18n.t("collapse")}
-            data-tippy-content={i18n.t("collapse")}
-          >
-            <Icon
-              icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
-              classes="icon-inline"
-            />
-          </button>
-        </h5>
+              <Link className="text-body" to="/communities">
+                #
+              </Link>
+            </T>
+          </h5>
+          {!isMobile && (
+            <button
+              type="button"
+              className="btn btn-sm text-muted"
+              onClick={linkEvent(this, this.handleCollapseSubscribe)}
+              aria-label={
+                subscribedCollapsed
+                  ? I18NextService.i18n.t("expand")
+                  : I18NextService.i18n.t("collapse")
+              }
+              data-tippy-content={
+                subscribedCollapsed
+                  ? I18NextService.i18n.t("expand")
+                  : I18NextService.i18n.t("collapse")
+              }
+              aria-expanded="true"
+              aria-controls="sidebarSubscribedBody"
+            >
+              <Icon
+                icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
+                classes="icon-inline"
+              />
+            </button>
+          )}
+        </header>
         {!subscribedCollapsed && (
-          <ul className="list-inline mb-0">
-            {UserService.Instance.myUserInfo?.follows.map(cfv => (
-              <li
-                key={cfv.community.id}
-                className="list-inline-item d-inline-block"
-              >
-                <CommunityLink community={cfv.community} />
-              </li>
-            ))}
-          </ul>
+          <div
+            id="sidebarSubscribedBody"
+            aria-labelledby="sidebarSubscribedHeader"
+          >
+            <div className="card-body">
+              <ul className="list-inline mb-0">
+                {UserService.Instance.myUserInfo?.follows.map(cfv => (
+                  <li
+                    key={cfv.community.id}
+                    className="list-inline-item d-inline-block"
+                  >
+                    <CommunityLink community={cfv.community} />
+                  </li>
+                ))}
+              </ul>
+            </div>
+          </div>
         )}
-      </div>
+      </>
     );
   }
 
@@ -552,10 +636,15 @@ export class Home extends Component<any, HomeState> {
       search: getQueryString(queryParams),
     });
 
+    if (!this.state.scrolled) {
+      this.setState({ scrolled: true });
+      setTimeout(() => window.scrollTo(0, 0), 0);
+    }
+
     await this.fetchData();
   }
 
-  posts() {
+  get posts() {
     const { page } = getHomeQueryParams();
 
     return (
@@ -575,6 +664,8 @@ export class Home extends Component<any, HomeState> {
 
     if (dataType === DataType.Post) {
       switch (this.state.postsRes?.state) {
+        case "empty":
+          return <div style="min-height: 20000px;"></div>;
         case "loading":
           return (
             <h5>
@@ -662,62 +753,37 @@ export class Home extends Component<any, HomeState> {
     const { listingType, dataType, sort } = getHomeQueryParams();
 
     return (
-      <div className="mb-3">
-        <span className="mr-3">
+      <div className="row align-items-center mb-3 g-3">
+        <div className="col-auto">
           <DataTypeSelect
             type_={dataType}
             onChange={this.handleDataTypeChange}
           />
-        </span>
-        <span className="mr-3">
+        </div>
+        <div className="col-auto">
           <ListingTypeSelect
-            type_={listingType}
+            type_={
+              listingType ??
+              this.state.siteRes.site_view.local_site.default_post_listing_type
+            }
             showLocal={showLocal(this.isoData)}
             showSubscribed
             onChange={this.handleListingTypeChange}
           />
-        </span>
-        <span className="mr-2">
+        </div>
+        <div className="col-auto">
           <SortSelect sort={sort} onChange={this.handleSortChange} />
-        </span>
-        {this.getRss(listingType)}
+        </div>
+        <div className="col-auto ps-0">
+          {getRss(
+            listingType ??
+              this.state.siteRes.site_view.local_site.default_post_listing_type
+          )}
+        </div>
       </div>
     );
   }
 
-  getRss(listingType: ListingType) {
-    const { sort } = getHomeQueryParams();
-    const auth = myAuth();
-
-    let rss: string | undefined = undefined;
-
-    switch (listingType) {
-      case "All": {
-        rss = `/feeds/all.xml?sort=${sort}`;
-        break;
-      }
-      case "Local": {
-        rss = `/feeds/local.xml?sort=${sort}`;
-        break;
-      }
-      case "Subscribed": {
-        rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
-        break;
-      }
-    }
-
-    return (
-      rss && (
-        <>
-          <a href={rss} rel={relTags} title="RSS">
-            <Icon icon="rss" classes="text-muted small" />
-          </a>
-          <link rel="alternate" type="application/atom+xml" href={rss} />
-        </>
-      )
-    );
-  }
-
   async fetchTrendingCommunities() {
     this.setState({ trendingCommunitiesRes: { state: "loading" } });
     this.setState({
@@ -735,17 +801,30 @@ export class Home extends Component<any, HomeState> {
     const { dataType, page, listingType, sort } = getHomeQueryParams();
 
     if (dataType === DataType.Post) {
-      this.setState({ postsRes: { state: "loading" } });
-      this.setState({
-        postsRes: await HttpService.client.getPosts({
-          page,
-          limit: fetchLimit,
-          sort,
-          saved_only: false,
-          type_: listingType,
-          auth,
-        }),
-      });
+      if (HomeCacheService.active) {
+        const { postsRes, scrollY } = HomeCacheService;
+        HomeCacheService.deactivate();
+        this.setState({ postsRes });
+        window.scrollTo({
+          left: 0,
+          top: scrollY,
+          behavior: "instant",
+        });
+      } else {
+        this.setState({ postsRes: { state: "loading" } });
+        this.setState({
+          postsRes: await HttpService.client.getPosts({
+            page,
+            limit: fetchLimit,
+            sort,
+            saved_only: false,
+            type_: listingType,
+            auth,
+          }),
+        });
+
+        HomeCacheService.postsRes = this.state.postsRes;
+      }
     } else {
       this.setState({ commentsRes: { state: "loading" } });
       this.setState({
@@ -760,7 +839,6 @@ export class Home extends Component<any, HomeState> {
       });
     }
 
-    restoreScrollPosition(this.context);
     setupTippy();
   }
 
@@ -781,23 +859,23 @@ export class Home extends Component<any, HomeState> {
   }
 
   handlePageChange(page: number) {
+    this.setState({ scrolled: false });
     this.updateUrl({ page });
-    window.scrollTo(0, 0);
   }
 
   handleSortChange(val: SortType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ sort: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   handleListingTypeChange(val: ListingType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ listingType: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ dataType: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   async handleAddModToCommunity(form: AddModToCommunity) {
@@ -894,14 +972,14 @@ export class Home extends Component<any, HomeState> {
   async handleCommentReport(form: CreateCommentReport) {
     const reportRes = await HttpService.client.createCommentReport(form);
     if (reportRes.state == "success") {
-      toast(i18n.t("report_created"));
+      toast(I18NextService.i18n.t("report_created"));
     }
   }
 
   async handlePostReport(form: CreatePostReport) {
     const reportRes = await HttpService.client.createPostReport(form);
     if (reportRes.state == "success") {
-      toast(i18n.t("report_created"));
+      toast(I18NextService.i18n.t("report_created"));
     }
   }
 
@@ -925,7 +1003,7 @@ export class Home extends Component<any, HomeState> {
 
   async handleTransferCommunity(form: TransferCommunity) {
     await HttpService.client.transferCommunity(form);
-    toast(i18n.t("transfer_community"));
+    toast(I18NextService.i18n.t("transfer_community"));
   }
 
   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
@@ -992,7 +1070,7 @@ export class Home extends Component<any, HomeState> {
 
   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
     if (purgeRes.state == "success") {
-      toast(i18n.t("purge_success"));
+      toast(I18NextService.i18n.t("purge_success"));
       this.context.router.history.push(`/`);
     }
   }