]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' into breakout-role-utils
authorAlec Armbruster <35377827+alectrocute@users.noreply.github.com>
Sat, 17 Jun 2023 12:44:47 +0000 (08:44 -0400)
committerGitHub <noreply@github.com>
Sat, 17 Jun 2023 12:44:47 +0000 (08:44 -0400)
14 files changed:
1  2 
src/shared/components/app/navbar.tsx
src/shared/components/common/markdown-textarea.tsx
src/shared/components/community/communities.tsx
src/shared/components/community/community.tsx
src/shared/components/community/sidebar.tsx
src/shared/components/home/home.tsx
src/shared/components/modlog.tsx
src/shared/components/person/profile.tsx
src/shared/components/person/reports.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/post-listing.tsx
src/shared/components/post/post.tsx
src/shared/components/search.tsx
src/shared/utils.ts

index 6a57659b0e564f0434195778282e3a6c5571e7df,a19ea5c5880c81e3dc4c29a79cd6131f3a432cfa..d0943af2d56b072e1f78c5652d13abd25e7861e9
@@@ -10,17 -10,17 +10,17 @@@ import { i18n } from "../../i18next"
  import { UserService } from "../../services";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  amAdmin,
 -  canCreateCommunity,
    donateLemmyUrl,
 -  isBrowser,
    myAuth,
    numToSI,
 -  poll,
    showAvatars,
    toast,
    updateUnreadCountsInterval,
  } from "../../utils";
 +import { isBrowser } from "../../utils/browser/is-browser";
 +import { poll } from "../../utils/helpers/poll";
 +import { amAdmin } from "../../utils/roles/am-admin";
 +import { canCreateCommunity } from "../../utils/roles/can-create-community";
  import { Icon } from "../common/icon";
  import { PictrsImage } from "../common/pictrs-image";
  
@@@ -224,11 -224,14 +224,14 @@@ export class Navbar extends Component<N
              )}
              <li className="nav-item">
                <a
-                 className="nav-link"
+                 className="nav-link d-inline-flex align-items-center d-md-inline-block"
                  title={i18n.t("support_lemmy")}
                  href={donateLemmyUrl}
                >
                  <Icon icon="heart" classes="small" />
+                 <span className="d-inline ml-1 d-md-none ml-md-0">
+                   {i18n.t("support_lemmy")}
+                 </span>
                </a>
              </li>
            </ul>
              <li id="navSearch" className="nav-item">
                <NavLink
                  to="/search"
-                 className="nav-link"
+                 className="nav-link d-inline-flex align-items-center d-md-inline-block"
                  title={i18n.t("search")}
                  onMouseUp={linkEvent(this, handleCollapseClick)}
                >
                  <Icon icon="search" />
+                 <span className="d-inline ml-1 d-md-none ml-md-0">
+                   {i18n.t("search")}
+                 </span>
                </NavLink>
              </li>
              {amAdmin() && (
                <li id="navAdmin" className="nav-item">
                  <NavLink
                    to="/admin"
-                   className="nav-link"
+                   className="nav-link d-inline-flex align-items-center d-md-inline-block"
                    title={i18n.t("admin_settings")}
                    onMouseUp={linkEvent(this, handleCollapseClick)}
                  >
                    <Icon icon="settings" />
+                   <span className="d-inline ml-1 d-md-none ml-md-0">
+                     {i18n.t("admin_settings")}
+                   </span>
                  </NavLink>
                </li>
              )}
                <>
                  <li id="navMessages" className="nav-item">
                    <NavLink
-                     className="nav-link"
+                     className="nav-link d-inline-flex align-items-center d-md-inline-block"
                      to="/inbox"
                      title={i18n.t("unread_messages", {
                        count: Number(this.unreadInboxCount),
                      onMouseUp={linkEvent(this, handleCollapseClick)}
                    >
                      <Icon icon="bell" />
+                     <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+                       {i18n.t("unread_messages", {
+                         count: Number(this.unreadInboxCount),
+                         formattedCount: numToSI(this.unreadInboxCount),
+                       })}
+                     </span>
                      {this.unreadInboxCount > 0 && (
                        <span className="mx-1 badge badge-light">
                          {numToSI(this.unreadInboxCount)}
                  {this.moderatesSomething && (
                    <li id="navModeration" className="nav-item">
                      <NavLink
-                       className="nav-link"
+                       className="nav-link d-inline-flex align-items-center d-md-inline-block"
                        to="/reports"
                        title={i18n.t("unread_reports", {
                          count: Number(this.unreadReportCount),
                        onMouseUp={linkEvent(this, handleCollapseClick)}
                      >
                        <Icon icon="shield" />
+                       <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+                         {i18n.t("unread_reports", {
+                           count: Number(this.unreadReportCount),
+                           formattedCount: numToSI(this.unreadReportCount),
+                         })}
+                       </span>
                        {this.unreadReportCount > 0 && (
                          <span className="mx-1 badge badge-light">
                            {numToSI(this.unreadReportCount)}
                    <li id="navApplications" className="nav-item">
                      <NavLink
                        to="/registration_applications"
-                       className="nav-link"
+                       className="nav-link d-inline-flex align-items-center d-md-inline-block"
                        title={i18n.t("unread_registration_applications", {
                          count: Number(this.unreadApplicationCount),
                          formattedCount: numToSI(this.unreadApplicationCount),
                        onMouseUp={linkEvent(this, handleCollapseClick)}
                      >
                        <Icon icon="clipboard" />
+                       <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+                         {i18n.t("unread_registration_applications", {
+                           count: Number(this.unreadApplicationCount),
+                           formattedCount: numToSI(this.unreadApplicationCount),
+                         })}
+                       </span>
                        {this.unreadApplicationCount > 0 && (
                          <span className="mx-1 badge badge-light">
                            {numToSI(this.unreadApplicationCount)}
index 55e73617f4a613fe0113aecc5ede8d2138cfab0c,01fa6155603cc43db989a654657f29ca9e3fe82d..094a8dd3ef78a76439a007beca90f3ee95bc8c49
@@@ -1,4 -1,5 +1,5 @@@
  import autosize from "autosize";
+ import classNames from "classnames";
  import { NoOptionI18nKeys } from "i18next";
  import { Component, linkEvent } from "inferno";
  import { Language } from "lemmy-js-client";
@@@ -7,6 -8,7 +8,6 @@@ import { HttpService, UserService } fro
  import {
    concurrentImageUpload,
    customEmojisLookup,
 -  isBrowser,
    markdownFieldCharacterLimit,
    markdownHelpUrl,
    maxUploadImages,
    setupTribute,
    toast,
  } from "../../utils";
 +import { isBrowser } from "../../utils/browser/is-browser";
  import { EmojiPicker } from "./emoji-picker";
  import { Icon, Spinner } from "./icon";
  import { LanguageSelect } from "./language-select";
  import NavigationPrompt from "./navigation-prompt";
  import ProgressBar from "./progress-bar";
 -
  interface MarkdownTextAreaProps {
    initialContent?: string;
    initialLanguageId?: number;
@@@ -143,101 -145,116 +144,116 @@@ export class MarkdownTextArea extends C
            }
          />
          <div className="form-group row">
-           <div className={`col-sm-12`}>
-             <textarea
-               id={this.id}
-               className={`form-control ${this.state.previewMode && "d-none"}`}
-               value={this.state.content}
-               onInput={linkEvent(this, this.handleContentChange)}
-               onPaste={linkEvent(this, this.handleImageUploadPaste)}
-               onKeyDown={linkEvent(this, this.handleKeyBinds)}
-               required
-               disabled={this.isDisabled}
-               rows={2}
-               maxLength={this.props.maxLength ?? markdownFieldCharacterLimit}
-               placeholder={this.props.placeholder}
-             />
-             {this.state.previewMode && this.state.content && (
-               <div
-                 className="card border-secondary card-body md-div"
-                 dangerouslySetInnerHTML={mdToHtml(this.state.content)}
-               />
-             )}
-             {this.state.imageUploadStatus &&
-               this.state.imageUploadStatus.total > 1 && (
-                 <ProgressBar
-                   className="mt-2"
-                   striped
-                   animated
-                   value={this.state.imageUploadStatus.uploaded}
-                   max={this.state.imageUploadStatus.total}
-                   text={i18n.t("pictures_uploded_progess", {
-                     uploaded: this.state.imageUploadStatus.uploaded,
-                     total: this.state.imageUploadStatus.total,
-                   })}
+           <div className="col-12">
+             <div className="rounded bg-light border border-light">
+               <div className="d-flex flex-wrap border-bottom border-light">
+                 {this.getFormatButton("bold", this.handleInsertBold)}
+                 {this.getFormatButton("italic", this.handleInsertItalic)}
+                 {this.getFormatButton("link", this.handleInsertLink)}
+                 <EmojiPicker
+                   onEmojiClick={e => this.handleEmoji(this, e)}
+                   disabled={this.isDisabled}
+                 ></EmojiPicker>
+                 <form className="btn btn-sm text-muted font-weight-bold">
+                   <label
+                     htmlFor={`file-upload-${this.id}`}
+                     className={`mb-0 ${
+                       UserService.Instance.myUserInfo && "pointer"
+                     }`}
+                     data-tippy-content={i18n.t("upload_image")}
+                   >
+                     {this.state.imageUploadStatus ? (
+                       <Spinner />
+                     ) : (
+                       <Icon icon="image" classes="icon-inline" />
+                     )}
+                   </label>
+                   <input
+                     id={`file-upload-${this.id}`}
+                     type="file"
+                     accept="image/*,video/*"
+                     name="file"
+                     className="d-none"
+                     multiple
+                     disabled={
+                       !UserService.Instance.myUserInfo || this.isDisabled
+                     }
+                     onChange={linkEvent(this, this.handleImageUpload)}
+                   />
+                 </form>
+                 {this.getFormatButton("header", this.handleInsertHeader)}
+                 {this.getFormatButton(
+                   "strikethrough",
+                   this.handleInsertStrikethrough
+                 )}
+                 {this.getFormatButton("quote", this.handleInsertQuote)}
+                 {this.getFormatButton("list", this.handleInsertList)}
+                 {this.getFormatButton("code", this.handleInsertCode)}
+                 {this.getFormatButton("subscript", this.handleInsertSubscript)}
+                 {this.getFormatButton(
+                   "superscript",
+                   this.handleInsertSuperscript
+                 )}
+                 {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
+                 <a
+                   href={markdownHelpUrl}
+                   className="btn btn-sm text-muted font-weight-bold"
+                   title={i18n.t("formatting_help")}
+                   rel={relTags}
+                 >
+                   <Icon icon="help-circle" classes="icon-inline" />
+                 </a>
+               </div>
+               <div>
+                 <textarea
+                   id={this.id}
+                   className={classNames(
+                     "form-control border-0 rounded-top-0 rounded-bottom",
+                     {
+                       "d-none": this.state.previewMode,
+                     }
+                   )}
+                   value={this.state.content}
+                   onInput={linkEvent(this, this.handleContentChange)}
+                   onPaste={linkEvent(this, this.handleImageUploadPaste)}
+                   onKeyDown={linkEvent(this, this.handleKeyBinds)}
+                   required
+                   disabled={this.isDisabled}
+                   rows={2}
+                   maxLength={
+                     this.props.maxLength ?? markdownFieldCharacterLimit
+                   }
+                   placeholder={this.props.placeholder}
                  />
-               )}
-           </div>
-           <label className="sr-only" htmlFor={this.id}>
-             {i18n.t("body")}
-           </label>
-         </div>
-         <div className="row">
-           <div className="col-sm-12 d-flex flex-wrap">
-             {this.getFormatButton("bold", this.handleInsertBold)}
-             {this.getFormatButton("italic", this.handleInsertItalic)}
-             {this.getFormatButton("link", this.handleInsertLink)}
-             <EmojiPicker
-               onEmojiClick={e => this.handleEmoji(this, e)}
-               disabled={this.isDisabled}
-             ></EmojiPicker>
-             <form className="btn btn-sm text-muted font-weight-bold">
-               <label
-                 htmlFor={`file-upload-${this.id}`}
-                 className={`mb-0 ${
-                   UserService.Instance.myUserInfo && "pointer"
-                 }`}
-                 data-tippy-content={i18n.t("upload_image")}
-               >
-                 {this.state.imageUploadStatus ? (
-                   <Spinner />
-                 ) : (
-                   <Icon icon="image" classes="icon-inline" />
+                 {this.state.previewMode && this.state.content && (
+                   <div
+                     className="card border-secondary card-body md-div"
+                     dangerouslySetInnerHTML={mdToHtml(this.state.content)}
+                   />
                  )}
+                 {this.state.imageUploadStatus &&
+                   this.state.imageUploadStatus.total > 1 && (
+                     <ProgressBar
+                       className="mt-2"
+                       striped
+                       animated
+                       value={this.state.imageUploadStatus.uploaded}
+                       max={this.state.imageUploadStatus.total}
+                       text={i18n.t("pictures_uploded_progess", {
+                         uploaded: this.state.imageUploadStatus.uploaded,
+                         total: this.state.imageUploadStatus.total,
+                       })}
+                     />
+                   )}
+               </div>
+               <label className="sr-only" htmlFor={this.id}>
+                 {i18n.t("body")}
                </label>
-               <input
-                 id={`file-upload-${this.id}`}
-                 type="file"
-                 accept="image/*,video/*"
-                 name="file"
-                 className="d-none"
-                 multiple
-                 disabled={!UserService.Instance.myUserInfo || this.isDisabled}
-                 onChange={linkEvent(this, this.handleImageUpload)}
-               />
-             </form>
-             {this.getFormatButton("header", this.handleInsertHeader)}
-             {this.getFormatButton(
-               "strikethrough",
-               this.handleInsertStrikethrough
-             )}
-             {this.getFormatButton("quote", this.handleInsertQuote)}
-             {this.getFormatButton("list", this.handleInsertList)}
-             {this.getFormatButton("code", this.handleInsertCode)}
-             {this.getFormatButton("subscript", this.handleInsertSubscript)}
-             {this.getFormatButton("superscript", this.handleInsertSuperscript)}
-             {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
-             <a
-               href={markdownHelpUrl}
-               className="btn btn-sm text-muted font-weight-bold"
-               title={i18n.t("formatting_help")}
-               rel={relTags}
-             >
-               <Icon icon="help-circle" classes="icon-inline" />
-             </a>
+             </div>
            </div>
  
-           <div className="col-sm-12 d-flex align-items-center flex-wrap">
+           <div className="col-12 d-flex align-items-center flex-wrap mt-2">
              {this.props.showLanguage && (
                <LanguageSelect
                  iconVersion
              {this.props.buttonTitle && (
                <button
                  type="submit"
-                 className="btn btn-sm btn-secondary mr-2"
+                 className="btn btn-sm btn-secondary ml-2"
                  disabled={this.isDisabled}
                >
                  {this.state.loading ? (
              {this.props.replyType && (
                <button
                  type="button"
-                 className="btn btn-sm btn-secondary mr-2"
+                 className="btn btn-sm btn-secondary ml-2"
                  onClick={linkEvent(this, this.handleReplyCancel)}
                >
                  {i18n.t("cancel")}
              )}
              {this.state.content && (
                <button
-                 className={`btn btn-sm btn-secondary mr-2 ${
+                 className={`btn btn-sm btn-secondary ml-2 ${
                    this.state.previewMode && "active"
                  }`}
                  onClick={linkEvent(this, this.handlePreviewToggle)}
index b98bf251ca76852e88c9908294bdc773a2db7143,3eb7bd3aa76cde879b0808b95ceeca618ffc4fe2..9ce4f492cffe3c11a2c2ec833cb1886e233a5bd1
@@@ -11,17 -11,18 +11,17 @@@ import { InitialFetchRequest } from "..
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  QueryParams,
 -  RouteDataResponse,
    editCommunity,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    myAuth,
    myAuthRequired,
    numToSI,
    setIsoData,
    showLocal,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import { getQueryString } from "../../utils/helpers/get-query-string";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
  import { ListingTypeSelect } from "../common/listing-type-select";
@@@ -30,6 -31,10 +30,10 @@@ import { CommunityLink } from "./commun
  
  const communityLimit = 50;
  
+ type CommunitiesData = RouteDataResponse<{
+   listCommunitiesResponse: ListCommunitiesResponse;
+ }>;
  interface CommunitiesState {
    listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
    siteRes: GetSiteResponse;
@@@ -47,7 -52,7 +51,7 @@@ function getListingTypeFromQuery(listin
  }
  
  export class Communities extends Component<any, CommunitiesState> {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<CommunitiesData>(this.context);
    state: CommunitiesState = {
      listCommunitiesResponse: { state: "empty" },
      siteRes: this.isoData.site_res,
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
+       const { listCommunitiesResponse } = this.isoData.routeData;
        this.state = {
          ...this.state,
-         listCommunitiesResponse: this.isoData.routeData[0],
+         listCommunitiesResponse,
          isIsomorphic: true,
        };
      }
      i.context.router.history.push(`/search?q=${searchParamEncoded}`);
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      query: { listingType, page },
      client,
      auth,
-   }: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<
-     RequestState<any>
-   >[] {
+   }: InitialFetchRequest<
+     QueryParams<CommunitiesProps>
+   >): Promise<CommunitiesData> {
      const listCommunitiesForm: ListCommunities = {
        type_: getListingTypeFromQuery(listingType),
        sort: "TopMonth",
        auth: auth,
      };
  
-     return [client.listCommunities(listCommunitiesForm)];
+     return {
+       listCommunitiesResponse: await client.listCommunities(
+         listCommunitiesForm
+       ),
+     };
    }
  
    getCommunitiesQueryParams() {
index 58b330f448ed9cbb6eae89edf415077b47125ad9,6f3c9112f782231e9af853b199414b2174541a10..5f1b37af484e80ec919b68bf946f026ebed78805
@@@ -62,6 -62,8 +62,6 @@@ import { UserService } from "../../serv
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  QueryParams,
 -  RouteDataResponse,
    commentsToFlatNodes,
    communityRSSUrl,
    editComment,
@@@ -73,6 -75,8 +73,6 @@@
    getCommentParentId,
    getDataTypeString,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    myAuth,
    postToCommentSortType,
    relTags,
@@@ -85,9 -89,6 +85,9 @@@
    updateCommunityBlock,
    updatePersonBlock,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import { getQueryString } from "../../utils/helpers/get-query-string";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { CommentNodes } from "../comment/comment-nodes";
  import { BannerIconHeader } from "../common/banner-icon-header";
  import { DataTypeSelect } from "../common/data-type-select";
@@@ -99,6 -100,13 +99,13 @@@ import { Sidebar } from "../community/s
  import { SiteSidebar } from "../home/site-sidebar";
  import { PostListings } from "../post/post-listings";
  import { CommunityLink } from "./community-link";
+ type CommunityData = RouteDataResponse<{
+   communityRes: GetCommunityResponse;
+   postsRes: GetPostsResponse;
+   commentsRes: GetCommentsResponse;
+ }>;
  interface State {
    communityRes: RequestState<GetCommunityResponse>;
    postsRes: RequestState<GetPostsResponse>;
@@@ -139,7 -147,7 +146,7 @@@ export class Community extends Componen
    RouteComponentProps<{ name: string }>,
    State
  > {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<CommunityData>(this.context);
    state: State = {
      communityRes: { state: "empty" },
      postsRes: { state: "empty" },
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [communityRes, postsRes, commentsRes] = this.isoData.routeData;
+       const { communityRes, commentsRes, postsRes } = this.isoData.routeData;
        this.state = {
          ...this.state,
+         isIsomorphic: true,
+         commentsRes,
          communityRes,
          postsRes,
-         commentsRes,
-         isIsomorphic: true,
        };
      }
    }
      saveScrollPosition(this.context);
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      path,
      query: { dataType: urlDataType, page: urlPage, sort: urlSort },
      auth,
    }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
-     RequestState<any>
-   >[] {
+     Promise<CommunityData>
+   > {
      const pathSplit = path.split("/");
-     const promises: Promise<RequestState<any>>[] = [];
  
      const communityName = pathSplit[2];
      const communityForm: GetCommunity = {
        name: communityName,
        auth,
      };
-     promises.push(client.getCommunity(communityForm));
  
      const dataType = getDataTypeFromQuery(urlDataType);
  
  
      const page = getPageFromString(urlPage);
  
+     let postsResponse: RequestState<GetPostsResponse> = { state: "empty" };
+     let commentsResponse: RequestState<GetCommentsResponse> = {
+       state: "empty",
+     };
      if (dataType === DataType.Post) {
        const getPostsForm: GetPosts = {
          community_name: communityName,
          saved_only: false,
          auth,
        };
-       promises.push(client.getPosts(getPostsForm));
-       promises.push(Promise.resolve({ state: "empty" }));
+       postsResponse = await client.getPosts(getPostsForm);
      } else {
        const getCommentsForm: GetComments = {
          community_name: communityName,
          saved_only: false,
          auth,
        };
-       promises.push(Promise.resolve({ state: "empty" }));
-       promises.push(client.getComments(getCommentsForm));
+       commentsResponse = await client.getComments(getCommentsForm);
      }
  
-     return promises;
+     return {
+       communityRes: await client.getCommunity(communityForm),
+       commentsRes: commentsResponse,
+       postsRes: postsResponse,
+     };
    }
  
    get documentTitle(): string {
index 997fe11ad71dad96de7ecf9b2e893ee5069a1681,720e596fb6a13f0b876cd046c2d2bf9b8fef4b8b..508e5a0d7cc86ac55a808b6913f4601ac6d3debc
@@@ -17,15 -17,15 +17,15 @@@ import 
  import { i18n } from "../../i18next";
  import { UserService } from "../../services";
  import {
 -  amAdmin,
 -  amMod,
 -  amTopMod,
    getUnixTime,
    hostname,
    mdToHtml,
    myAuthRequired,
-   numToSI,
  } from "../../utils";
 +import { amAdmin } from "../../utils/roles/am-admin";
 +import { amMod } from "../../utils/roles/am-mod";
 +import { amTopMod } from "../../utils/roles/am-top-mod";
+ import { Badges } from "../common/badges";
  import { BannerIconHeader } from "../common/banner-icon-header";
  import { Icon, PurgeWarning, Spinner } from "../common/icon";
  import { CommunityForm } from "../community/community-form";
@@@ -158,7 -158,10 +158,10 @@@ export class Sidebar extends Component<
            <section id="sidebarInfo" className="card border-secondary mb-3">
              <div className="card-body">
                {this.description()}
-               {this.badges()}
+               <Badges
+                 communityId={this.props.community_view.community.id}
+                 counts={this.props.community_view.counts}
+               />
                {this.mods()}
              </div>
            </section>
      );
    }
  
-   badges() {
-     const community_view = this.props.community_view;
-     const counts = community_view.counts;
-     return (
-       <ul className="my-1 list-inline">
-         <li
-           className="list-inline-item badge badge-secondary pointer"
-           data-tippy-content={i18n.t("active_users_in_the_last_day", {
-             count: Number(counts.users_active_day),
-             formattedCount: numToSI(counts.users_active_day),
-           })}
-         >
-           {i18n.t("number_of_users", {
-             count: Number(counts.users_active_day),
-             formattedCount: numToSI(counts.users_active_day),
-           })}{" "}
-           / {i18n.t("day")}
-         </li>
-         <li
-           className="list-inline-item badge badge-secondary pointer"
-           data-tippy-content={i18n.t("active_users_in_the_last_week", {
-             count: Number(counts.users_active_week),
-             formattedCount: numToSI(counts.users_active_week),
-           })}
-         >
-           {i18n.t("number_of_users", {
-             count: Number(counts.users_active_week),
-             formattedCount: numToSI(counts.users_active_week),
-           })}{" "}
-           / {i18n.t("week")}
-         </li>
-         <li
-           className="list-inline-item badge badge-secondary pointer"
-           data-tippy-content={i18n.t("active_users_in_the_last_month", {
-             count: Number(counts.users_active_month),
-             formattedCount: numToSI(counts.users_active_month),
-           })}
-         >
-           {i18n.t("number_of_users", {
-             count: Number(counts.users_active_month),
-             formattedCount: numToSI(counts.users_active_month),
-           })}{" "}
-           / {i18n.t("month")}
-         </li>
-         <li
-           className="list-inline-item badge badge-secondary pointer"
-           data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
-             count: Number(counts.users_active_half_year),
-             formattedCount: numToSI(counts.users_active_half_year),
-           })}
-         >
-           {i18n.t("number_of_users", {
-             count: Number(counts.users_active_half_year),
-             formattedCount: numToSI(counts.users_active_half_year),
-           })}{" "}
-           / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
-         </li>
-         <li className="list-inline-item badge badge-secondary">
-           {i18n.t("number_of_subscribers", {
-             count: Number(counts.subscribers),
-             formattedCount: numToSI(counts.subscribers),
-           })}
-         </li>
-         <li className="list-inline-item badge badge-secondary">
-           {i18n.t("number_of_posts", {
-             count: Number(counts.posts),
-             formattedCount: numToSI(counts.posts),
-           })}
-         </li>
-         <li className="list-inline-item badge badge-secondary">
-           {i18n.t("number_of_comments", {
-             count: Number(counts.comments),
-             formattedCount: numToSI(counts.comments),
-           })}
-         </li>
-         <li className="list-inline-item">
-           <Link
-             className="badge badge-primary"
-             to={`/modlog/${this.props.community_view.community.id}`}
-           >
-             {i18n.t("modlog")}
-           </Link>
-         </li>
-       </ul>
-     );
-   }
    mods() {
      return (
        <ul className="list-inline small">
index 9f7c322f720ce07d01f0505312364f4f0576d07c,4d79bc6359e1fc8f069310e3bca173640243284b..54378f340fc4552f1dbd517c2bde9a8cdc01d7dd
@@@ -57,6 -57,7 +57,6 @@@ import { UserService } from "../../serv
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  canCreateCommunity,
    commentsToFlatNodes,
    editComment,
    editPost,
    getCommentParentId,
    getDataTypeString,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    getRandomFromList,
    mdToHtml,
    myAuth,
    postToCommentSortType,
 -  QueryParams,
    relTags,
    restoreScrollPosition,
+   RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
    trendingFetchLimit,
    updatePersonBlock,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import { getQueryString } from "../../utils/helpers/get-query-string";
 +import { canCreateCommunity } from "../../utils/roles/can-create-community";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { CommentNodes } from "../comment/comment-nodes";
  import { DataTypeSelect } from "../common/data-type-select";
  import { HtmlTags } from "../common/html-tags";
@@@ -117,6 -118,45 +118,45 @@@ interface HomeProps 
    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;
  }
@@@ -176,7 -216,7 +216,7 @@@ const LinkButton = (
  );
  
  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" },
  
      // 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,
+         commentsRes,
+         postsRes,
          tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
            ?.content,
          isIsomorphic: true,
    }
  
    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()]);
      }
  
      saveScrollPosition(this.context);
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      auth,
      query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
-   }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<
-     RequestState<any>
-   >[] {
+   }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
      const dataType = getDataTypeFromQuery(urlDataType);
  
      // TODO figure out auth default_listingType, default_sort_type
  
      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 = {
          auth,
        };
  
-       promises.push(client.getPosts(getPostsForm));
-       promises.push(Promise.resolve({ state: "empty" }));
+       postsRes = await client.getPosts(getPostsForm);
      } else {
        const getCommentsForm: GetComments = {
          page,
          saved_only: false,
          auth,
        };
-       promises.push(Promise.resolve({ state: "empty" }));
-       promises.push(client.getComments(getCommentsForm));
+       commentsRes = await client.getComments(getCommentsForm);
      }
  
      const trendingCommunitiesForm: ListCommunities = {
        limit: trendingFetchLimit,
        auth,
      };
-     promises.push(client.listCommunities(trendingCommunitiesForm));
  
-     return promises;
+     return {
+       trendingCommunitiesRes: await client.listCommunities(
+         trendingCommunitiesForm
+       ),
+       commentsRes,
+       postsRes,
+     };
    }
  
    get documentTitle(): string {
                  ></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">
                {this.mySidebar}
      await this.fetchData();
    }
  
-   posts() {
+   get posts() {
      const { page } = getHomeQueryParams();
  
      return (
      const siteRes = this.state.siteRes;
  
      if (dataType === DataType.Post) {
-       switch (this.state.postsRes?.state) {
+       switch (this.state.postsRes.state) {
          case "loading":
            return (
              <h5>
          <span className="mr-2">
            <SortSelect sort={sort} onChange={this.handleSortChange} />
          </span>
-         {this.getRss(listingType)}
+         {getRss(listingType)}
        </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({
index 99f15e501204b474886512b3cf2c0c5833684bf8,b3f1fff1542c44b440c166aa1d6a62b1870945bf..2db3d4b10d251225bff135b4cebe66edc8110642
@@@ -13,6 -13,7 +13,7 @@@ import 
    GetModlog,
    GetModlogResponse,
    GetPersonDetails,
+   GetPersonDetailsResponse,
    ModAddCommunityView,
    ModAddView,
    ModBanFromCommunityView,
@@@ -33,21 -34,22 +34,21 @@@ import { FirstLoadService } from "../se
  import { HttpService, RequestState } from "../services/HttpService";
  import {
    Choice,
 -  QueryParams,
 -  RouteDataResponse,
 -  amAdmin,
 -  amMod,
 -  debounce,
    fetchLimit,
    fetchUsers,
    getIdFromString,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    getUpdatedSearchId,
    myAuth,
    personToChoice,
    setIsoData,
  } from "../utils";
 +import { debounce } from "../utils/helpers/debounce";
 +import { getQueryParams } from "../utils/helpers/get-query-params";
 +import { getQueryString } from "../utils/helpers/get-query-string";
 +import { amAdmin } from "../utils/roles/am-admin";
 +import { amMod } from "../utils/roles/am-mod";
 +import type { QueryParams } from "../utils/types/query-params";
  import { HtmlTags } from "./common/html-tags";
  import { Icon, Spinner } from "./common/icon";
  import { MomentTime } from "./common/moment-time";
@@@ -74,6 -76,13 +75,13 @@@ type View 
    | AdminPurgePostView
    | AdminPurgeCommentView;
  
+ type ModlogData = RouteDataResponse<{
+   res: GetModlogResponse;
+   communityRes: GetCommunityResponse;
+   modUserResponse: GetPersonDetailsResponse;
+   userResponse: GetPersonDetailsResponse;
+ }>;
  interface ModlogType {
    id: number;
    type_: ModlogActionType;
@@@ -631,7 -640,7 +639,7 @@@ export class Modlog extends Component
    RouteComponentProps<{ communityId?: string }>,
    ModlogState
  > {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<ModlogData>(this.context);
  
    state: ModlogState = {
      res: { state: "empty" },
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [res, communityRes, filteredModRes, filteredUserRes] =
+       const { res, communityRes, modUserResponse, userResponse } =
          this.isoData.routeData;
        this.state = {
          ...this.state,
          res,
          communityRes,
        };
  
-       if (filteredModRes.state === "success") {
+       if (modUserResponse.state === "success") {
          this.state = {
            ...this.state,
-           modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
+           modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
          };
        }
  
-       if (filteredUserRes.state === "success") {
+       if (userResponse.state === "success") {
          this.state = {
            ...this.state,
-           userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
+           userSearchOptions: [personToChoice(userResponse.data.person_view)],
          };
        }
      }
      }
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      path,
      query: { modId: urlModId, page, userId: urlUserId, actionType },
      auth,
      site,
-   }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<
-     RequestState<any>
-   >[] {
+   }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
      const pathSplit = path.split("/");
-     const promises: Promise<RequestState<any>>[] = [];
      const communityId = getIdFromString(pathSplit[2]);
      const modId = !site.site_view.local_site.hide_modlog_mod_names
        ? getIdFromString(urlModId)
        auth,
      };
  
-     promises.push(client.getModlog(modlogForm));
+     let communityResponse: RequestState<GetCommunityResponse> = {
+       state: "empty",
+     };
  
      if (communityId) {
        const communityForm: GetCommunity = {
          id: communityId,
          auth,
        };
-       promises.push(client.getCommunity(communityForm));
-     } else {
-       promises.push(Promise.resolve({ state: "empty" }));
+       communityResponse = await client.getCommunity(communityForm);
      }
  
+     let modUserResponse: RequestState<GetPersonDetailsResponse> = {
+       state: "empty",
+     };
      if (modId) {
        const getPersonForm: GetPersonDetails = {
          person_id: modId,
          auth,
        };
  
-       promises.push(client.getPersonDetails(getPersonForm));
-     } else {
-       promises.push(Promise.resolve({ state: "empty" }));
+       modUserResponse = await client.getPersonDetails(getPersonForm);
      }
  
+     let userResponse: RequestState<GetPersonDetailsResponse> = {
+       state: "empty",
+     };
      if (userId) {
        const getPersonForm: GetPersonDetails = {
          person_id: userId,
          auth,
        };
  
-       promises.push(client.getPersonDetails(getPersonForm));
-     } else {
-       promises.push(Promise.resolve({ state: "empty" }));
+       userResponse = await client.getPersonDetails(getPersonForm);
      }
  
-     return promises;
+     return {
+       res: await client.getModlog(modlogForm),
+       communityRes: communityResponse,
+       modUserResponse,
+       userResponse,
+     };
    }
  }
index c12114bc8f3d8e4fa2ccfb32dfdfb897b7dbad37,5466bc5fcb746b2d388a7642725208e007201857..b6a3200d62bf833dd0316878711e160b2eceeee2
@@@ -53,6 -53,9 +53,6 @@@ import { UserService } from "../../serv
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  QueryParams,
 -  RouteDataResponse,
 -  canMod,
    capitalizeFirstLetter,
    editComment,
    editPost,
    futureDaysToUnixTime,
    getCommentParentId,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
 -  isAdmin,
 -  isBanned,
    mdToHtml,
    myAuth,
    myAuthRequired,
    toast,
    updatePersonBlock,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import { getQueryString } from "../../utils/helpers/get-query-string";
 +import { canMod } from "../../utils/roles/can-mod";
 +import { isAdmin } from "../../utils/roles/is-admin";
 +import { isBanned } from "../../utils/roles/is-banned";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { BannerIconHeader } from "../common/banner-icon-header";
  import { HtmlTags } from "../common/html-tags";
  import { Icon, Spinner } from "../common/icon";
@@@ -90,6 -91,10 +90,10 @@@ import { CommunityLink } from "../commu
  import { PersonDetails } from "./person-details";
  import { PersonListing } from "./person-listing";
  
+ type ProfileData = RouteDataResponse<{
+   personResponse: GetPersonDetailsResponse;
+ }>;
  interface ProfileState {
    personRes: RequestState<GetPersonDetailsResponse>;
    personBlocked: boolean;
@@@ -156,7 -161,7 +160,7 @@@ export class Profile extends Component
    RouteComponentProps<{ username: string }>,
    ProfileState
  > {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<ProfileData>(this.context);
    state: ProfileState = {
      personRes: { state: "empty" },
      personBlocked: false,
      if (FirstLoadService.isFirstLoad) {
        this.state = {
          ...this.state,
-         personRes: this.isoData.routeData[0],
+         personRes: this.isoData.routeData.personResponse,
          isIsomorphic: true,
        };
      }
      }
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      path,
      query: { page, sort, view: urlView },
      auth,
-   }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<
-     RequestState<any>
-   >[] {
+   }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
      const pathSplit = path.split("/");
  
      const username = pathSplit[2];
        auth,
      };
  
-     return [client.getPersonDetails(form)];
+     return {
+       personResponse: await client.getPersonDetails(form),
+     };
    }
  
    get documentTitle(): string {
index 187fe4c2e0e06d71548793e49df30728e62ec2ce,99a0333645fe4e719b031eefbba68c5e53862780..0be753791d4bb192456a74f5188319d960852041
@@@ -23,6 -23,8 +23,7 @@@ import { HttpService, UserService } fro
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { RequestState } from "../../services/HttpService";
  import {
 -  amAdmin,
+   RouteDataResponse,
    editCommentReport,
    editPostReport,
    editPrivateMessageReport,
@@@ -30,7 -32,6 +31,7 @@@
    myAuthRequired,
    setIsoData,
  } from "../../utils";
 +import { amAdmin } from "../../utils/roles/am-admin";
  import { CommentReport } from "../comment/comment-report";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
@@@ -56,6 -57,12 +57,12 @@@ enum MessageEnum 
    PrivateMessageReport,
  }
  
+ type ReportsData = RouteDataResponse<{
+   commentReportsRes: ListCommentReportsResponse;
+   postReportsRes: ListPostReportsResponse;
+   messageReportsRes: ListPrivateMessageReportsResponse;
+ }>;
  type ItemType = {
    id: number;
    type_: MessageEnum;
@@@ -75,7 -82,7 +82,7 @@@ interface ReportsState 
  }
  
  export class Reports extends Component<any, ReportsState> {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<ReportsData>(this.context);
    state: ReportsState = {
      commentReportsRes: { state: "empty" },
      postReportsRes: { state: "empty" },
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [commentReportsRes, postReportsRes, messageReportsRes] =
+       const { commentReportsRes, postReportsRes, messageReportsRes } =
          this.isoData.routeData;
        this.state = {
          ...this.state,
          commentReportsRes,
        if (amAdmin()) {
          this.state = {
            ...this.state,
-           messageReportsRes,
+           messageReportsRes: messageReportsRes,
          };
        }
      }
      await i.refetch();
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      auth,
      client,
-   }: InitialFetchRequest): Promise<any>[] {
-     const promises: Promise<RequestState<any>>[] = [];
+   }: InitialFetchRequest): Promise<ReportsData> {
      const unresolved_only = true;
      const page = 1;
      const limit = fetchLimit;
  
-     if (auth) {
-       const commentReportsForm: ListCommentReports = {
-         unresolved_only,
-         page,
-         limit,
-         auth,
-       };
-       promises.push(client.listCommentReports(commentReportsForm));
+     const commentReportsForm: ListCommentReports = {
+       unresolved_only,
+       page,
+       limit,
+       auth: auth as string,
+     };
  
-       const postReportsForm: ListPostReports = {
+     const postReportsForm: ListPostReports = {
+       unresolved_only,
+       page,
+       limit,
+       auth: auth as string,
+     };
+     const data: ReportsData = {
+       commentReportsRes: await client.listCommentReports(commentReportsForm),
+       postReportsRes: await client.listPostReports(postReportsForm),
+       messageReportsRes: { state: "empty" },
+     };
+     if (amAdmin()) {
+       const privateMessageReportsForm: ListPrivateMessageReports = {
          unresolved_only,
          page,
          limit,
-         auth,
+         auth: auth as string,
        };
-       promises.push(client.listPostReports(postReportsForm));
  
-       if (amAdmin()) {
-         const privateMessageReportsForm: ListPrivateMessageReports = {
-           unresolved_only,
-           page,
-           limit,
-           auth,
-         };
-         promises.push(
-           client.listPrivateMessageReports(privateMessageReportsForm)
-         );
-       } else {
-         promises.push(Promise.resolve({ state: "empty" }));
-       }
-     } else {
-       promises.push(
-         Promise.resolve({ state: "empty" }),
-         Promise.resolve({ state: "empty" }),
-         Promise.resolve({ state: "empty" })
+       data.messageReportsRes = await client.listPrivateMessageReports(
+         privateMessageReportsForm
        );
      }
  
-     return promises;
+     return data;
    }
  
    async refetch() {
index 63ee390f8ad0d85e36b5a040bef25b80c16460a5,278977977bb9a2df8511cb71d942c71dd82461ed..7df628b2b2b0461439eb2846285c993ef6cf5c7d
@@@ -3,6 -3,7 +3,7 @@@ import { RouteComponentProps } from "in
  import {
    CreatePost as CreatePostI,
    GetCommunity,
+   GetCommunityResponse,
    GetSiteResponse,
    ListCommunitiesResponse,
  } from "lemmy-js-client";
@@@ -16,14 -17,15 +17,15 @@@ import 
  } from "../../services/HttpService";
  import {
    Choice,
 -  QueryParams,
+   RouteDataResponse,
    enableDownvotes,
    enableNsfw,
    getIdFromString,
 -  getQueryParams,
    myAuth,
    setIsoData,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
  import { PostForm } from "./post-form";
@@@ -32,6 -34,11 +34,11 @@@ export interface CreatePostProps 
    communityId?: number;
  }
  
+ type CreatePostData = RouteDataResponse<{
+   communityResponse: GetCommunityResponse;
+   initialCommunitiesRes: ListCommunitiesResponse;
+ }>;
  function getCreatePostQueryParams() {
    return getQueryParams<CreatePostProps>({
      communityId: getIdFromString,
@@@ -54,7 -61,7 +61,7 @@@ export class CreatePost extends Compone
    RouteComponentProps<Record<string, never>>,
    CreatePostState
  > {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<CreatePostData>(this.context);
    state: CreatePostState = {
      siteRes: this.isoData.site_res,
      loading: true,
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [communityRes, listCommunitiesRes] = this.isoData.routeData;
+       const { communityResponse: communityRes, initialCommunitiesRes } =
+         this.isoData.routeData;
+       this.state = {
+         ...this.state,
+         loading: false,
+         initialCommunitiesRes,
+         isIsomorphic: true,
+       };
  
        if (communityRes?.state === "success") {
          const communityChoice: Choice = {
            selectedCommunityChoice: communityChoice,
          };
        }
-       this.state = {
-         ...this.state,
-         loading: false,
-         initialCommunitiesRes: listCommunitiesRes,
-         isIsomorphic: true,
-       };
      }
    }
  
      }
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      query: { communityId },
      auth,
-   }: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
-     RequestState<any>
-   >[] {
-     const promises: Promise<RequestState<any>>[] = [];
+   }: InitialFetchRequest<
+     QueryParams<CreatePostProps>
+   >): Promise<CreatePostData> {
+     const data: CreatePostData = {
+       initialCommunitiesRes: await fetchCommunitiesForOptions(client),
+       communityResponse: { state: "empty" },
+     };
  
      if (communityId) {
        const form: GetCommunity = {
          id: getIdFromString(communityId),
        };
  
-       promises.push(client.getCommunity(form));
-     } else {
-       promises.push(Promise.resolve({ state: "empty" }));
+       data.communityResponse = await client.getCommunity(form);
      }
  
-     promises.push(fetchCommunitiesForOptions(client));
-     return promises;
+     return data;
    }
  }
index e4d5f77267137e0f4034b36354d848079d62365e,b9d4125075fff34fb8d9b1eaba4156205dfdcf4f..a95599b258e74c5fd480ce1fa418d28a0078662c
@@@ -28,9 -28,18 +28,9 @@@ import { i18n } from "../../i18next"
  import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces";
  import { UserService } from "../../services";
  import {
 -  amAdmin,
 -  amCommunityCreator,
 -  amMod,
 -  canAdmin,
 -  canMod,
 -  canShare,
    futureDaysToUnixTime,
    hostname,
 -  isAdmin,
 -  isBanned,
    isImage,
 -  isMod,
    isVideo,
    mdNoImages,
    mdToHtml,
    numToSI,
    relTags,
    setupTippy,
 -  share,
    showScores,
  } from "../../utils";
 +import { canShare } from "../../utils/browser/can-share";
 +import { share } from "../../utils/browser/share";
 +import { amAdmin } from "../../utils/roles/am-admin";
 +import { amCommunityCreator } from "../../utils/roles/am-community-creator";
 +import { amMod } from "../../utils/roles/am-mod";
 +import { canAdmin } from "../../utils/roles/can-admin";
 +import { canMod } from "../../utils/roles/can-mod";
 +import { isAdmin } from "../../utils/roles/is-admin";
 +import { isBanned } from "../../utils/roles/is-banned";
 +import { isMod } from "../../utils/roles/is-mod";
  import { Icon, PurgeWarning, Spinner } from "../common/icon";
  import { MomentTime } from "../common/moment-time";
  import { PictrsImage } from "../common/pictrs-image";
@@@ -631,7 -631,7 +631,7 @@@ export class PostListing extends Compon
      const post = this.postView.post;
  
      return (
-       <div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
+       <div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
          {this.commentsButton}
          {canShare() && (
            <button
index 1c4b6816baaef582a17c1253e2aa172331ba3076,a7365e0d00337f69b96cb700769f38f89b082565..3dee31a7710c79440baef4123ff76d98ff790d57
@@@ -64,6 -64,7 +64,6 @@@ import 
    buildCommentsTree,
    commentsToFlatNodes,
    commentTreeMaxDepth,
 -  debounce,
    editComment,
    editWith,
    enableDownvotes,
    getCommentParentId,
    getDepthFromComment,
    getIdFromProps,
 -  isBrowser,
    isImage,
    myAuth,
    restoreScrollPosition,
+   RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -82,8 -85,6 +83,8 @@@
    updateCommunityBlock,
    updatePersonBlock,
  } from "../../utils";
 +import { isBrowser } from "../../utils/browser/is-browser";
 +import { debounce } from "../../utils/helpers/debounce";
  import { CommentForm } from "../comment/comment-form";
  import { CommentNodes } from "../comment/comment-nodes";
  import { HtmlTags } from "../common/html-tags";
@@@ -93,6 -94,11 +94,11 @@@ import { PostListing } from "./post-lis
  
  const commentsShownInterval = 15;
  
+ type PostData = RouteDataResponse<{
+   postRes: GetPostResponse;
+   commentsRes: GetCommentsResponse;
+ }>;
  interface PostState {
    postId?: number;
    commentId?: number;
  }
  
  export class Post extends Component<any, PostState> {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<PostData>(this.context);
    private commentScrollDebounced: () => void;
    state: PostState = {
      postRes: { state: "empty" },
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [postRes, commentsRes] = this.isoData.routeData;
+       const { commentsRes, postRes } = this.isoData.routeData;
  
        this.state = {
          ...this.state,
      }
    }
  
-   static fetchInitialData({
-     auth,
+   static async fetchInitialData({
      client,
      path,
-   }: InitialFetchRequest): Promise<any>[] {
+     auth,
+   }: InitialFetchRequest): Promise<PostData> {
      const pathSplit = path.split("/");
-     const promises: Promise<RequestState<any>>[] = [];
  
      const pathType = pathSplit.at(1);
      const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
        commentsForm.parent_id = id;
      }
  
-     promises.push(client.getPost(postForm));
-     promises.push(client.getComments(commentsForm));
-     return promises;
+     return {
+       postRes: await client.getPost(postForm),
+       commentsRes: await client.getComments(commentsForm),
+     };
    }
  
    componentWillUnmount() {
index d32e408755de63e2f6b23f98c18a677e4d927b4a,054cab016be94dd34e2b3294a1bd4825590cc75d..e56056acc01baa4abbc9b905b350f9c4acad786f
@@@ -26,9 -26,12 +26,10 @@@ import { FirstLoadService } from "../se
  import { HttpService, RequestState } from "../services/HttpService";
  import {
    Choice,
 -  QueryParams,
+   RouteDataResponse,
    capitalizeFirstLetter,
    commentsToFlatNodes,
    communityToChoice,
 -  debounce,
    enableDownvotes,
    enableNsfw,
    fetchCommunities,
@@@ -36,6 -39,8 +37,6 @@@
    fetchUsers,
    getIdFromString,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    getUpdatedSearchId,
    myAuth,
    numToSI,
    setIsoData,
    showLocal,
  } from "../utils";
 +import { debounce } from "../utils/helpers/debounce";
 +import { getQueryParams } from "../utils/helpers/get-query-params";
 +import { getQueryString } from "../utils/helpers/get-query-string";
 +import type { QueryParams } from "../utils/types/query-params";
  import { CommentNodes } from "./comment/comment-nodes";
  import { HtmlTags } from "./common/html-tags";
  import { Spinner } from "./common/icon";
@@@ -70,6 -71,14 +71,14 @@@ interface SearchProps 
    page: number;
  }
  
+ type SearchData = RouteDataResponse<{
+   communityResponse: GetCommunityResponse;
+   listCommunitiesResponse: ListCommunitiesResponse;
+   creatorDetailsResponse: GetPersonDetailsResponse;
+   searchResponse: SearchResponse;
+   resolveObjectResponse: ResolveObjectResponse;
+ }>;
  type FilterType = "creator" | "community";
  
  interface SearchState {
@@@ -228,7 -237,8 +237,8 @@@ function getListing
  }
  
  export class Search extends Component<any, SearchState> {
-   private isoData = setIsoData(this.context);
+   private isoData = setIsoData<SearchData>(this.context);
    state: SearchState = {
      resolveObjectRes: { state: "empty" },
      creatorDetailsRes: { state: "empty" },
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
-       const [
-         communityRes,
-         communitiesRes,
-         creatorDetailsRes,
-         searchRes,
-         resolveObjectRes,
-       ] = this.isoData.routeData;
+       const {
+         communityResponse: communityRes,
+         creatorDetailsResponse: creatorDetailsRes,
+         listCommunitiesResponse: communitiesRes,
+         resolveObjectResponse: resolveObjectRes,
+         searchResponse: searchRes,
+       } = this.isoData.routeData;
  
        this.state = {
          ...this.state,
-         communitiesRes,
-         communityRes,
-         creatorDetailsRes,
-         creatorSearchOptions:
-           creatorDetailsRes.state == "success"
-             ? [personToChoice(creatorDetailsRes.data.person_view)]
-             : [],
          isIsomorphic: true,
        };
  
-       if (communityRes.state === "success") {
+       if (creatorDetailsRes?.state === "success") {
+         this.state = {
+           ...this.state,
+           creatorSearchOptions:
+             creatorDetailsRes?.state === "success"
+               ? [personToChoice(creatorDetailsRes.data.person_view)]
+               : [],
+           creatorDetailsRes,
+         };
+       }
+       if (communitiesRes?.state === "success") {
          this.state = {
            ...this.state,
-           communitySearchOptions: [
-             communityToChoice(communityRes.data.community_view),
-           ],
+           communitiesRes,
          };
        }
  
-       if (q) {
+       if (communityRes?.state === "success") {
          this.state = {
            ...this.state,
-           searchRes,
-           resolveObjectRes,
+           communityRes,
          };
        }
+       if (q !== "") {
+         this.state = {
+           ...this.state,
+         };
+         if (searchRes?.state === "success") {
+           this.state = {
+             ...this.state,
+             searchRes,
+           };
+         }
+         if (resolveObjectRes?.state === "success") {
+           this.state = {
+             ...this.state,
+             resolveObjectRes,
+           };
+         }
+       }
      }
    }
  
      saveScrollPosition(this.context);
    }
  
-   static fetchInitialData({
+   static async fetchInitialData({
      client,
      auth,
      query: { communityId, creatorId, q, type, sort, listingType, page },
-   }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
-     RequestState<any>
-   >[] {
-     const promises: Promise<RequestState<any>>[] = [];
+   }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
      const community_id = getIdFromString(communityId);
+     let communityResponse: RequestState<GetCommunityResponse> = {
+       state: "empty",
+     };
+     let listCommunitiesResponse: RequestState<ListCommunitiesResponse> = {
+       state: "empty",
+     };
      if (community_id) {
        const getCommunityForm: GetCommunity = {
          id: community_id,
          auth,
        };
-       promises.push(client.getCommunity(getCommunityForm));
-       promises.push(Promise.resolve({ state: "empty" }));
+       communityResponse = await client.getCommunity(getCommunityForm);
      } else {
        const listCommunitiesForm: ListCommunities = {
          type_: defaultListingType,
          limit: fetchLimit,
          auth,
        };
-       promises.push(Promise.resolve({ state: "empty" }));
-       promises.push(client.listCommunities(listCommunitiesForm));
+       listCommunitiesResponse = await client.listCommunities(
+         listCommunitiesForm
+       );
      }
  
      const creator_id = getIdFromString(creatorId);
+     let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = {
+       state: "empty",
+     };
      if (creator_id) {
        const getCreatorForm: GetPersonDetails = {
          person_id: creator_id,
          auth,
        };
-       promises.push(client.getPersonDetails(getCreatorForm));
-     } else {
-       promises.push(Promise.resolve({ state: "empty" }));
+       creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
      }
  
      const query = getSearchQueryFromQuery(q);
  
+     let searchResponse: RequestState<SearchResponse> = { state: "empty" };
+     let resolveObjectResponse: RequestState<ResolveObjectResponse> = {
+       state: "empty",
+     };
      if (query) {
        const form: SearchForm = {
          q: query,
        };
  
        if (query !== "") {
-         promises.push(client.search(form));
+         searchResponse = await client.search(form);
          if (auth) {
            const resolveObjectForm: ResolveObject = {
              q: query,
              auth,
            };
-           promises.push(client.resolveObject(resolveObjectForm));
+           resolveObjectResponse = await client.resolveObject(resolveObjectForm);
          }
-       } else {
-         promises.push(Promise.resolve({ state: "empty" }));
-         promises.push(Promise.resolve({ state: "empty" }));
        }
      }
  
-     return promises;
+     return {
+       communityResponse,
+       creatorDetailsResponse,
+       listCommunitiesResponse,
+       resolveObjectResponse,
+       searchResponse,
+     };
    }
  
    get documentTitle(): string {
            minLength={1}
          />
          <button type="submit" className="btn btn-secondary mr-2 mb-2">
-           {this.state.searchRes.state == "loading" ? (
+           {this.state.searchRes.state === "loading" ? (
              <Spinner />
            ) : (
              <span>{i18n.t("search")}</span>
diff --combined src/shared/utils.ts
index 6f6e61c74e2d95ea75f3123fb4885d21abeced9e,c7fbca6b5654db597b098c452af5dc6ed522273b..be461dcd0145ab3fabdb9405407b210ebc2d5bd3
@@@ -9,6 -9,7 +9,6 @@@ import 
    CommentReportView,
    CommentSortType,
    CommentView,
 -  CommunityModeratorView,
    CommunityView,
    CustomEmojiView,
    GetSiteMetadata,
@@@ -16,6 -17,7 +16,6 @@@
    Language,
    LemmyHttp,
    MyUserInfo,
 -  Person,
    PersonMentionView,
    PersonView,
    PostReportView,
@@@ -41,11 -43,15 +41,18 @@@ import tippy from "tippy.js"
  import Toastify from "toastify-js";
  import { getHttpBase } from "./env";
  import { i18n } from "./i18next";
- import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
+ import {
+   CommentNodeI,
+   DataType,
+   IsoData,
+   RouteData,
+   VoteType,
+ } from "./interfaces";
  import { HttpService, UserService } from "./services";
 +import { isBrowser } from "./utils/browser/is-browser";
 +import { debounce } from "./utils/helpers/debounce";
 +import { groupBy } from "./utils/helpers/group-by";
+ import { RequestState } from "./services/HttpService";
  
  let Tribute: any;
  if (isBrowser()) {
@@@ -229,6 -235,92 +236,6 @@@ export function futureDaysToUnixTime(da
      : undefined;
  }
  
 -export function canMod(
 -  creator_id: number,
 -  mods?: CommunityModeratorView[],
 -  admins?: PersonView[],
 -  myUserInfo = UserService.Instance.myUserInfo,
 -  onSelf = false
 -): boolean {
 -  // You can do moderator actions only on the mods added after you.
 -  let adminsThenMods =
 -    admins
 -      ?.map(a => a.person.id)
 -      .concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
 -
 -  if (myUserInfo) {
 -    const myIndex = adminsThenMods.findIndex(
 -      id => id == myUserInfo.local_user_view.person.id
 -    );
 -    if (myIndex == -1) {
 -      return false;
 -    } else {
 -      // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
 -      adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1));
 -      return !adminsThenMods.includes(creator_id);
 -    }
 -  } else {
 -    return false;
 -  }
 -}
 -
 -export function canAdmin(
 -  creatorId: number,
 -  admins?: PersonView[],
 -  myUserInfo = UserService.Instance.myUserInfo,
 -  onSelf = false
 -): boolean {
 -  return canMod(creatorId, undefined, admins, myUserInfo, onSelf);
 -}
 -
 -export function isMod(
 -  creatorId: number,
 -  mods?: CommunityModeratorView[]
 -): boolean {
 -  return mods?.map(m => m.moderator.id).includes(creatorId) ?? false;
 -}
 -
 -export function amMod(
 -  mods?: CommunityModeratorView[],
 -  myUserInfo = UserService.Instance.myUserInfo
 -): boolean {
 -  return myUserInfo ? isMod(myUserInfo.local_user_view.person.id, mods) : false;
 -}
 -
 -export function isAdmin(creatorId: number, admins?: PersonView[]): boolean {
 -  return admins?.map(a => a.person.id).includes(creatorId) ?? false;
 -}
 -
 -export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean {
 -  return myUserInfo?.local_user_view.person.admin ?? false;
 -}
 -
 -export function amCommunityCreator(
 -  creator_id: number,
 -  mods?: CommunityModeratorView[],
 -  myUserInfo = UserService.Instance.myUserInfo
 -): boolean {
 -  const myId = myUserInfo?.local_user_view.person.id;
 -  // Don't allow mod actions on yourself
 -  return myId == mods?.at(0)?.moderator.id && myId != creator_id;
 -}
 -
 -export function amSiteCreator(
 -  creator_id: number,
 -  admins?: PersonView[],
 -  myUserInfo = UserService.Instance.myUserInfo
 -): boolean {
 -  const myId = myUserInfo?.local_user_view.person.id;
 -  return myId == admins?.at(0)?.person.id && myId != creator_id;
 -}
 -
 -export function amTopMod(
 -  mods: CommunityModeratorView[],
 -  myUserInfo = UserService.Instance.myUserInfo
 -): boolean {
 -  return mods.at(0)?.moderator.id == myUserInfo?.local_user_view.person.id;
 -}
 -
  const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
  const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/;
  const tldRegex = /([a-z0-9]+\.)*[a-z0-9]+\.[a-z]+/;
@@@ -242,7 -334,12 +249,12 @@@ export function isVideo(url: string) 
  }
  
  export function validURL(str: string) {
-   return !!new URL(str);
+   try {
+     new URL(str);
+     return true;
+   } catch {
+     return false;
+   }
  }
  
  export function validInstanceTLD(str: string) {
@@@ -274,6 -371,51 +286,6 @@@ export function getDataTypeString(dt: D
    return dt === DataType.Post ? "Post" : "Comment";
  }
  
 -export function debounce<T extends any[], R>(
 -  func: (...e: T) => R,
 -  wait = 1000,
 -  immediate = false
 -) {
 -  // 'private' variable for instance
 -  // The returned function will be able to reference this due to closure.
 -  // Each call to the returned function will share this common timer.
 -  let timeout: NodeJS.Timeout | null;
 -
 -  // Calling debounce returns a new anonymous function
 -  return function () {
 -    // reference the context and args for the setTimeout function
 -    const args = arguments;
 -
 -    // Should the function be called now? If immediate is true
 -    //   and not already in a timeout then the answer is: Yes
 -    const callNow = immediate && !timeout;
 -
 -    // This is the basic debounce behavior where you can call this
 -    //   function several times, but it will only execute once
 -    //   [before or after imposing a delay].
 -    //   Each time the returned function is called, the timer starts over.
 -    clearTimeout(timeout ?? undefined);
 -
 -    // Set the new timeout
 -    timeout = setTimeout(function () {
 -      // Inside the timeout function, clear the timeout variable
 -      // which will let the next execution run when in 'immediate' mode
 -      timeout = null;
 -
 -      // Check if the function already ran with the immediate flag
 -      if (!immediate) {
 -        // Call the original function with apply
 -        // apply lets you define the 'this' object as well as the arguments
 -        //    (both captured before setTimeout)
 -        func.apply(this, args);
 -      }
 -    }, wait);
 -
 -    // Immediate mode and no wait timer? Execute the function..
 -    if (callNow) func.apply(this, args);
 -  } as (...e: T) => R;
 -}
 -
  export async function fetchThemeList(): Promise<string[]> {
    return fetch("/css/themelist").then(res => res.json());
  }
@@@ -1011,7 -1153,11 +1023,7 @@@ export function siteBannerCss(banner: s
      `;
  }
  
- export function setIsoData(context: any): IsoData {
 -export function isBrowser() {
 -  return typeof window !== "undefined";
 -}
 -
+ export function setIsoData<T extends RouteData>(context: any): IsoData<T> {
    // If its the browser, you need to deserialize the data from the window
    if (isBrowser()) {
      return window.isoData;
@@@ -1140,6 -1286,21 +1152,6 @@@ export function numToSI(value: number)
    return SHORTNUM_SI_FORMAT.format(value);
  }
  
 -export function isBanned(ps: Person): boolean {
 -  const expires = ps.ban_expires;
 -  // Add Z to convert from UTC date
 -  // TODO this check probably isn't necessary anymore
 -  if (expires) {
 -    if (ps.banned && new Date(expires + "Z") > new Date()) {
 -      return true;
 -    } else {
 -      return false;
 -    }
 -  } else {
 -    return ps.banned;
 -  }
 -}
 -
  export function myAuth(): string | undefined {
    return UserService.Instance.auth();
  }
@@@ -1171,6 -1332,15 +1183,6 @@@ export function postToCommentSortType(s
    }
  }
  
 -export function canCreateCommunity(
 -  siteRes: GetSiteResponse,
 -  myUserInfo = UserService.Instance.myUserInfo
 -): boolean {
 -  const adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
 -  // TODO: Make this check if user is logged on as well
 -  return !adminOnly || amAdmin(myUserInfo);
 -}
 -
  export function isPostBlocked(
    pv: PostView,
    myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
@@@ -1251,12 -1421,64 +1263,12 @@@ interface EmojiMartSkin 
    src: string;
  }
  
 -const groupBy = <T>(
 -  array: T[],
 -  predicate: (value: T, index: number, array: T[]) => string
 -) =>
 -  array.reduce((acc, value, index, array) => {
 -    (acc[predicate(value, index, array)] ||= []).push(value);
 -    return acc;
 -  }, {} as { [key: string]: T[] });
 -
 -export type QueryParams<T extends Record<string, any>> = {
 -  [key in keyof T]?: string;
 -};
 -
 -export function getQueryParams<T extends Record<string, any>>(processors: {
 -  [K in keyof T]: (param: string) => T[K];
 -}): T {
 -  if (isBrowser()) {
 -    const searchParams = new URLSearchParams(window.location.search);
 -
 -    return Array.from(Object.entries(processors)).reduce(
 -      (acc, [key, process]) => ({
 -        ...acc,
 -        [key]: process(searchParams.get(key)),
 -      }),
 -      {} as T
 -    );
 -  }
 -
 -  return {} as T;
 -}
 -
 -export function getQueryString<T extends Record<string, string | undefined>>(
 -  obj: T
 -) {
 -  return Object.entries(obj)
 -    .filter(([, val]) => val !== undefined && val !== null)
 -    .reduce(
 -      (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`,
 -      "?"
 -    );
 -}
 -
  export function isAuthPath(pathname: string) {
    return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
      pathname
    );
  }
  
 -export function canShare() {
 -  return isBrowser() && !!navigator.canShare;
 -}
 -
 -export function share(shareData: ShareData) {
 -  if (isBrowser()) {
 -    navigator.share(shareData);
 -  }
 -}
 -
  export function newVote(voteType: VoteType, myVote?: number): number {
    if (voteType == VoteType.Upvote) {
      return myVote == 1 ? 0 : 1;
      return myVote == -1 ? 0 : -1;
    }
  }
 -
 -function sleep(millis: number): Promise<void> {
 -  return new Promise(resolve => setTimeout(resolve, millis));
 -}
 -
 -/**
 - * Polls / repeatedly runs a promise, every X milliseconds
 - */
 -export async function poll(promiseFn: any, millis: number) {
 -  if (window.document.visibilityState !== "hidden") {
 -    await promiseFn();
 -  }
 -  await sleep(millis);
 -  return poll(promiseFn, millis);
 -}
+ export type RouteDataResponse<T extends Record<string, any>> = {
+   [K in keyof T]: RequestState<T[K]>;
+ };