]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' into route-data-refactor
authorabias <abias1122@gmail.com>
Fri, 16 Jun 2023 02:08:14 +0000 (22:08 -0400)
committerabias <abias1122@gmail.com>
Fri, 16 Jun 2023 02:08:14 +0000 (22:08 -0400)
19 files changed:
1  2 
src/server/index.tsx
src/shared/components/community/communities.tsx
src/shared/components/community/community.tsx
src/shared/components/home/admin-settings.tsx
src/shared/components/home/home.tsx
src/shared/components/home/instances.tsx
src/shared/components/modlog.tsx
src/shared/components/person/inbox.tsx
src/shared/components/person/profile.tsx
src/shared/components/person/registration-applications.tsx
src/shared/components/person/reports.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/post.tsx
src/shared/components/private_message/create-private-message.tsx
src/shared/components/search.tsx
src/shared/interfaces.ts
src/shared/routes.ts
src/shared/services/HttpService.ts
src/shared/utils.ts

diff --combined src/server/index.tsx
index 4ab4f76f93a047107957cd31eda8dce8614f5f3f,43024076ebb74db9d7624a89cf354555d355d3f0..98063558cfab62e0fb622d3f2868a08b498ca4b7
@@@ -6,19 -6,20 +6,24 @@@ import { Helmet } from "inferno-helmet"
  import { matchPath, StaticRouter } from "inferno-router";
  import { renderToString } from "inferno-server";
  import IsomorphicCookie from "isomorphic-cookie";
- import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client";
+ import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
  import path from "path";
  import process from "process";
  import serialize from "serialize-javascript";
  import sharp from "sharp";
  import { App } from "../shared/components/app/app";
- import { getHttpBase, getHttpBaseInternal } from "../shared/env";
+ import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
  import {
    ILemmyConfig,
    InitialFetchRequest,
    IsoDataOptionalSite,
  } from "../shared/interfaces";
  import { routes } from "../shared/routes";
 -import { RequestState, wrapClient } from "../shared/services/HttpService";
++import {
++  FailedRequestState,
++  RequestState,
++  wrapClient,
++} from "../shared/services/HttpService";
  import {
    ErrorPageData,
    favIconPngUrl,
@@@ -38,7 -39,7 +43,7 @@@ if (!process.env["LEMMY_UI_DISABLE_CSP"
    server.use(function (_req, res, next) {
      res.setHeader(
        "Content-Security-Policy",
-       `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *`
+       `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
      );
      next();
    });
@@@ -64,7 -65,13 +69,13 @@@ Disallow: /search
  
  server.get("/service-worker.js", async (_req, res) => {
    res.setHeader("Content-Type", "application/javascript");
-   res.sendFile(path.resolve("./dist/service-worker.js"));
+   res.sendFile(
+     path.resolve(
+       `./dist/service-worker${
+         process.env.NODE_ENV === "development" ? "-development" : ""
+       }.js`
+     )
+   );
  });
  
  server.get("/robots.txt", async (_req, res) => {
@@@ -121,7 -128,7 +132,7 @@@ server.get("/*", async (req, res) => 
      const getSiteForm: GetSite = { auth };
  
      const headers = setForwardedHeaders(req.headers);
-     const client = new LemmyHttp(getHttpBaseInternal(), headers);
+     const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
  
      const { path, url, query } = req;
  
      // This bypasses errors, so that the client can hit the error on its own,
      // in order to remove the jwt on the browser. Necessary for wrong jwts
      let site: GetSiteResponse | undefined = undefined;
-     let routeData: Record<string, any> = {};
-     let errorPageData: ErrorPageData | undefined;
-     try {
-       let try_site: any = await client.getSite(getSiteForm);
-       if (try_site.error == "not_logged_in") {
-         console.error(
-           "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
-         );
-         getSiteForm.auth = undefined;
-         auth = undefined;
-         try_site = await client.getSite(getSiteForm);
-       }
 -    const routeData: RequestState<any>[] = [];
++    let routeData: Record<string, RequestState<any>> = {};
+     let errorPageData: ErrorPageData | undefined = undefined;
+     let try_site = await client.getSite(getSiteForm);
+     if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
+       console.error(
+         "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
+       );
+       getSiteForm.auth = undefined;
+       auth = undefined;
+       try_site = await client.getSite(getSiteForm);
+     }
  
-       if (!auth && isAuthPath(path)) {
-         res.redirect("/login");
-         return;
-       }
+     if (!auth && isAuthPath(path)) {
+       return res.redirect("/login");
+     }
  
-       site = try_site;
+     if (try_site.state === "success") {
+       site = try_site.data;
        initializeSite(site);
  
+       if (path != "/setup" && !site.site_view.local_site.site_setup) {
+         return res.redirect("/setup");
+       }
        if (site) {
          const initialFetchReq: InitialFetchRequest = {
            client,
          };
  
          if (activeRoute?.fetchInitialData) {
 -          routeData.push(
 -            ...(await Promise.all([
 -              ...activeRoute.fetchInitialData(initialFetchReq),
 -            ]))
 +          const routeDataKeysAndVals = await Promise.all(
 +            Object.entries(activeRoute.fetchInitialData(initialFetchReq)).map(
 +              async ([key, val]) => [key, await val]
 +            )
            );
 +
 +          routeData = routeDataKeysAndVals.reduce((acc, [key, val]) => {
 +            acc[key] = val;
 +
 +            return acc;
 +          }, {});
          }
        }
-     } catch (error) {
-       errorPageData = getErrorPageData(error, site);
+     } else if (try_site.state === "failed") {
+       errorPageData = getErrorPageData(new Error(try_site.msg), site);
      }
  
-     const error = Object.values(routeData).find(val => val?.error)?.error;
++    const error = Object.values(routeData).find(
++      res => res.state === "failed"
++    ) as FailedRequestState | undefined;
 +
      // Redirect to the 404 if there's an API error
 -    if (routeData[0] && routeData[0].state === "failed") {
 -      const error = routeData[0].msg;
 -      console.error(error);
 -      if (error === "instance_is_private") {
 +    if (error) {
-       console.error(error);
-       if (error === "instance_is_private") {
++      console.error(error.msg);
++      if (error.msg === "instance_is_private") {
          return res.redirect(`/signup`);
        } else {
-         errorPageData = getErrorPageData(error, site);
 -        errorPageData = getErrorPageData(new Error(error), site);
++        errorPageData = getErrorPageData(new Error(error.msg), site);
        }
      }
  
@@@ -222,15 -225,15 +238,15 @@@ server.listen(Number(port), hostname, (
  function setForwardedHeaders(headers: IncomingHttpHeaders): {
    [key: string]: string;
  } {
-   let out: { [key: string]: string } = {};
+   const out: { [key: string]: string } = {};
    if (headers.host) {
      out.host = headers.host;
    }
-   let realIp = headers["x-real-ip"];
+   const realIp = headers["x-real-ip"];
    if (realIp) {
      out["x-real-ip"] = realIp as string;
    }
-   let forwardedFor = headers["x-forwarded-for"];
+   const forwardedFor = headers["x-forwarded-for"];
    if (forwardedFor) {
      out["x-forwarded-for"] = forwardedFor as string;
    }
@@@ -243,7 -246,7 +259,7 @@@ process.on("SIGINT", () => 
    process.exit(0);
  });
  
- const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
+ const iconSizes = [72, 96, 144, 192, 512];
  const defaultLogoPathDirectory = path.join(
    process.cwd(),
    "dist",
    "icons"
  );
  
- export async function generateManifestBase64(site: Site) {
-   const url = (
-     process.env.NODE_ENV === "development"
-       ? "http://localhost:1236/"
-       : getHttpBase()
-   ).replace(/\/$/g, "");
+ export async function generateManifestBase64({
+   my_user,
+   site_view: {
+     site,
+     local_site: { community_creation_admin_only },
+   },
+ }: GetSiteResponse) {
+   const url = getHttpBaseExternal();
    const icon = site.icon ? await fetchIconPng(site.icon) : null;
  
    const manifest = {
          };
        })
      ),
+     shortcuts: [
+       {
+         name: "Search",
+         short_name: "Search",
+         description: "Perform a search.",
+         url: "/search",
+       },
+       {
+         name: "Communities",
+         url: "/communities",
+         short_name: "Communities",
+         description: "Browse communities",
+       },
+     ]
+       .concat(
+         my_user
+           ? [
+               {
+                 name: "Create Post",
+                 url: "/create_post",
+                 short_name: "Create Post",
+                 description: "Create a post.",
+               },
+             ]
+           : []
+       )
+       .concat(
+         my_user?.local_user_view.person.admin || !community_creation_admin_only
+           ? [
+               {
+                 name: "Create Community",
+                 url: "/create_community",
+                 short_name: "Create Community",
+                 description: "Create a community",
+               },
+             ]
+           : []
+       ),
+     related_applications: [
+       {
+         platform: "f-droid",
+         url: "https://f-droid.org/packages/com.jerboa/",
+         id: "com.jerboa",
+       },
+     ],
    };
  
    return Buffer.from(JSON.stringify(manifest)).toString("base64");
  }
  
  async function fetchIconPng(iconUrl: string) {
-   return await fetch(
-     iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal())
-   )
+   return await fetch(iconUrl)
      .then(res => res.blob())
      .then(blob => blob.arrayBuffer());
  }
@@@ -339,14 -388,15 +401,15 @@@ async function createSsrHtml(root: stri
          .then(buf => buf.toString("base64"))}`
      : favIconPngUrl;
  
-   const eruda = (
-     <>
-       <script src="//cdn.jsdelivr.net/npm/eruda"></script>
-       <script>eruda.init();</script>
-     </>
-   );
-   const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
+   const erudaStr =
+     process.env["LEMMY_UI_DEBUG"] === "true"
+       ? renderToString(
+           <>
+             <script src="//cdn.jsdelivr.net/npm/eruda"></script>
+             <script>eruda.init();</script>
+           </>
+         )
+       : "";
  
    const helmet = Helmet.renderStatic();
  
  
    return `
    <!DOCTYPE html>
-   <html ${helmet.htmlAttributes.toString()} lang="en">
+   <html ${helmet.htmlAttributes.toString()}>
    <head>
-   <script>window.isoData = ${JSON.stringify(isoData)}</script>
+   <script>window.isoData = ${serialize(isoData)}</script>
    <script>window.lemmyConfig = ${serialize(config)}</script>
  
    <!-- A remote debugging utility for mobile -->
      site &&
      `<link
          rel="manifest"
-         href={${`data:application/manifest+json;base64,${await generateManifestBase64(
-           site.site_view.site
-         )}`}}
+         href=${`data:application/manifest+json;base64,${await generateManifestBase64(
+           site
+         )}`}
        />`
    }
    <link rel="apple-touch-icon" href=${appleTouchIcon} />
index 53e0e967e908d2bccefe202db94244680d952e4f,623269439f8642444aad0fcaa6cb8cdb0649d02d..3eb7bd3aa76cde879b0808b95ceeca618ffc4fe2
@@@ -1,33 -1,26 +1,27 @@@
  import { Component, linkEvent } from "inferno";
  import {
    CommunityResponse,
-   FollowCommunity,
    GetSiteResponse,
    ListCommunities,
    ListCommunitiesResponse,
    ListingType,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
- import { InitialFetchRequest } from "shared/interfaces";
  import { i18n } from "../../i18next";
- import { WebSocketService } from "../../services";
+ import { InitialFetchRequest } from "../../interfaces";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
+   editCommunity,
    getPageFromString,
    getQueryParams,
    getQueryString,
-   isBrowser,
    myAuth,
+   myAuthRequired,
    numToSI,
    setIsoData,
    showLocal,
-   toast,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
@@@ -37,15 -30,11 +31,15 @@@ import { CommunityLink } from "./commun
  
  const communityLimit = 50;
  
- interface CommunitiesData {
++type CommunitiesData = RouteDataResponse<{
 +  listCommunitiesResponse: ListCommunitiesResponse;
- }
++}>;
 +
  interface CommunitiesState {
-   listCommunitiesResponse?: ListCommunitiesResponse;
-   loading: boolean;
+   listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
    siteRes: GetSiteResponse;
    searchText: string;
+   isIsomorphic: boolean;
  }
  
  interface CommunitiesProps {
    page: number;
  }
  
- function getCommunitiesQueryParams() {
-   return getQueryParams<CommunitiesProps>({
-     listingType: getListingTypeFromQuery,
-     page: getPageFromString,
-   });
- }
  function getListingTypeFromQuery(listingType?: string): ListingType {
    return listingType ? (listingType as ListingType) : "Local";
  }
  
- function toggleSubscribe(community_id: number, follow: boolean) {
-   const auth = myAuth();
-   if (auth) {
-     const form: FollowCommunity = {
-       community_id,
-       follow,
-       auth,
-     };
-     WebSocketService.Instance.send(wsClient.followCommunity(form));
-   }
- }
- function refetch() {
-   const { listingType, page } = getCommunitiesQueryParams();
-   const listCommunitiesForm: ListCommunities = {
-     type_: listingType,
-     sort: "TopMonth",
-     limit: communityLimit,
-     page,
-     auth: myAuth(false),
-   };
-   WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
- }
  export class Communities extends Component<any, CommunitiesState> {
-   private subscription?: Subscription;
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<CommunitiesData>(this.context);
    state: CommunitiesState = {
-     loading: true,
+     listCommunitiesResponse: { state: "empty" },
      siteRes: this.isoData.site_res,
      searchText: "",
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      this.handlePageChange = this.handlePageChange.bind(this);
      this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
 +      const { listCommunitiesResponse } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
 -        listCommunitiesResponse: this.isoData.routeData[0],
 +        listCommunitiesResponse,
-         loading: false,
+         isIsomorphic: true,
        };
-     } else {
-       refetch();
      }
    }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.refetch();
      }
    }
  
      }`;
    }
  
-   render() {
-     const { listingType, page } = getCommunitiesQueryParams();
-     return (
-       <div className="container-lg">
-         <HtmlTags
-           title={this.documentTitle}
-           path={this.context.router.route.match.url}
-         />
-         {this.state.loading ? (
+   renderListings() {
+     switch (this.state.listCommunitiesResponse.state) {
+       case "loading":
+         return (
            <h5>
              <Spinner large />
            </h5>
-         ) : (
+         );
+       case "success": {
+         const { listingType, page } = this.getCommunitiesQueryParams();
+         return (
            <div>
              <div className="row">
                <div className="col-md-6">
                    </tr>
                  </thead>
                  <tbody>
-                   {this.state.listCommunitiesResponse?.communities.map(cv => (
-                     <tr key={cv.community.id}>
-                       <td>
-                         <CommunityLink community={cv.community} />
-                       </td>
-                       <td className="text-right">
-                         {numToSI(cv.counts.subscribers)}
-                       </td>
-                       <td className="text-right">
-                         {numToSI(cv.counts.users_active_month)}
-                       </td>
-                       <td className="text-right d-none d-lg-table-cell">
-                         {numToSI(cv.counts.posts)}
-                       </td>
-                       <td className="text-right d-none d-lg-table-cell">
-                         {numToSI(cv.counts.comments)}
-                       </td>
-                       <td className="text-right">
-                         {cv.subscribed == "Subscribed" && (
-                           <button
-                             className="btn btn-link d-inline-block"
-                             onClick={linkEvent(
-                               cv.community.id,
-                               this.handleUnsubscribe
-                             )}
-                           >
-                             {i18n.t("unsubscribe")}
-                           </button>
-                         )}
-                         {cv.subscribed === "NotSubscribed" && (
-                           <button
-                             className="btn btn-link d-inline-block"
-                             onClick={linkEvent(
-                               cv.community.id,
-                               this.handleSubscribe
-                             )}
-                           >
-                             {i18n.t("subscribe")}
-                           </button>
-                         )}
-                         {cv.subscribed === "Pending" && (
-                           <div className="text-warning d-inline-block">
-                             {i18n.t("subscribe_pending")}
-                           </div>
-                         )}
-                       </td>
-                     </tr>
-                   ))}
+                   {this.state.listCommunitiesResponse.data.communities.map(
+                     cv => (
+                       <tr key={cv.community.id}>
+                         <td>
+                           <CommunityLink community={cv.community} />
+                         </td>
+                         <td className="text-right">
+                           {numToSI(cv.counts.subscribers)}
+                         </td>
+                         <td className="text-right">
+                           {numToSI(cv.counts.users_active_month)}
+                         </td>
+                         <td className="text-right d-none d-lg-table-cell">
+                           {numToSI(cv.counts.posts)}
+                         </td>
+                         <td className="text-right d-none d-lg-table-cell">
+                           {numToSI(cv.counts.comments)}
+                         </td>
+                         <td className="text-right">
+                           {cv.subscribed == "Subscribed" && (
+                             <button
+                               className="btn btn-link d-inline-block"
+                               onClick={linkEvent(
+                                 {
+                                   i: this,
+                                   communityId: cv.community.id,
+                                   follow: false,
+                                 },
+                                 this.handleFollow
+                               )}
+                             >
+                               {i18n.t("unsubscribe")}
+                             </button>
+                           )}
+                           {cv.subscribed === "NotSubscribed" && (
+                             <button
+                               className="btn btn-link d-inline-block"
+                               onClick={linkEvent(
+                                 {
+                                   i: this,
+                                   communityId: cv.community.id,
+                                   follow: true,
+                                 },
+                                 this.handleFollow
+                               )}
+                             >
+                               {i18n.t("subscribe")}
+                             </button>
+                           )}
+                           {cv.subscribed === "Pending" && (
+                             <div className="text-warning d-inline-block">
+                               {i18n.t("subscribe_pending")}
+                             </div>
+                           )}
+                         </td>
+                       </tr>
+                     )
+                   )}
                  </tbody>
                </table>
              </div>
              <Paginator page={page} onChange={this.handlePageChange} />
            </div>
-         )}
+         );
+       }
+     }
+   }
+   render() {
+     return (
+       <div className="container-lg">
+         <HtmlTags
+           title={this.documentTitle}
+           path={this.context.router.route.match.url}
+         />
+         {this.renderListings()}
        </div>
      );
    }
      );
    }
  
-   updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
+   async updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
      const { listingType: urlListingType, page: urlPage } =
-       getCommunitiesQueryParams();
+       this.getCommunitiesQueryParams();
  
      const queryParams: QueryParams<CommunitiesProps> = {
        listingType: listingType ?? urlListingType,
  
      this.props.history.push(`/communities${getQueryString(queryParams)}`);
  
-     refetch();
+     await this.refetch();
    }
  
    handlePageChange(page: number) {
      });
    }
  
-   handleUnsubscribe(communityId: number) {
-     toggleSubscribe(communityId, false);
-   }
-   handleSubscribe(communityId: number) {
-     toggleSubscribe(communityId, true);
-   }
    handleSearchChange(i: Communities, event: any) {
      i.setState({ searchText: event.target.value });
    }
  
-   handleSearchSubmit(i: Communities) {
+   handleSearchSubmit(i: Communities, event: any) {
+     event.preventDefault();
      const searchParamEncoded = encodeURIComponent(i.state.searchText);
      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>
-   >): WithPromiseKeys<CommunitiesData> {
++  >): Promise<CommunitiesData> {
      const listCommunitiesForm: ListCommunities = {
        type_: getListingTypeFromQuery(listingType),
        sort: "TopMonth",
        auth: auth,
      };
  
 -    return [client.listCommunities(listCommunitiesForm)];
 +    return {
-       listCommunitiesResponse: client.listCommunities(listCommunitiesForm),
++      listCommunitiesResponse: await client.listCommunities(
++        listCommunitiesForm
++      ),
 +    };
    }
  
-   parseMessage(msg: any) {
-     const op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-     } else if (op === UserOperation.ListCommunities) {
-       const data = wsJsonToRes<ListCommunitiesResponse>(msg);
-       this.setState({ listCommunitiesResponse: data, loading: false });
-       window.scrollTo(0, 0);
-     } else if (op === UserOperation.FollowCommunity) {
-       const {
-         community_view: {
-           community,
-           subscribed,
-           counts: { subscribers },
-         },
-       } = wsJsonToRes<CommunityResponse>(msg);
-       const res = this.state.listCommunitiesResponse;
-       const found = res?.communities.find(
-         ({ community: { id } }) => id == community.id
-       );
-       if (found) {
-         found.subscribed = subscribed;
-         found.counts.subscribers = subscribers;
-         this.setState(this.state);
+   getCommunitiesQueryParams() {
+     return getQueryParams<CommunitiesProps>({
+       listingType: getListingTypeFromQuery,
+       page: getPageFromString,
+     });
+   }
+   async handleFollow(data: {
+     i: Communities;
+     communityId: number;
+     follow: boolean;
+   }) {
+     const res = await HttpService.client.followCommunity({
+       community_id: data.communityId,
+       follow: data.follow,
+       auth: myAuthRequired(),
+     });
+     data.i.findAndUpdateCommunity(res);
+   }
+   async refetch() {
+     this.setState({ listCommunitiesResponse: { state: "loading" } });
+     const { listingType, page } = this.getCommunitiesQueryParams();
+     this.setState({
+       listCommunitiesResponse: await HttpService.client.listCommunities({
+         type_: listingType,
+         sort: "TopMonth",
+         limit: communityLimit,
+         page,
+         auth: myAuth(),
+       }),
+     });
+     window.scrollTo(0, 0);
+   }
+   findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
+     this.setState(s => {
+       if (
+         s.listCommunitiesResponse.state == "success" &&
+         res.state == "success"
+       ) {
+         s.listCommunitiesResponse.data.communities = editCommunity(
+           res.data.community_view,
+           s.listCommunitiesResponse.data.communities
+         );
        }
-     }
+       return s;
+     });
    }
  }
index f3ab1e21795317b0c75f66b88bc00c4d4cc712a7,7dc150f332fd89b40a8635c79e526ffa53e3b3ed..e8412e76842f9481a8b07654642b799f11ac716d
@@@ -1,60 -1,85 +1,86 @@@
  import { Component, linkEvent } from "inferno";
  import { RouteComponentProps } from "inferno-router/dist/Route";
  import {
+   AddAdmin,
+   AddModToCommunity,
    AddModToCommunityResponse,
+   BanFromCommunity,
    BanFromCommunityResponse,
-   BlockCommunityResponse,
-   BlockPersonResponse,
+   BanPerson,
+   BanPersonResponse,
+   BlockCommunity,
+   BlockPerson,
+   CommentId,
+   CommentReplyResponse,
    CommentResponse,
-   CommentView,
    CommunityResponse,
+   CreateComment,
+   CreateCommentLike,
+   CreateCommentReport,
+   CreatePostLike,
+   CreatePostReport,
+   DeleteComment,
+   DeleteCommunity,
+   DeletePost,
+   DistinguishComment,
+   EditComment,
+   EditCommunity,
+   EditPost,
+   FeaturePost,
+   FollowCommunity,
    GetComments,
    GetCommentsResponse,
    GetCommunity,
    GetCommunityResponse,
    GetPosts,
    GetPostsResponse,
-   PostReportResponse,
+   GetSiteResponse,
+   LockPost,
+   MarkCommentReplyAsRead,
+   MarkPersonMentionAsRead,
    PostResponse,
-   PostView,
+   PurgeComment,
+   PurgeCommunity,
    PurgeItemResponse,
+   PurgePerson,
+   PurgePost,
+   RemoveComment,
+   RemoveCommunity,
+   RemovePost,
+   SaveComment,
+   SavePost,
    SortType,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   TransferCommunity,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import {
    CommentViewType,
    DataType,
    InitialFetchRequest,
  } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    commentsToFlatNodes,
    communityRSSUrl,
-   createCommentLikeRes,
-   createPostLikeFindRes,
-   editCommentRes,
-   editPostFindRes,
+   editComment,
+   editPost,
+   editWith,
    enableDownvotes,
    enableNsfw,
    fetchLimit,
+   getCommentParentId,
    getDataTypeString,
    getPageFromString,
    getQueryParams,
    getQueryString,
-   isPostBlocked,
    myAuth,
-   notifyPost,
-   nsfwCheck,
    postToCommentSortType,
    relTags,
    restoreScrollPosition,
-   saveCommentRes,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -62,8 -87,6 +88,6 @@@
    toast,
    updateCommunityBlock,
    updatePersonBlock,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { CommentNodes } from "../comment/comment-nodes";
  import { BannerIconHeader } from "../common/banner-icon-header";
@@@ -77,19 -100,14 +101,20 @@@ import { SiteSidebar } from "../home/si
  import { PostListings } from "../post/post-listings";
  import { CommunityLink } from "./community-link";
  
- interface CommunityData {
++type CommunityData = RouteDataResponse<{
 +  communityResponse: GetCommunityResponse;
 +  postsResponse?: GetPostsResponse;
 +  commentsResponse?: GetCommentsResponse;
- }
++}>;
 +
  interface State {
-   communityRes?: GetCommunityResponse;
-   communityLoading: boolean;
-   listingsLoading: boolean;
-   posts: PostView[];
-   comments: CommentView[];
+   communityRes: RequestState<GetCommunityResponse>;
+   postsRes: RequestState<GetPostsResponse>;
+   commentsRes: RequestState<GetCommentsResponse>;
+   siteRes: GetSiteResponse;
    showSidebarMobile: boolean;
+   finished: Map<CommentId, boolean | undefined>;
+   isIsomorphic: boolean;
  }
  
  interface CommunityProps {
@@@ -122,14 -140,15 +147,15 @@@ export class Community extends Componen
    RouteComponentProps<{ name: string }>,
    State
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<CommunityData>(this.context);
-   private subscription?: Subscription;
    state: State = {
-     communityLoading: true,
-     listingsLoading: true,
-     posts: [],
-     comments: [],
+     communityRes: { state: "empty" },
+     postsRes: { state: "empty" },
+     commentsRes: { state: "empty" },
+     siteRes: this.isoData.site_res,
      showSidebarMobile: false,
+     finished: new Map(),
+     isIsomorphic: false,
    };
  
    constructor(props: RouteComponentProps<{ name: string }>, context: any) {
      this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
      this.handlePageChange = this.handlePageChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     // All of the action binds
+     this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this);
+     this.handleEditCommunity = this.handleEditCommunity.bind(this);
+     this.handleFollow = this.handleFollow.bind(this);
+     this.handleRemoveCommunity = this.handleRemoveCommunity.bind(this);
+     this.handleCreateComment = this.handleCreateComment.bind(this);
+     this.handleEditComment = this.handleEditComment.bind(this);
+     this.handleSaveComment = this.handleSaveComment.bind(this);
+     this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+     this.handleBlockPerson = this.handleBlockPerson.bind(this);
+     this.handleDeleteComment = this.handleDeleteComment.bind(this);
+     this.handleRemoveComment = this.handleRemoveComment.bind(this);
+     this.handleCommentVote = this.handleCommentVote.bind(this);
+     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+     this.handleAddAdmin = this.handleAddAdmin.bind(this);
+     this.handlePurgePerson = this.handlePurgePerson.bind(this);
+     this.handlePurgeComment = this.handlePurgeComment.bind(this);
+     this.handleCommentReport = this.handleCommentReport.bind(this);
+     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+     this.handleBanPerson = this.handleBanPerson.bind(this);
+     this.handlePostVote = this.handlePostVote.bind(this);
+     this.handlePostEdit = this.handlePostEdit.bind(this);
+     this.handlePostReport = this.handlePostReport.bind(this);
+     this.handleLockPost = this.handleLockPost.bind(this);
+     this.handleDeletePost = this.handleDeletePost.bind(this);
+     this.handleRemovePost = this.handleRemovePost.bind(this);
+     this.handleSavePost = this.handleSavePost.bind(this);
+     this.handlePurgePost = this.handlePurgePost.bind(this);
+     this.handleFeaturePost = this.handleFeaturePost.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path == this.context.router.route.match.url) {
-       const { communityResponse, commentsResponse, postsResponse } =
-         this.isoData.routeData;
+     if (FirstLoadService.isFirstLoad) {
 -      const [communityRes, postsRes, commentsRes] = this.isoData.routeData;
++      const {
++        communityResponse: communityRes,
++        commentsResponse: commentsRes,
++        postsResponse: postsRes,
++      } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
-         communityRes: communityResponse,
 -        communityRes,
 -        postsRes,
 -        commentsRes,
+         isIsomorphic: true,
        };
-       if (postsResponse) {
-         this.state = { ...this.state, posts: postsResponse.posts };
 +
-       if (commentsResponse) {
-         this.state = { ...this.state, comments: commentsResponse.comments };
++      if (communityRes.state === "success") {
++        this.state = {
++          ...this.state,
++          communityRes,
++        };
 +      }
 +
-       this.state = {
-         ...this.state,
-         communityLoading: false,
-         listingsLoading: false,
-       };
-     } else {
-       this.fetchCommunity();
-       this.fetchData();
++      if (postsRes?.state === "success") {
++        this.state = {
++          ...this.state,
++          postsRes,
++        };
 +      }
 +
++      if (commentsRes?.state === "success") {
++        this.state = {
++          ...this.state,
++          commentsRes,
++        };
++      }
      }
    }
  
-   fetchCommunity() {
-     const form: GetCommunity = {
-       name: this.props.match.params.name,
-       auth: myAuth(false),
-     };
-     WebSocketService.Instance.send(wsClient.getCommunity(form));
+   async fetchCommunity() {
+     this.setState({ communityRes: { state: "loading" } });
+     this.setState({
+       communityRes: await HttpService.client.getCommunity({
+         name: this.props.match.params.name,
+         auth: myAuth(),
+       }),
+     });
    }
  
-   componentDidMount() {
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await Promise.all([this.fetchCommunity(), this.fetchData()]);
+     }
      setupTippy();
    }
  
    componentWillUnmount() {
      saveScrollPosition(this.context);
-     this.subscription?.unsubscribe();
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      path,
      query: { dataType: urlDataType, page: urlPage, sort: urlSort },
      auth,
-   }: InitialFetchRequest<
-     QueryParams<CommunityProps>
-   >): WithPromiseKeys<CommunityData> {
+   }: 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: Promise<GetPostsResponse> | undefined = undefined;
-     let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
++    let postsResponse: RequestState<GetPostsResponse> | undefined = undefined;
++    let commentsResponse: RequestState<GetCommentsResponse> | undefined =
++      undefined;
 +
      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 = client.getPosts(getPostsForm);
++      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 = client.getComments(getCommentsForm);
++      commentsResponse = await client.getComments(getCommentsForm);
      }
  
 -    return promises;
 +    return {
-       communityResponse: client.getCommunity(communityForm),
++      communityResponse: await client.getCommunity(communityForm),
 +      commentsResponse,
 +      postsResponse,
 +    };
    }
  
    get documentTitle(): string {
      const cRes = this.state.communityRes;
-     return cRes
-       ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
+     return cRes.state == "success"
+       ? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
        : "";
    }
  
-   render() {
-     const res = this.state.communityRes;
-     const { page } = getCommunityQueryParams();
-     return (
-       <div className="container-lg">
-         {this.state.communityLoading ? (
+   renderCommunity() {
+     switch (this.state.communityRes.state) {
+       case "loading":
+         return (
            <h5>
              <Spinner large />
            </h5>
-         ) : (
-           res && (
-             <>
-               <HtmlTags
-                 title={this.documentTitle}
-                 path={this.context.router.route.match.url}
-                 description={res.community_view.community.description}
-                 image={res.community_view.community.icon}
-               />
-               <div className="row">
-                 <div className="col-12 col-md-8">
-                   {this.communityInfo}
-                   <div className="d-block d-md-none">
-                     <button
-                       className="btn btn-secondary d-inline-block mb-2 mr-3"
-                       onClick={linkEvent(this, this.handleShowSidebarMobile)}
-                     >
-                       {i18n.t("sidebar")}{" "}
-                       <Icon
-                         icon={
-                           this.state.showSidebarMobile
-                             ? `minus-square`
-                             : `plus-square`
-                         }
-                         classes="icon-inline"
-                       />
-                     </button>
-                     {this.state.showSidebarMobile && this.sidebar(res)}
-                   </div>
-                   {this.selects}
-                   {this.listings}
-                   <Paginator page={page} onChange={this.handlePageChange} />
-                 </div>
-                 <div className="d-none d-md-block col-md-4">
-                   {this.sidebar(res)}
+         );
+       case "success": {
+         const res = this.state.communityRes.data;
+         const { page } = getCommunityQueryParams();
+         return (
+           <>
+             <HtmlTags
+               title={this.documentTitle}
+               path={this.context.router.route.match.url}
+               description={res.community_view.community.description}
+               image={res.community_view.community.icon}
+             />
+             <div className="row">
+               <div className="col-12 col-md-8">
+                 {this.communityInfo(res)}
+                 <div className="d-block d-md-none">
+                   <button
+                     className="btn btn-secondary d-inline-block mb-2 mr-3"
+                     onClick={linkEvent(this, this.handleShowSidebarMobile)}
+                   >
+                     {i18n.t("sidebar")}{" "}
+                     <Icon
+                       icon={
+                         this.state.showSidebarMobile
+                           ? `minus-square`
+                           : `plus-square`
+                       }
+                       classes="icon-inline"
+                     />
+                   </button>
+                   {this.state.showSidebarMobile && this.sidebar(res)}
                  </div>
+                 {this.selects(res)}
+                 {this.listings(res)}
+                 <Paginator page={page} onChange={this.handlePageChange} />
                </div>
-             </>
-           )
-         )}
-       </div>
-     );
+               <div className="d-none d-md-block col-md-4">
+                 {this.sidebar(res)}
+               </div>
+             </div>
+           </>
+         );
+       }
+     }
+   }
+   render() {
+     return <div className="container-lg">{this.renderCommunity()}</div>;
    }
  
-   sidebar({
-     community_view,
-     moderators,
-     online,
-     discussion_languages,
-     site,
-   }: GetCommunityResponse) {
+   sidebar(res: GetCommunityResponse) {
      const { site_res } = this.isoData;
      // For some reason, this returns an empty vec if it matches the site langs
      const communityLangs =
-       discussion_languages.length === 0
+       res.discussion_languages.length === 0
          ? site_res.all_languages.map(({ id }) => id)
-         : discussion_languages;
+         : res.discussion_languages;
  
      return (
        <>
          <Sidebar
-           community_view={community_view}
-           moderators={moderators}
+           community_view={res.community_view}
+           moderators={res.moderators}
            admins={site_res.admins}
-           online={online}
+           online={res.online}
            enableNsfw={enableNsfw(site_res)}
            editable
            allLanguages={site_res.all_languages}
            siteLanguages={site_res.discussion_languages}
            communityLanguages={communityLangs}
+           onDeleteCommunity={this.handleDeleteCommunity}
+           onRemoveCommunity={this.handleRemoveCommunity}
+           onLeaveModTeam={this.handleAddModToCommunity}
+           onFollowCommunity={this.handleFollow}
+           onBlockCommunity={this.handleBlockCommunity}
+           onPurgeCommunity={this.handlePurgeCommunity}
+           onEditCommunity={this.handleEditCommunity}
          />
-         {!community_view.community.local && site && (
-           <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
+         {!res.community_view.community.local && res.site && (
+           <SiteSidebar site={res.site} showLocal={showLocal(this.isoData)} />
          )}
        </>
      );
    }
  
-   get listings() {
+   listings(communityRes: GetCommunityResponse) {
      const { dataType } = getCommunityQueryParams();
      const { site_res } = this.isoData;
-     const { listingsLoading, posts, comments, communityRes } = this.state;
-     if (listingsLoading) {
-       return (
-         <h5>
-           <Spinner large />
-         </h5>
-       );
-     } else if (dataType === DataType.Post) {
-       return (
-         <PostListings
-           posts={posts}
-           removeDuplicates
-           enableDownvotes={enableDownvotes(site_res)}
-           enableNsfw={enableNsfw(site_res)}
-           allLanguages={site_res.all_languages}
-           siteLanguages={site_res.discussion_languages}
-         />
-       );
+     if (dataType === DataType.Post) {
+       switch (this.state.postsRes.state) {
+         case "loading":
+           return (
+             <h5>
+               <Spinner large />
+             </h5>
+           );
+         case "success":
+           return (
+             <PostListings
+               posts={this.state.postsRes.data.posts}
+               removeDuplicates
+               enableDownvotes={enableDownvotes(site_res)}
+               enableNsfw={enableNsfw(site_res)}
+               allLanguages={site_res.all_languages}
+               siteLanguages={site_res.discussion_languages}
+               onBlockPerson={this.handleBlockPerson}
+               onPostEdit={this.handlePostEdit}
+               onPostVote={this.handlePostVote}
+               onPostReport={this.handlePostReport}
+               onLockPost={this.handleLockPost}
+               onDeletePost={this.handleDeletePost}
+               onRemovePost={this.handleRemovePost}
+               onSavePost={this.handleSavePost}
+               onPurgePerson={this.handlePurgePerson}
+               onPurgePost={this.handlePurgePost}
+               onBanPerson={this.handleBanPerson}
+               onBanPersonFromCommunity={this.handleBanFromCommunity}
+               onAddModToCommunity={this.handleAddModToCommunity}
+               onAddAdmin={this.handleAddAdmin}
+               onTransferCommunity={this.handleTransferCommunity}
+               onFeaturePost={this.handleFeaturePost}
+             />
+           );
+       }
      } else {
-       return (
-         <CommentNodes
-           nodes={commentsToFlatNodes(comments)}
-           viewType={CommentViewType.Flat}
-           noIndent
-           showContext
-           enableDownvotes={enableDownvotes(site_res)}
-           moderators={communityRes?.moderators}
-           admins={site_res.admins}
-           allLanguages={site_res.all_languages}
-           siteLanguages={site_res.discussion_languages}
-         />
-       );
+       switch (this.state.commentsRes.state) {
+         case "loading":
+           return (
+             <h5>
+               <Spinner large />
+             </h5>
+           );
+         case "success":
+           return (
+             <CommentNodes
+               nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
+               viewType={CommentViewType.Flat}
+               finished={this.state.finished}
+               noIndent
+               showContext
+               enableDownvotes={enableDownvotes(site_res)}
+               moderators={communityRes.moderators}
+               admins={site_res.admins}
+               allLanguages={site_res.all_languages}
+               siteLanguages={site_res.discussion_languages}
+               onSaveComment={this.handleSaveComment}
+               onBlockPerson={this.handleBlockPerson}
+               onDeleteComment={this.handleDeleteComment}
+               onRemoveComment={this.handleRemoveComment}
+               onCommentVote={this.handleCommentVote}
+               onCommentReport={this.handleCommentReport}
+               onDistinguishComment={this.handleDistinguishComment}
+               onAddModToCommunity={this.handleAddModToCommunity}
+               onAddAdmin={this.handleAddAdmin}
+               onTransferCommunity={this.handleTransferCommunity}
+               onPurgeComment={this.handlePurgeComment}
+               onPurgePerson={this.handlePurgePerson}
+               onCommentReplyRead={this.handleCommentReplyRead}
+               onPersonMentionRead={this.handlePersonMentionRead}
+               onBanPersonFromCommunity={this.handleBanFromCommunity}
+               onBanPerson={this.handleBanPerson}
+               onCreateComment={this.handleCreateComment}
+               onEditComment={this.handleEditComment}
+             />
+           );
+       }
      }
    }
  
-   get communityInfo() {
-     const community = this.state.communityRes?.community_view.community;
+   communityInfo(res: GetCommunityResponse) {
+     const community = res.community_view.community;
  
      return (
        community && (
      );
    }
  
-   get selects() {
+   selects(res: GetCommunityResponse) {
      // let communityRss = this.state.communityRes.map(r =>
      //   communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
      // );
      const { dataType, sort } = getCommunityQueryParams();
-     const res = this.state.communityRes;
      const communityRss = res
        ? communityRSSUrl(res.community_view.community.actor_id, sort)
        : undefined;
      }));
    }
  
-   updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
+   async updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
      const {
        dataType: urlDataType,
        page: urlPage,
        `/c/${this.props.match.params.name}${getQueryString(queryParams)}`
      );
  
-     this.setState({
-       comments: [],
-       posts: [],
-       listingsLoading: true,
-     });
-     this.fetchData();
+     await this.fetchData();
    }
  
-   fetchData() {
+   async fetchData() {
      const { dataType, page, sort } = getCommunityQueryParams();
      const { name } = this.props.match.params;
  
-     let req: string;
      if (dataType === DataType.Post) {
-       const form: GetPosts = {
-         page,
-         limit: fetchLimit,
-         sort,
-         type_: "All",
-         community_name: name,
-         saved_only: false,
-         auth: myAuth(false),
-       };
-       req = wsClient.getPosts(form);
+       this.setState({ postsRes: { state: "loading" } });
+       this.setState({
+         postsRes: await HttpService.client.getPosts({
+           page,
+           limit: fetchLimit,
+           sort,
+           type_: "All",
+           community_name: name,
+           saved_only: false,
+           auth: myAuth(),
+         }),
+       });
      } else {
-       const form: GetComments = {
-         page,
-         limit: fetchLimit,
-         sort: postToCommentSortType(sort),
-         type_: "All",
-         community_name: name,
-         saved_only: false,
-         auth: myAuth(false),
-       };
-       req = wsClient.getComments(form);
+       this.setState({ commentsRes: { state: "loading" } });
+       this.setState({
+         commentsRes: await HttpService.client.getComments({
+           page,
+           limit: fetchLimit,
+           sort: postToCommentSortType(sort),
+           type_: "All",
+           community_name: name,
+           saved_only: false,
+           auth: myAuth(),
+         }),
+       });
      }
  
-     WebSocketService.Instance.send(req);
+     restoreScrollPosition(this.context);
+     setupTippy();
    }
  
-   parseMessage(msg: any) {
-     const { page } = getCommunityQueryParams();
-     const op = wsUserOp(msg);
-     console.log(msg);
-     const res = this.state.communityRes;
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       this.context.router.history.push("/");
-     } else if (msg.reconnect) {
-       if (res) {
-         WebSocketService.Instance.send(
-           wsClient.communityJoin({
-             community_id: res.community_view.community.id,
-           })
-         );
-       }
-       this.fetchData();
-     } else {
-       switch (op) {
-         case UserOperation.GetCommunity: {
-           const data = wsJsonToRes<GetCommunityResponse>(msg);
-           this.setState({ communityRes: data, communityLoading: false });
-           // TODO why is there no auth in this form?
-           WebSocketService.Instance.send(
-             wsClient.communityJoin({
-               community_id: data.community_view.community.id,
-             })
-           );
+   async handleDeleteCommunity(form: DeleteCommunity) {
+     const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
+     this.updateCommunity(deleteCommunityRes);
+   }
  
-           break;
-         }
+   async handleAddModToCommunity(form: AddModToCommunity) {
+     const addModRes = await HttpService.client.addModToCommunity(form);
+     this.updateModerators(addModRes);
+   }
  
-         case UserOperation.EditCommunity:
-         case UserOperation.DeleteCommunity:
-         case UserOperation.RemoveCommunity: {
-           const { community_view, discussion_languages } =
-             wsJsonToRes<CommunityResponse>(msg);
+   async handleFollow(form: FollowCommunity) {
+     const followCommunityRes = await HttpService.client.followCommunity(form);
+     this.updateCommunity(followCommunityRes);
  
-           if (res) {
-             res.community_view = community_view;
-             res.discussion_languages = discussion_languages;
-             this.setState(this.state);
-           }
+     // Update myUserInfo
+     if (followCommunityRes.state == "success") {
+       const communityId = followCommunityRes.data.community_view.community.id;
+       const mui = UserService.Instance.myUserInfo;
+       if (mui) {
+         mui.follows = mui.follows.filter(i => i.community.id != communityId);
+       }
+     }
+   }
  
-           break;
-         }
+   async handlePurgeCommunity(form: PurgeCommunity) {
+     const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
+     this.purgeItem(purgeCommunityRes);
+   }
  
-         case UserOperation.FollowCommunity: {
-           const {
-             community_view: {
-               subscribed,
-               counts: { subscribers },
-             },
-           } = wsJsonToRes<CommunityResponse>(msg);
-           if (res) {
-             res.community_view.subscribed = subscribed;
-             res.community_view.counts.subscribers = subscribers;
-             this.setState(this.state);
-           }
-           break;
-         }
+   async handlePurgePerson(form: PurgePerson) {
+     const purgePersonRes = await HttpService.client.purgePerson(form);
+     this.purgeItem(purgePersonRes);
+   }
  
-         case UserOperation.GetPosts: {
-           const { posts } = wsJsonToRes<GetPostsResponse>(msg);
+   async handlePurgeComment(form: PurgeComment) {
+     const purgeCommentRes = await HttpService.client.purgeComment(form);
+     this.purgeItem(purgeCommentRes);
+   }
  
-           this.setState({ posts, listingsLoading: false });
-           restoreScrollPosition(this.context);
-           setupTippy();
+   async handlePurgePost(form: PurgePost) {
+     const purgeRes = await HttpService.client.purgePost(form);
+     this.purgeItem(purgeRes);
+   }
  
-           break;
-         }
+   async handleBlockCommunity(form: BlockCommunity) {
+     const blockCommunityRes = await HttpService.client.blockCommunity(form);
+     if (blockCommunityRes.state == "success") {
+       updateCommunityBlock(blockCommunityRes.data);
+     }
+   }
  
-         case UserOperation.EditPost:
-         case UserOperation.DeletePost:
-         case UserOperation.RemovePost:
-         case UserOperation.LockPost:
-         case UserOperation.FeaturePost:
-         case UserOperation.SavePost: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
+   async handleBlockPerson(form: BlockPerson) {
+     const blockPersonRes = await HttpService.client.blockPerson(form);
+     if (blockPersonRes.state == "success") {
+       updatePersonBlock(blockPersonRes.data);
+     }
+   }
  
-           editPostFindRes(post_view, this.state.posts);
-           this.setState(this.state);
+   async handleRemoveCommunity(form: RemoveCommunity) {
+     const removeCommunityRes = await HttpService.client.removeCommunity(form);
+     this.updateCommunity(removeCommunityRes);
+   }
  
-           break;
-         }
+   async handleEditCommunity(form: EditCommunity) {
+     const res = await HttpService.client.editCommunity(form);
+     this.updateCommunity(res);
  
-         case UserOperation.CreatePost: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
+     return res;
+   }
  
-           const showPostNotifs =
-             UserService.Instance.myUserInfo?.local_user_view.local_user
-               .show_new_post_notifs;
+   async handleCreateComment(form: CreateComment) {
+     const createCommentRes = await HttpService.client.createComment(form);
+     this.createAndUpdateComments(createCommentRes);
  
-           // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
-           if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
-             this.state.posts.unshift(post_view);
-             if (showPostNotifs) {
-               notifyPost(post_view, this.context.router);
-             }
-             this.setState(this.state);
-           }
+     return createCommentRes;
+   }
  
-           break;
-         }
+   async handleEditComment(form: EditComment) {
+     const editCommentRes = await HttpService.client.editComment(form);
+     this.findAndUpdateComment(editCommentRes);
  
-         case UserOperation.CreatePostLike: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
+     return editCommentRes;
+   }
  
-           createPostLikeFindRes(post_view, this.state.posts);
-           this.setState(this.state);
+   async handleDeleteComment(form: DeleteComment) {
+     const deleteCommentRes = await HttpService.client.deleteComment(form);
+     this.findAndUpdateComment(deleteCommentRes);
+   }
  
-           break;
-         }
+   async handleDeletePost(form: DeletePost) {
+     const deleteRes = await HttpService.client.deletePost(form);
+     this.findAndUpdatePost(deleteRes);
+   }
  
-         case UserOperation.AddModToCommunity: {
-           const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
+   async handleRemovePost(form: RemovePost) {
+     const removeRes = await HttpService.client.removePost(form);
+     this.findAndUpdatePost(removeRes);
+   }
  
-           if (res) {
-             res.moderators = moderators;
-             this.setState(this.state);
-           }
+   async handleRemoveComment(form: RemoveComment) {
+     const removeCommentRes = await HttpService.client.removeComment(form);
+     this.findAndUpdateComment(removeCommentRes);
+   }
  
-           break;
-         }
+   async handleSaveComment(form: SaveComment) {
+     const saveCommentRes = await HttpService.client.saveComment(form);
+     this.findAndUpdateComment(saveCommentRes);
+   }
  
-         case UserOperation.BanFromCommunity: {
-           const {
-             person_view: {
-               person: { id: personId },
-             },
-             banned,
-           } = wsJsonToRes<BanFromCommunityResponse>(msg);
+   async handleSavePost(form: SavePost) {
+     const saveRes = await HttpService.client.savePost(form);
+     this.findAndUpdatePost(saveRes);
+   }
  
-           // TODO this might be incorrect
-           this.state.posts
-             .filter(p => p.creator.id === personId)
-             .forEach(p => (p.creator_banned_from_community = banned));
+   async handleFeaturePost(form: FeaturePost) {
+     const featureRes = await HttpService.client.featurePost(form);
+     this.findAndUpdatePost(featureRes);
+   }
  
-           this.setState(this.state);
+   async handleCommentVote(form: CreateCommentLike) {
+     const voteRes = await HttpService.client.likeComment(form);
+     this.findAndUpdateComment(voteRes);
+   }
  
-           break;
-         }
+   async handlePostEdit(form: EditPost) {
+     const res = await HttpService.client.editPost(form);
+     this.findAndUpdatePost(res);
+   }
  
-         case UserOperation.GetComments: {
-           const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
-           this.setState({ comments, listingsLoading: false });
+   async handlePostVote(form: CreatePostLike) {
+     const voteRes = await HttpService.client.likePost(form);
+     this.findAndUpdatePost(voteRes);
+   }
  
-           break;
-         }
+   async handleCommentReport(form: CreateCommentReport) {
+     const reportRes = await HttpService.client.createCommentReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-         case UserOperation.EditComment:
-         case UserOperation.DeleteComment:
-         case UserOperation.RemoveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           editCommentRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handlePostReport(form: CreatePostReport) {
+     const reportRes = await HttpService.client.createPostReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-           break;
-         }
+   async handleLockPost(form: LockPost) {
+     const lockRes = await HttpService.client.lockPost(form);
+     this.findAndUpdatePost(lockRes);
+   }
  
-         case UserOperation.CreateComment: {
-           const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
+   async handleDistinguishComment(form: DistinguishComment) {
+     const distinguishRes = await HttpService.client.distinguishComment(form);
+     this.findAndUpdateComment(distinguishRes);
+   }
  
-           // Necessary since it might be a user reply
-           if (form_id) {
-             this.setState(({ comments }) => ({
-               comments: [comment_view].concat(comments),
-             }));
-           }
+   async handleAddAdmin(form: AddAdmin) {
+     const addAdminRes = await HttpService.client.addAdmin(form);
  
-           break;
-         }
+     if (addAdminRes.state == "success") {
+       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+     }
+   }
  
-         case UserOperation.SaveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+   async handleTransferCommunity(form: TransferCommunity) {
+     const transferCommunityRes = await HttpService.client.transferCommunity(
+       form
+     );
+     toast(i18n.t("transfer_community"));
+     this.updateCommunityFull(transferCommunityRes);
+   }
  
-           saveCommentRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+     const readRes = await HttpService.client.markCommentReplyAsRead(form);
+     this.findAndUpdateCommentReply(readRes);
+   }
  
-           break;
-         }
+   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+     await HttpService.client.markPersonMentionAsRead(form);
+   }
  
-         case UserOperation.CreateCommentLike: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+   async handleBanFromCommunity(form: BanFromCommunity) {
+     const banRes = await HttpService.client.banFromCommunity(form);
+     this.updateBanFromCommunity(banRes);
+   }
  
-           createCommentLikeRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handleBanPerson(form: BanPerson) {
+     const banRes = await HttpService.client.banPerson(form);
+     this.updateBan(banRes);
+   }
  
-           break;
+   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.postsRes.state == "success") {
+           s.postsRes.data.posts
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
          }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
+         }
+         return s;
+       });
+     }
+   }
  
-         case UserOperation.BlockPerson: {
-           const data = wsJsonToRes<BlockPersonResponse>(msg);
-           updatePersonBlock(data);
-           break;
+   updateBan(banRes: RequestState<BanPersonResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.postsRes.state == "success") {
+           s.postsRes.data.posts
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
+         }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
          }
+         return s;
+       });
+     }
+   }
  
-         case UserOperation.CreatePostReport:
-         case UserOperation.CreateCommentReport: {
-           const data = wsJsonToRes<PostReportResponse>(msg);
+   updateCommunity(res: RequestState<CommunityResponse>) {
+     this.setState(s => {
+       if (s.communityRes.state == "success" && res.state == "success") {
+         s.communityRes.data.community_view = res.data.community_view;
+         s.communityRes.data.discussion_languages =
+           res.data.discussion_languages;
+       }
+       return s;
+     });
+   }
+   updateCommunityFull(res: RequestState<GetCommunityResponse>) {
+     this.setState(s => {
+       if (s.communityRes.state == "success" && res.state == "success") {
+         s.communityRes.data.community_view = res.data.community_view;
+         s.communityRes.data.moderators = res.data.moderators;
+       }
+       return s;
+     });
+   }
  
-           if (data) {
-             toast(i18n.t("report_created"));
-           }
+   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+     if (purgeRes.state == "success") {
+       toast(i18n.t("purge_success"));
+       this.context.router.history.push(`/`);
+     }
+   }
  
-           break;
-         }
+   findAndUpdateComment(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editComment(
+           res.data.comment_view,
+           s.commentsRes.data.comments
+         );
+         s.finished.set(res.data.comment_view.comment.id, true);
+       }
+       return s;
+     });
+   }
  
-         case UserOperation.PurgeCommunity: {
-           const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+   createAndUpdateComments(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments.unshift(res.data.comment_view);
  
-           if (success) {
-             toast(i18n.t("purge_success"));
-             this.context.router.history.push(`/`);
-           }
+         // Set finished for the parent
+         s.finished.set(
+           getCommentParentId(res.data.comment_view.comment) ?? 0,
+           true
+         );
+       }
+       return s;
+     });
+   }
  
-           break;
-         }
+   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editWith(
+           res.data.comment_reply_view,
+           s.commentsRes.data.comments
+         );
+       }
+       return s;
+     });
+   }
  
-         case UserOperation.BlockCommunity: {
-           const data = wsJsonToRes<BlockCommunityResponse>(msg);
-           if (res) {
-             res.community_view.blocked = data.blocked;
-             this.setState(this.state);
-           }
-           updateCommunityBlock(data);
+   findAndUpdatePost(res: RequestState<PostResponse>) {
+     this.setState(s => {
+       if (s.postsRes.state == "success" && res.state == "success") {
+         s.postsRes.data.posts = editPost(
+           res.data.post_view,
+           s.postsRes.data.posts
+         );
+       }
+       return s;
+     });
+   }
  
-           break;
-         }
+   updateModerators(res: RequestState<AddModToCommunityResponse>) {
+     // Update the moderators
+     this.setState(s => {
+       if (s.communityRes.state == "success" && res.state == "success") {
+         s.communityRes.data.moderators = res.data.moderators;
        }
-     }
+       return s;
+     });
    }
  }
index 4419cf36c8fba786f484a5c2e3fa6c3ec72b3808,9b7256d03a507e561f5bf2e92cecc0586b216b60..11be72579c4ca410b3a667f878a430e7a6a0d7b8
@@@ -1,30 -1,27 +1,28 @@@
- import autosize from "autosize";
  import { Component, linkEvent } from "inferno";
  import {
    BannedPersonsResponse,
+   CreateCustomEmoji,
+   DeleteCustomEmoji,
+   EditCustomEmoji,
+   EditSite,
    GetFederatedInstancesResponse,
    GetSiteResponse,
    PersonView,
-   SiteResponse,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
-   WithPromiseKeys,
++  RouteDataResponse,
    capitalizeFirstLetter,
-   isBrowser,
-   myAuth,
-   randomStr,
+   fetchThemeList,
+   myAuthRequired,
+   removeFromEmojiDataModel,
    setIsoData,
    showLocal,
    toast,
-   wsClient,
-   wsSubscribe,
+   updateEmojiDataModel,
  } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
@@@ -35,84 -32,94 +33,76 @@@ import RateLimitForm from "./rate-limit
  import { SiteForm } from "./site-form";
  import { TaglineForm } from "./tagline-form";
  
- interface AdminSettingsData {
++type AdminSettingsData = RouteDataResponse<{
 +  bannedPersonsResponse: BannedPersonsResponse;
 +  federatedInstancesResponse: GetFederatedInstancesResponse;
- }
++}>;
 +
  interface AdminSettingsState {
    siteRes: GetSiteResponse;
-   instancesRes?: GetFederatedInstancesResponse;
    banned: PersonView[];
-   loading: boolean;
-   leaveAdminTeamLoading: boolean;
+   currentTab: string;
+   instancesRes: RequestState<GetFederatedInstancesResponse>;
+   bannedRes: RequestState<BannedPersonsResponse>;
+   leaveAdminTeamRes: RequestState<GetSiteResponse>;
+   themeList: string[];
+   isIsomorphic: boolean;
  }
  
  export class AdminSettings extends Component<any, AdminSettingsState> {
-   private siteConfigTextAreaId = `site-config-${randomStr()}`;
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<AdminSettingsData>(this.context);
-   private subscription?: Subscription;
    state: AdminSettingsState = {
      siteRes: this.isoData.site_res,
      banned: [],
-     loading: true,
-     leaveAdminTeamLoading: false,
+     currentTab: "site",
+     bannedRes: { state: "empty" },
+     instancesRes: { state: "empty" },
+     leaveAdminTeamRes: { state: "empty" },
+     themeList: [],
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      super(props, context);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleEditSite = this.handleEditSite.bind(this);
+     this.handleEditEmoji = this.handleEditEmoji.bind(this);
+     this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
+     this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path == this.context.router.route.match.url) {
-       const { bannedPersonsResponse, federatedInstancesResponse } =
-         this.isoData.routeData;
+     if (FirstLoadService.isFirstLoad) {
 -      const [bannedRes, instancesRes] = this.isoData.routeData;
++      const {
++        bannedPersonsResponse: bannedRes,
++        federatedInstancesResponse: instancesRes,
++      } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
-         banned: bannedPersonsResponse.banned,
-         instancesRes: federatedInstancesResponse,
-         loading: false,
+         bannedRes,
+         instancesRes,
+         isIsomorphic: true,
        };
-     } else {
-       let cAuth = myAuth();
-       if (cAuth) {
-         WebSocketService.Instance.send(
-           wsClient.getBannedPersons({
-             auth: cAuth,
-           })
-         );
-         WebSocketService.Instance.send(
-           wsClient.getFederatedInstances({ auth: cAuth })
-         );
-       }
      }
    }
  
 -  async fetchData() {
 -    this.setState({
 -      bannedRes: { state: "loading" },
 -      instancesRes: { state: "loading" },
 -      themeList: [],
 -    });
 -
 -    const auth = myAuthRequired();
 -
 -    const [bannedRes, instancesRes, themeList] = await Promise.all([
 -      HttpService.client.getBannedPersons({ auth }),
 -      HttpService.client.getFederatedInstances({ auth }),
 -      fetchThemeList(),
 -    ]);
 -
 -    this.setState({
 -      bannedRes,
 -      instancesRes,
 -      themeList,
 -    });
 -  }
 -
--  static fetchInitialData({
++  static async fetchInitialData({
      auth,
      client,
-   }: InitialFetchRequest): WithPromiseKeys<AdminSettingsData> {
 -  }: InitialFetchRequest): Promise<any>[] {
 -    const promises: Promise<RequestState<any>>[] = [];
 -
 -    if (auth) {
 -      promises.push(client.getBannedPersons({ auth }));
 -      promises.push(client.getFederatedInstances({ auth }));
 -    } else {
 -      promises.push(
 -        Promise.resolve({ state: "empty" }),
 -        Promise.resolve({ state: "empty" })
 -      );
 -    }
 -
 -    return promises;
++  }: InitialFetchRequest): Promise<AdminSettingsData> {
 +    return {
-       bannedPersonsResponse: client.getBannedPersons({ auth: auth as string }),
-       federatedInstancesResponse: client.getFederatedInstances({
++      bannedPersonsResponse: await client.getBannedPersons({
 +        auth: auth as string,
-       }) as Promise<GetFederatedInstancesResponse>,
++      }),
++      federatedInstancesResponse: await client.getFederatedInstances({
++        auth: auth as string,
++      }),
 +    };
    }
  
-   componentDidMount() {
-     if (isBrowser()) {
-       var textarea: any = document.getElementById(this.siteConfigTextAreaId);
-       autosize(textarea);
-     }
-   }
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.fetchData();
      }
    }
  
    }
  
    render() {
+     const federationData =
+       this.state.instancesRes.state === "success"
+         ? this.state.instancesRes.data.federated_instances
+         : undefined;
      return (
        <div className="container-lg">
          <HtmlTags
            title={this.documentTitle}
            path={this.context.router.route.match.url}
          />
-         {this.state.loading ? (
-           <h5>
-             <Spinner large />
-           </h5>
-         ) : (
-           <Tabs
-             tabs={[
-               {
-                 key: "site",
-                 label: i18n.t("site"),
-                 getNode: () => (
-                   <div className="row">
-                     <div className="col-12 col-md-6">
-                       <SiteForm
-                         siteRes={this.state.siteRes}
-                         instancesRes={this.state.instancesRes}
-                         showLocal={showLocal(this.isoData)}
-                       />
-                     </div>
-                     <div className="col-12 col-md-6">
-                       {this.admins()}
-                       {this.bannedUsers()}
-                     </div>
-                   </div>
-                 ),
-               },
-               {
-                 key: "rate_limiting",
-                 label: "Rate Limiting",
-                 getNode: () => (
-                   <RateLimitForm
-                     localSiteRateLimit={
-                       this.state.siteRes.site_view.local_site_rate_limit
-                     }
-                     applicationQuestion={
-                       this.state.siteRes.site_view.local_site
-                         .application_question
-                     }
-                   />
-                 ),
-               },
-               {
-                 key: "taglines",
-                 label: i18n.t("taglines"),
-                 getNode: () => (
-                   <div className="row">
-                     <TaglineForm siteRes={this.state.siteRes} />
+         <Tabs
+           tabs={[
+             {
+               key: "site",
+               label: i18n.t("site"),
+               getNode: () => (
+                 <div className="row">
+                   <div className="col-12 col-md-6">
+                     <SiteForm
+                       showLocal={showLocal(this.isoData)}
+                       allowedInstances={federationData?.allowed}
+                       blockedInstances={federationData?.blocked}
+                       onSaveSite={this.handleEditSite}
+                       siteRes={this.state.siteRes}
+                       themeList={this.state.themeList}
+                     />
                    </div>
-                 ),
-               },
-               {
-                 key: "emojis",
-                 label: i18n.t("emojis"),
-                 getNode: () => (
-                   <div className="row">
-                     <EmojiForm />
+                   <div className="col-12 col-md-6">
+                     {this.admins()}
+                     {this.bannedUsers()}
                    </div>
-                 ),
-               },
-             ]}
-           />
-         )}
+                 </div>
+               ),
+             },
+             {
+               key: "rate_limiting",
+               label: "Rate Limiting",
+               getNode: () => (
+                 <RateLimitForm
+                   rateLimits={
+                     this.state.siteRes.site_view.local_site_rate_limit
+                   }
+                   onSaveSite={this.handleEditSite}
+                 />
+               ),
+             },
+             {
+               key: "taglines",
+               label: i18n.t("taglines"),
+               getNode: () => (
+                 <div className="row">
+                   <TaglineForm
+                     taglines={this.state.siteRes.taglines}
+                     onSaveSite={this.handleEditSite}
+                   />
+                 </div>
+               ),
+             },
+             {
+               key: "emojis",
+               label: i18n.t("emojis"),
+               getNode: () => (
+                 <div className="row">
+                   <EmojiForm
+                     onCreate={this.handleCreateEmoji}
+                     onDelete={this.handleDeleteEmoji}
+                     onEdit={this.handleEditEmoji}
+                   />
+                 </div>
+               ),
+             },
+           ]}
+         />
        </div>
      );
    }
  
++  async fetchData() {
++    this.setState({
++      bannedRes: { state: "loading" },
++      instancesRes: { state: "loading" },
++      themeList: [],
++    });
++
++    const auth = myAuthRequired();
++
++    const [bannedRes, instancesRes, themeList] = await Promise.all([
++      HttpService.client.getBannedPersons({ auth }),
++      HttpService.client.getFederatedInstances({ auth }),
++      fetchThemeList(),
++    ]);
++
++    this.setState({
++      bannedRes,
++      instancesRes,
++      themeList,
++    });
++  }
++
    admins() {
      return (
        <>
          onClick={linkEvent(this, this.handleLeaveAdminTeam)}
          className="btn btn-danger mb-2"
        >
-         {this.state.leaveAdminTeamLoading ? (
+         {this.state.leaveAdminTeamRes.state == "loading" ? (
            <Spinner />
          ) : (
            i18n.t("leave_admin_team")
    }
  
    bannedUsers() {
-     return (
-       <>
-         <h5>{i18n.t("banned_users")}</h5>
-         <ul className="list-unstyled">
-           {this.state.banned.map(banned => (
-             <li key={banned.person.id} className="list-inline-item">
-               <PersonListing person={banned.person} />
-             </li>
-           ))}
-         </ul>
-       </>
-     );
+     switch (this.state.bannedRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const bans = this.state.bannedRes.data.banned;
+         return (
+           <>
+             <h5>{i18n.t("banned_users")}</h5>
+             <ul className="list-unstyled">
+               {bans.map(banned => (
+                 <li key={banned.person.id} className="list-inline-item">
+                   <PersonListing person={banned.person} />
+                 </li>
+               ))}
+             </ul>
+           </>
+         );
+       }
+     }
    }
  
-   handleLeaveAdminTeam(i: AdminSettings) {
-     let auth = myAuth();
-     if (auth) {
-       i.setState({ leaveAdminTeamLoading: true });
-       WebSocketService.Instance.send(wsClient.leaveAdmin({ auth }));
+   async handleEditSite(form: EditSite) {
+     const editRes = await HttpService.client.editSite(form);
+     if (editRes.state === "success") {
+       this.setState(s => {
+         s.siteRes.site_view = editRes.data.site_view;
+         // TODO: Where to get taglines from?
+         s.siteRes.taglines = editRes.data.taglines;
+         return s;
+       });
+       toast(i18n.t("site_saved"));
      }
+     return editRes;
    }
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       this.context.router.history.push("/");
-       this.setState({ loading: false });
-       return;
-     } else if (op == UserOperation.EditSite) {
-       let data = wsJsonToRes<SiteResponse>(msg);
-       this.setState(s => ((s.siteRes.site_view = data.site_view), s));
-       toast(i18n.t("site_saved"));
-     } else if (op == UserOperation.GetBannedPersons) {
-       let data = wsJsonToRes<BannedPersonsResponse>(msg);
-       this.setState({ banned: data.banned, loading: false });
-     } else if (op == UserOperation.LeaveAdmin) {
-       let data = wsJsonToRes<GetSiteResponse>(msg);
-       this.setState(s => ((s.siteRes.site_view = data.site_view), s));
-       this.setState({ leaveAdminTeamLoading: false });
+   handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
+     i.ctx.setState({ currentTab: i.tab });
+   }
+   async handleLeaveAdminTeam(i: AdminSettings) {
+     i.setState({ leaveAdminTeamRes: { state: "loading" } });
+     this.setState({
+       leaveAdminTeamRes: await HttpService.client.leaveAdmin({
+         auth: myAuthRequired(),
+       }),
+     });
+     if (this.state.leaveAdminTeamRes.state === "success") {
        toast(i18n.t("left_admin_team"));
-       this.context.router.history.push("/");
-     } else if (op == UserOperation.GetFederatedInstances) {
-       let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
-       this.setState({ instancesRes: data });
+       this.context.router.history.replace("/");
+     }
+   }
+   async handleEditEmoji(form: EditCustomEmoji) {
+     const res = await HttpService.client.editCustomEmoji(form);
+     if (res.state === "success") {
+       updateEmojiDataModel(res.data.custom_emoji);
+     }
+   }
+   async handleDeleteEmoji(form: DeleteCustomEmoji) {
+     const res = await HttpService.client.deleteCustomEmoji(form);
+     if (res.state === "success") {
+       removeFromEmojiDataModel(res.data.id);
+     }
+   }
+   async handleCreateEmoji(form: CreateCustomEmoji) {
+     const res = await HttpService.client.createCustomEmoji(form);
+     if (res.state === "success") {
+       updateEmojiDataModel(res.data.custom_emoji);
      }
    }
  }
index e85c3e66899ff326b693f2645d1c7e42dbc20ded,8be983042a5ce584988bb282d928656678ddfeb0..cc9dd518dbddac5ae5a9c651f691a1597dc9ec8f
@@@ -3,13 -3,27 +3,27 @@@ import { Component, linkEvent, MouseEve
  import { T } from "inferno-i18next-dess";
  import { Link } from "inferno-router";
  import {
-   AddAdminResponse,
+   AddAdmin,
+   AddModToCommunity,
+   BanFromCommunity,
+   BanFromCommunityResponse,
+   BanPerson,
    BanPersonResponse,
-   BlockPersonResponse,
-   CommentReportResponse,
+   BlockPerson,
+   CommentId,
+   CommentReplyResponse,
    CommentResponse,
-   CommentView,
-   CommunityView,
+   CreateComment,
+   CreateCommentLike,
+   CreateCommentReport,
+   CreatePostLike,
+   CreatePostReport,
+   DeleteComment,
+   DeletePost,
+   DistinguishComment,
+   EditComment,
+   EditPost,
+   FeaturePost,
    GetComments,
    GetCommentsResponse,
    GetPosts,
    ListCommunities,
    ListCommunitiesResponse,
    ListingType,
-   PostReportResponse,
+   LockPost,
+   MarkCommentReplyAsRead,
+   MarkPersonMentionAsRead,
    PostResponse,
-   PostView,
+   PurgeComment,
    PurgeItemResponse,
-   SiteResponse,
+   PurgePerson,
+   PurgePost,
+   RemoveComment,
+   RemovePost,
+   SaveComment,
+   SavePost,
    SortType,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   TransferCommunity,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import {
    CommentViewType,
    DataType,
    InitialFetchRequest,
  } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
    canCreateCommunity,
    commentsToFlatNodes,
-   createCommentLikeRes,
-   createPostLikeFindRes,
-   editCommentRes,
-   editPostFindRes,
+   editComment,
+   editPost,
+   editWith,
    enableDownvotes,
    enableNsfw,
    fetchLimit,
+   getCommentParentId,
    getDataTypeString,
    getPageFromString,
    getQueryParams,
    getQueryString,
    getRandomFromList,
-   isBrowser,
-   isPostBlocked,
    mdToHtml,
    myAuth,
-   notifyPost,
-   nsfwCheck,
    postToCommentSortType,
    QueryParams,
    relTags,
    restoreScrollPosition,
-   saveCommentRes,
++  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -69,9 -84,6 +85,6 @@@
    toast,
    trendingFetchLimit,
    updatePersonBlock,
-   WithPromiseKeys,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { CommentNodes } from "../comment/comment-nodes";
  import { DataTypeSelect } from "../common/data-type-select";
@@@ -85,16 -97,17 +98,17 @@@ import { PostListings } from "../post/p
  import { SiteSidebar } from "./site-sidebar";
  
  interface HomeState {
-   trendingCommunities: CommunityView[];
-   siteRes: GetSiteResponse;
-   posts: PostView[];
-   comments: CommentView[];
+   postsRes: RequestState<GetPostsResponse>;
+   commentsRes: RequestState<GetCommentsResponse>;
+   trendingCommunitiesRes: RequestState<ListCommunitiesResponse>;
    showSubscribedMobile: boolean;
    showTrendingMobile: boolean;
    showSidebarMobile: boolean;
    subscribedCollapsed: boolean;
-   loading: boolean;
    tagline?: string;
+   siteRes: GetSiteResponse;
+   finished: Map<CommentId, boolean | undefined>;
+   isIsomorphic: boolean;
  }
  
  interface HomeProps {
    page: number;
  }
  
- interface HomeData {
++type HomeData = RouteDataResponse<{
 +  postsResponse?: GetPostsResponse;
 +  commentsResponse?: GetCommentsResponse;
 +  trendingResponse: ListCommunitiesResponse;
- }
++}>;
 +
  function getDataTypeFromQuery(type?: string): DataType {
    return type ? DataType[type] : DataType.Post;
  }
@@@ -119,7 -126,7 +133,7 @@@ function getListingTypeFromQuery(type?
      UserService.Instance.myUserInfo?.local_user_view?.local_user
        ?.default_listing_type;
  
-   return type ? (type as ListingType) : myListingType ?? "Local";
+   return (type ? (type as ListingType) : myListingType) ?? "Local";
  }
  
  function getSortTypeFromQuery(type?: string): SortType {
      UserService.Instance.myUserInfo?.local_user_view?.local_user
        ?.default_sort_type;
  
-   return type ? (type as SortType) : mySortType ?? "Active";
+   return (type ? (type as SortType) : mySortType) ?? "Active";
  }
  
  const getHomeQueryParams = () =>
      dataType: getDataTypeFromQuery,
    });
  
- function fetchTrendingCommunities() {
-   const listCommunitiesForm: ListCommunities = {
-     type_: "Local",
-     sort: "Hot",
-     limit: trendingFetchLimit,
-     auth: myAuth(false),
-   };
-   WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
- }
- function fetchData() {
-   const auth = myAuth(false);
-   const { dataType, page, listingType, sort } = getHomeQueryParams();
-   let req: string;
-   if (dataType === DataType.Post) {
-     const getPostsForm: GetPosts = {
-       page,
-       limit: fetchLimit,
-       sort,
-       saved_only: false,
-       type_: listingType,
-       auth,
-     };
-     req = wsClient.getPosts(getPostsForm);
-   } else {
-     const getCommentsForm: GetComments = {
-       page,
-       limit: fetchLimit,
-       sort: postToCommentSortType(sort),
-       saved_only: false,
-       type_: listingType,
-       auth,
-     };
-     req = wsClient.getComments(getCommentsForm);
-   }
-   WebSocketService.Instance.send(req);
- }
  const MobileButton = ({
    textKey,
    show,
@@@ -210,52 -175,19 +182,19 @@@ const LinkButton = (
    </Link>
  );
  
- function getRss(listingType: ListingType) {
-   const { sort } = getHomeQueryParams();
-   const auth = myAuth(false);
-   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} />
-       </>
-     )
-   );
- }
  export class Home extends Component<any, HomeState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<HomeData>(this.context);
-   private subscription?: Subscription;
    state: HomeState = {
-     trendingCommunities: [],
+     postsRes: { state: "empty" },
+     commentsRes: { state: "empty" },
+     trendingCommunitiesRes: { state: "empty" },
      siteRes: this.isoData.site_res,
      showSubscribedMobile: false,
      showTrendingMobile: false,
      showSidebarMobile: false,
      subscribedCollapsed: false,
-     loading: true,
-     posts: [],
-     comments: [],
+     finished: new Map(),
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
      this.handlePageChange = this.handlePageChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleCreateComment = this.handleCreateComment.bind(this);
+     this.handleEditComment = this.handleEditComment.bind(this);
+     this.handleSaveComment = this.handleSaveComment.bind(this);
+     this.handleBlockPerson = this.handleBlockPerson.bind(this);
+     this.handleDeleteComment = this.handleDeleteComment.bind(this);
+     this.handleRemoveComment = this.handleRemoveComment.bind(this);
+     this.handleCommentVote = this.handleCommentVote.bind(this);
+     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+     this.handleAddAdmin = this.handleAddAdmin.bind(this);
+     this.handlePurgePerson = this.handlePurgePerson.bind(this);
+     this.handlePurgeComment = this.handlePurgeComment.bind(this);
+     this.handleCommentReport = this.handleCommentReport.bind(this);
+     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+     this.handleBanPerson = this.handleBanPerson.bind(this);
+     this.handlePostEdit = this.handlePostEdit.bind(this);
+     this.handlePostVote = this.handlePostVote.bind(this);
+     this.handlePostReport = this.handlePostReport.bind(this);
+     this.handleLockPost = this.handleLockPost.bind(this);
+     this.handleDeletePost = this.handleDeletePost.bind(this);
+     this.handleRemovePost = this.handleRemovePost.bind(this);
+     this.handleSavePost = this.handleSavePost.bind(this);
+     this.handlePurgePost = this.handlePurgePost.bind(this);
+     this.handleFeaturePost = this.handleFeaturePost.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
-       const { trendingResponse, commentsResponse, postsResponse } =
-         this.isoData.routeData;
+     if (FirstLoadService.isFirstLoad) {
 -      const [postsRes, commentsRes, trendingCommunitiesRes] =
 -        this.isoData.routeData;
++      const {
++        trendingResponse: trendingCommunitiesRes,
++        commentsResponse: commentsRes,
++        postsResponse: postsRes,
++      } = this.isoData.routeData;
  
-       if (postsResponse) {
-         this.state = { ...this.state, posts: postsResponse.posts };
-       }
+       this.state = {
+         ...this.state,
 -        postsRes,
 -        commentsRes,
+         trendingCommunitiesRes,
+         tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
+           ?.content,
+         isIsomorphic: true,
+       };
 +
-       if (commentsResponse) {
-         this.state = { ...this.state, comments: commentsResponse.comments };
++      if (commentsRes?.state === "success") {
++        this.state = {
++          ...this.state,
++          commentsRes,
++        };
 +      }
 +
-       if (isBrowser()) {
-         WebSocketService.Instance.send(
-           wsClient.communityJoin({ community_id: 0 })
-         );
++      if (postsRes?.state === "success") {
++        this.state = {
++          ...this.state,
++          postsRes,
++        };
 +      }
-       const taglines = this.state?.siteRes?.taglines ?? [];
-       this.state = {
-         ...this.state,
-         trendingCommunities: trendingResponse?.communities ?? [],
-         loading: false,
-         tagline: getRandomFromList(taglines)?.content,
-       };
-     } else {
-       fetchTrendingCommunities();
-       fetchData();
      }
    }
  
-   componentDidMount() {
-     // This means it hasn't been set up yet
-     if (!this.state.siteRes.site_view.local_site.site_setup) {
-       this.context.router.history.push("/setup");
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
      }
      setupTippy();
    }
  
    componentWillUnmount() {
      saveScrollPosition(this.context);
-     this.subscription?.unsubscribe();
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      auth,
      query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
-   }: InitialFetchRequest<QueryParams<HomeProps>>): WithPromiseKeys<HomeData> {
 -  }: 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<any>[] = [];
-     let postsResponse: Promise<GetPostsResponse> | undefined = undefined;
-     let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
 -    const promises: Promise<RequestState<any>>[] = [];
++    let postsResponse: RequestState<GetPostsResponse> | undefined = undefined;
++    let commentsResponse: RequestState<GetCommentsResponse> | undefined =
++      undefined;
  
      if (dataType === DataType.Post) {
        const getPostsForm: GetPosts = {
          auth,
        };
  
-       postsResponse = client.getPosts(getPostsForm);
 -      promises.push(client.getPosts(getPostsForm));
 -      promises.push(Promise.resolve({ state: "empty" }));
++      postsResponse = await client.getPosts(getPostsForm);
      } else {
        const getCommentsForm: GetComments = {
          page,
          saved_only: false,
          auth,
        };
 -      promises.push(Promise.resolve({ state: "empty" }));
 -      promises.push(client.getComments(getCommentsForm));
 +
-       commentsResponse = client.getComments(getCommentsForm);
++      commentsResponse = await client.getComments(getCommentsForm);
      }
  
      const trendingCommunitiesForm: ListCommunities = {
        limit: trendingFetchLimit,
        auth,
      };
--    promises.push(client.listCommunities(trendingCommunitiesForm));
  
 -    return promises;
 +    return {
-       trendingResponse: client.listCommunities(trendingCommunitiesForm),
++      trendingResponse: await client.listCommunities(trendingCommunitiesForm),
 +      commentsResponse,
 +      postsResponse,
 +    };
    }
  
    get documentTitle(): string {
          admins,
          online,
        },
-       loading,
      } = this.state;
  
      return (
        <div>
-         {!loading && (
-           <div>
-             <div className="card border-secondary mb-3">
-               <div className="card-body">
-                 {this.trendingCommunities()}
-                 {canCreateCommunity(this.state.siteRes) && (
-                   <LinkButton
-                     path="/create_community"
-                     translationKey="create_a_community"
-                   />
-                 )}
+         <div>
+           <div className="card border-secondary mb-3">
+             <div className="card-body">
+               {this.trendingCommunities()}
+               {canCreateCommunity(this.state.siteRes) && (
                  <LinkButton
-                   path="/communities"
-                   translationKey="explore_communities"
+                   path="/create_community"
+                   translationKey="create_a_community"
                  />
-               </div>
+               )}
+               <LinkButton
+                 path="/communities"
+                 translationKey="explore_communities"
+               />
              </div>
-             <SiteSidebar
-               site={site}
-               admins={admins}
-               counts={counts}
-               online={online}
-               showLocal={showLocal(this.isoData)}
-             />
-             {this.hasFollows && (
-               <div className="card border-secondary mb-3">
-                 <div className="card-body">{this.subscribedCommunities}</div>
-               </div>
-             )}
            </div>
-         )}
+           <SiteSidebar
+             site={site}
+             admins={admins}
+             counts={counts}
+             online={online}
+             showLocal={showLocal(this.isoData)}
+           />
+           {this.hasFollows && (
+             <div className="card border-secondary mb-3">
+               <div className="card-body">{this.subscribedCommunities}</div>
+             </div>
+           )}
+         </div>
        </div>
      );
    }
  
    trendingCommunities(isMobile = false) {
-     return (
-       <div className={!isMobile ? "mb-2" : ""}>
-         <h5>
-           <T i18nKey="trending_communities">
-             #
-             <Link className="text-body" to="/communities">
-               #
-             </Link>
-           </T>
-         </h5>
-         <ul className="list-inline mb-0">
-           {this.state.trendingCommunities.map(cv => (
-             <li
-               key={cv.community.id}
-               className="list-inline-item d-inline-block"
-             >
-               <CommunityLink community={cv.community} />
-             </li>
-           ))}
-         </ul>
-       </div>
-     );
+     switch (this.state.trendingCommunitiesRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       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">
+                   #
+                 </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>
+         );
+       }
+     }
    }
  
    get subscribedCommunities() {
      );
    }
  
-   updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
+   async updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
      const {
        dataType: urlDataType,
        listingType: urlListingType,
        search: getQueryString(queryParams),
      });
  
-     this.setState({
-       loading: true,
-       posts: [],
-       comments: [],
-     });
-     fetchData();
+     await this.fetchData();
    }
  
    posts() {
  
      return (
        <div className="main-content-wrapper">
-         {this.state.loading ? (
-           <h5>
-             <Spinner large />
-           </h5>
-         ) : (
-           <div>
-             {this.selects()}
-             {this.listings}
-             <Paginator page={page} onChange={this.handlePageChange} />
-           </div>
-         )}
+         <div>
+           {this.selects}
+           {this.listings}
+           <Paginator page={page} onChange={this.handlePageChange} />
+         </div>
        </div>
      );
    }
  
    get listings() {
      const { dataType } = getHomeQueryParams();
-     const { siteRes, posts, comments } = this.state;
-     return dataType === DataType.Post ? (
-       <PostListings
-         posts={posts}
-         showCommunity
-         removeDuplicates
-         enableDownvotes={enableDownvotes(siteRes)}
-         enableNsfw={enableNsfw(siteRes)}
-         allLanguages={siteRes.all_languages}
-         siteLanguages={siteRes.discussion_languages}
-       />
-     ) : (
-       <CommentNodes
-         nodes={commentsToFlatNodes(comments)}
-         viewType={CommentViewType.Flat}
-         noIndent
-         showCommunity
-         showContext
-         enableDownvotes={enableDownvotes(siteRes)}
-         allLanguages={siteRes.all_languages}
-         siteLanguages={siteRes.discussion_languages}
-       />
-     );
+     const siteRes = this.state.siteRes;
+     if (dataType === DataType.Post) {
+       switch (this.state.postsRes.state) {
+         case "loading":
+           return (
+             <h5>
+               <Spinner large />
+             </h5>
+           );
+         case "success": {
+           const posts = this.state.postsRes.data.posts;
+           return (
+             <PostListings
+               posts={posts}
+               showCommunity
+               removeDuplicates
+               enableDownvotes={enableDownvotes(siteRes)}
+               enableNsfw={enableNsfw(siteRes)}
+               allLanguages={siteRes.all_languages}
+               siteLanguages={siteRes.discussion_languages}
+               onBlockPerson={this.handleBlockPerson}
+               onPostEdit={this.handlePostEdit}
+               onPostVote={this.handlePostVote}
+               onPostReport={this.handlePostReport}
+               onLockPost={this.handleLockPost}
+               onDeletePost={this.handleDeletePost}
+               onRemovePost={this.handleRemovePost}
+               onSavePost={this.handleSavePost}
+               onPurgePerson={this.handlePurgePerson}
+               onPurgePost={this.handlePurgePost}
+               onBanPerson={this.handleBanPerson}
+               onBanPersonFromCommunity={this.handleBanFromCommunity}
+               onAddModToCommunity={this.handleAddModToCommunity}
+               onAddAdmin={this.handleAddAdmin}
+               onTransferCommunity={this.handleTransferCommunity}
+               onFeaturePost={this.handleFeaturePost}
+             />
+           );
+         }
+       }
+     } else {
+       switch (this.state.commentsRes.state) {
+         case "loading":
+           return (
+             <h5>
+               <Spinner large />
+             </h5>
+           );
+         case "success": {
+           const comments = this.state.commentsRes.data.comments;
+           return (
+             <CommentNodes
+               nodes={commentsToFlatNodes(comments)}
+               viewType={CommentViewType.Flat}
+               finished={this.state.finished}
+               noIndent
+               showCommunity
+               showContext
+               enableDownvotes={enableDownvotes(siteRes)}
+               allLanguages={siteRes.all_languages}
+               siteLanguages={siteRes.discussion_languages}
+               onSaveComment={this.handleSaveComment}
+               onBlockPerson={this.handleBlockPerson}
+               onDeleteComment={this.handleDeleteComment}
+               onRemoveComment={this.handleRemoveComment}
+               onCommentVote={this.handleCommentVote}
+               onCommentReport={this.handleCommentReport}
+               onDistinguishComment={this.handleDistinguishComment}
+               onAddModToCommunity={this.handleAddModToCommunity}
+               onAddAdmin={this.handleAddAdmin}
+               onTransferCommunity={this.handleTransferCommunity}
+               onPurgeComment={this.handlePurgeComment}
+               onPurgePerson={this.handlePurgePerson}
+               onCommentReplyRead={this.handleCommentReplyRead}
+               onPersonMentionRead={this.handlePersonMentionRead}
+               onBanPersonFromCommunity={this.handleBanFromCommunity}
+               onBanPerson={this.handleBanPerson}
+               onCreateComment={this.handleCreateComment}
+               onEditComment={this.handleEditComment}
+             />
+           );
+         }
+       }
+     }
    }
  
-   selects() {
+   get selects() {
      const { listingType, dataType, sort } = getHomeQueryParams();
  
      return (
          <span className="mr-2">
            <SortSelect sort={sort} onChange={this.handleSortChange} />
          </span>
-         {getRss(listingType)}
+         {this.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({
+       trendingCommunitiesRes: await HttpService.client.listCommunities({
+         type_: "Local",
+         sort: "Hot",
+         limit: trendingFetchLimit,
+         auth: myAuth(),
+       }),
+     });
+   }
+   async fetchData() {
+     const auth = myAuth();
+     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,
+         }),
+       });
+     } else {
+       this.setState({ commentsRes: { state: "loading" } });
+       this.setState({
+         commentsRes: await HttpService.client.getComments({
+           page,
+           limit: fetchLimit,
+           sort: postToCommentSortType(sort),
+           saved_only: false,
+           type_: listingType,
+           auth,
+         }),
+       });
+     }
+     restoreScrollPosition(this.context);
+     setupTippy();
+   }
    handleShowSubscribedMobile(i: Home) {
      i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
    }
      window.scrollTo(0, 0);
    }
  
-   parseMessage(msg: any) {
-     const op = wsUserOp(msg);
-     console.log(msg);
+   async handleAddModToCommunity(form: AddModToCommunity) {
+     // TODO not sure what to do here
+     await HttpService.client.addModToCommunity(form);
+   }
  
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-     } else if (msg.reconnect) {
-       WebSocketService.Instance.send(
-         wsClient.communityJoin({ community_id: 0 })
-       );
-       fetchData();
-     } else {
-       switch (op) {
-         case UserOperation.ListCommunities: {
-           const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
-           this.setState({ trendingCommunities: communities });
+   async handlePurgePerson(form: PurgePerson) {
+     const purgePersonRes = await HttpService.client.purgePerson(form);
+     this.purgeItem(purgePersonRes);
+   }
  
-           break;
-         }
+   async handlePurgeComment(form: PurgeComment) {
+     const purgeCommentRes = await HttpService.client.purgeComment(form);
+     this.purgeItem(purgeCommentRes);
+   }
  
-         case UserOperation.EditSite: {
-           const { site_view } = wsJsonToRes<SiteResponse>(msg);
-           this.setState(s => ((s.siteRes.site_view = site_view), s));
-           toast(i18n.t("site_saved"));
+   async handlePurgePost(form: PurgePost) {
+     const purgeRes = await HttpService.client.purgePost(form);
+     this.purgeItem(purgeRes);
+   }
  
-           break;
-         }
+   async handleBlockPerson(form: BlockPerson) {
+     const blockPersonRes = await HttpService.client.blockPerson(form);
+     if (blockPersonRes.state == "success") {
+       updatePersonBlock(blockPersonRes.data);
+     }
+   }
  
-         case UserOperation.GetPosts: {
-           const { posts } = wsJsonToRes<GetPostsResponse>(msg);
-           this.setState({ posts, loading: false });
-           WebSocketService.Instance.send(
-             wsClient.communityJoin({ community_id: 0 })
-           );
-           restoreScrollPosition(this.context);
-           setupTippy();
+   async handleCreateComment(form: CreateComment) {
+     const createCommentRes = await HttpService.client.createComment(form);
+     this.createAndUpdateComments(createCommentRes);
  
-           break;
-         }
+     return createCommentRes;
+   }
  
-         case UserOperation.CreatePost: {
-           const { page, listingType } = getHomeQueryParams();
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
-           if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
-             const mui = UserService.Instance.myUserInfo;
-             const showPostNotifs =
-               mui?.local_user_view.local_user.show_new_post_notifs;
-             let shouldAddPost: boolean;
-             switch (listingType) {
-               case "Subscribed": {
-                 // If you're on subscribed, only push it if you're subscribed.
-                 shouldAddPost = !!mui?.follows.some(
-                   ({ community: { id } }) => id === post_view.community.id
-                 );
-                 break;
-               }
-               case "Local": {
-                 // If you're on the local view, only push it if its local
-                 shouldAddPost = post_view.post.local;
-                 break;
-               }
-               default: {
-                 shouldAddPost = true;
-                 break;
-               }
-             }
-             if (shouldAddPost) {
-               this.setState(({ posts }) => ({
-                 posts: [post_view].concat(posts),
-               }));
-               if (showPostNotifs) {
-                 notifyPost(post_view, this.context.router);
-               }
-             }
-           }
-           break;
-         }
+   async handleEditComment(form: EditComment) {
+     const editCommentRes = await HttpService.client.editComment(form);
+     this.findAndUpdateComment(editCommentRes);
  
-         case UserOperation.EditPost:
-         case UserOperation.DeletePost:
-         case UserOperation.RemovePost:
-         case UserOperation.LockPost:
-         case UserOperation.FeaturePost:
-         case UserOperation.SavePost: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           editPostFindRes(post_view, this.state.posts);
-           this.setState(this.state);
-           break;
-         }
+     return editCommentRes;
+   }
  
-         case UserOperation.CreatePostLike: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           createPostLikeFindRes(post_view, this.state.posts);
-           this.setState(this.state);
+   async handleDeleteComment(form: DeleteComment) {
+     const deleteCommentRes = await HttpService.client.deleteComment(form);
+     this.findAndUpdateComment(deleteCommentRes);
+   }
  
-           break;
-         }
+   async handleDeletePost(form: DeletePost) {
+     const deleteRes = await HttpService.client.deletePost(form);
+     this.findAndUpdatePost(deleteRes);
+   }
  
-         case UserOperation.AddAdmin: {
-           const { admins } = wsJsonToRes<AddAdminResponse>(msg);
-           this.setState(s => ((s.siteRes.admins = admins), s));
+   async handleRemovePost(form: RemovePost) {
+     const removeRes = await HttpService.client.removePost(form);
+     this.findAndUpdatePost(removeRes);
+   }
  
-           break;
-         }
+   async handleRemoveComment(form: RemoveComment) {
+     const removeCommentRes = await HttpService.client.removeComment(form);
+     this.findAndUpdateComment(removeCommentRes);
+   }
  
-         case UserOperation.BanPerson: {
-           const {
-             banned,
-             person_view: {
-               person: { id },
-             },
-           } = wsJsonToRes<BanPersonResponse>(msg);
+   async handleSaveComment(form: SaveComment) {
+     const saveCommentRes = await HttpService.client.saveComment(form);
+     this.findAndUpdateComment(saveCommentRes);
+   }
  
-           this.state.posts
-             .filter(p => p.creator.id == id)
-             .forEach(p => (p.creator.banned = banned));
-           this.setState(this.state);
+   async handleSavePost(form: SavePost) {
+     const saveRes = await HttpService.client.savePost(form);
+     this.findAndUpdatePost(saveRes);
+   }
  
-           break;
-         }
+   async handleFeaturePost(form: FeaturePost) {
+     const featureRes = await HttpService.client.featurePost(form);
+     this.findAndUpdatePost(featureRes);
+   }
  
-         case UserOperation.GetComments: {
-           const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
-           this.setState({ comments, loading: false });
+   async handleCommentVote(form: CreateCommentLike) {
+     const voteRes = await HttpService.client.likeComment(form);
+     this.findAndUpdateComment(voteRes);
+   }
  
-           break;
-         }
+   async handlePostEdit(form: EditPost) {
+     const res = await HttpService.client.editPost(form);
+     this.findAndUpdatePost(res);
+   }
  
-         case UserOperation.EditComment:
-         case UserOperation.DeleteComment:
-         case UserOperation.RemoveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           editCommentRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handlePostVote(form: CreatePostLike) {
+     const voteRes = await HttpService.client.likePost(form);
+     this.findAndUpdatePost(voteRes);
+   }
  
-           break;
-         }
+   async handleCommentReport(form: CreateCommentReport) {
+     const reportRes = await HttpService.client.createCommentReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-         case UserOperation.CreateComment: {
-           const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
-           // Necessary since it might be a user reply
-           if (form_id) {
-             const { listingType } = getHomeQueryParams();
-             // If you're on subscribed, only push it if you're subscribed.
-             const shouldAddComment =
-               listingType === "Subscribed"
-                 ? UserService.Instance.myUserInfo?.follows.some(
-                     ({ community: { id } }) => id === comment_view.community.id
-                   )
-                 : true;
-             if (shouldAddComment) {
-               this.setState(({ comments }) => ({
-                 comments: [comment_view].concat(comments),
-               }));
-             }
-           }
-           break;
-         }
+   async handlePostReport(form: CreatePostReport) {
+     const reportRes = await HttpService.client.createPostReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-         case UserOperation.SaveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           saveCommentRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handleLockPost(form: LockPost) {
+     const lockRes = await HttpService.client.lockPost(form);
+     this.findAndUpdatePost(lockRes);
+   }
  
-           break;
-         }
+   async handleDistinguishComment(form: DistinguishComment) {
+     const distinguishRes = await HttpService.client.distinguishComment(form);
+     this.findAndUpdateComment(distinguishRes);
+   }
  
-         case UserOperation.CreateCommentLike: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           createCommentLikeRes(comment_view, this.state.comments);
-           this.setState(this.state);
+   async handleAddAdmin(form: AddAdmin) {
+     const addAdminRes = await HttpService.client.addAdmin(form);
  
-           break;
-         }
+     if (addAdminRes.state == "success") {
+       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+     }
+   }
  
-         case UserOperation.BlockPerson: {
-           const data = wsJsonToRes<BlockPersonResponse>(msg);
-           updatePersonBlock(data);
+   async handleTransferCommunity(form: TransferCommunity) {
+     await HttpService.client.transferCommunity(form);
+     toast(i18n.t("transfer_community"));
+   }
  
-           break;
-         }
+   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+     const readRes = await HttpService.client.markCommentReplyAsRead(form);
+     this.findAndUpdateCommentReply(readRes);
+   }
  
-         case UserOperation.CreatePostReport: {
-           const data = wsJsonToRes<PostReportResponse>(msg);
-           if (data) {
-             toast(i18n.t("report_created"));
-           }
+   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+     await HttpService.client.markPersonMentionAsRead(form);
+   }
  
-           break;
-         }
+   async handleBanFromCommunity(form: BanFromCommunity) {
+     const banRes = await HttpService.client.banFromCommunity(form);
+     this.updateBanFromCommunity(banRes);
+   }
  
-         case UserOperation.CreateCommentReport: {
-           const data = wsJsonToRes<CommentReportResponse>(msg);
-           if (data) {
-             toast(i18n.t("report_created"));
-           }
+   async handleBanPerson(form: BanPerson) {
+     const banRes = await HttpService.client.banPerson(form);
+     this.updateBan(banRes);
+   }
  
-           break;
+   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.postsRes.state == "success") {
+           s.postsRes.data.posts
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
+         }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
          }
+         return s;
+       });
+     }
+   }
  
-         case UserOperation.PurgePerson:
-         case UserOperation.PurgePost:
-         case UserOperation.PurgeComment:
-         case UserOperation.PurgeCommunity: {
-           const data = wsJsonToRes<PurgeItemResponse>(msg);
-           if (data.success) {
-             toast(i18n.t("purge_success"));
-             this.context.router.history.push(`/`);
-           }
-           break;
+   updateBan(banRes: RequestState<BanPersonResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.postsRes.state == "success") {
+           s.postsRes.data.posts
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
          }
-       }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
+         }
+         return s;
+       });
      }
    }
+   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+     if (purgeRes.state == "success") {
+       toast(i18n.t("purge_success"));
+       this.context.router.history.push(`/`);
+     }
+   }
+   findAndUpdateComment(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editComment(
+           res.data.comment_view,
+           s.commentsRes.data.comments
+         );
+         s.finished.set(res.data.comment_view.comment.id, true);
+       }
+       return s;
+     });
+   }
+   createAndUpdateComments(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments.unshift(res.data.comment_view);
+         // Set finished for the parent
+         s.finished.set(
+           getCommentParentId(res.data.comment_view.comment) ?? 0,
+           true
+         );
+       }
+       return s;
+     });
+   }
+   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editWith(
+           res.data.comment_reply_view,
+           s.commentsRes.data.comments
+         );
+       }
+       return s;
+     });
+   }
+   findAndUpdatePost(res: RequestState<PostResponse>) {
+     this.setState(s => {
+       if (s.postsRes.state == "success" && res.state == "success") {
+         s.postsRes.data.posts = editPost(
+           res.data.post_view,
+           s.postsRes.data.posts
+         );
+       }
+       return s;
+     });
+   }
  }
index fd1ed617600f8ee25de625bbb6e2a0c4408f55f3,30cb9dea0c0491a020890c3206d20c14709dd09e..bec472cf1ea9d6aca77ed7bd3f6685f5788cc272
@@@ -3,112 -3,113 +3,119 @@@ import 
    GetFederatedInstancesResponse,
    GetSiteResponse,
    Instance,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
- import {
-   WithPromiseKeys,
-   isBrowser,
-   relTags,
-   setIsoData,
-   toast,
-   wsClient,
-   wsSubscribe,
- } from "../../utils";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
 -import { relTags, setIsoData } from "../../utils";
++import { RouteDataResponse, relTags, setIsoData } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
+ import { Spinner } from "../common/icon";
  
- interface InstancesData {
++type InstancesData = RouteDataResponse<{
 +  federatedInstancesResponse: GetFederatedInstancesResponse;
- }
++}>;
 +
  interface InstancesState {
+   instancesRes: RequestState<GetFederatedInstancesResponse>;
    siteRes: GetSiteResponse;
-   instancesRes?: GetFederatedInstancesResponse;
-   loading: boolean;
+   isIsomorphic: boolean;
  }
  
  export class Instances extends Component<any, InstancesState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<InstancesData>(this.context);
    state: InstancesState = {
+     instancesRes: { state: "empty" },
      siteRes: this.isoData.site_res,
-     loading: true,
+     isIsomorphic: false,
    };
-   private subscription?: Subscription;
  
    constructor(props: any, context: any) {
      super(props, context);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      // Only fetch the data if coming from another route
-     if (this.isoData.path == this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
        this.state = {
          ...this.state,
 -        instancesRes: this.isoData.routeData[0],
 +        instancesRes: this.isoData.routeData.federatedInstancesResponse,
-         loading: false,
+         isIsomorphic: true,
        };
-     } else {
-       WebSocketService.Instance.send(wsClient.getFederatedInstances({}));
      }
    }
  
-   static fetchInitialData({
-     client,
-   }: InitialFetchRequest): WithPromiseKeys<InstancesData> {
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.fetchInstances();
+     }
+   }
+   async fetchInstances() {
+     this.setState({
+       instancesRes: { state: "loading" },
+     });
+     this.setState({
+       instancesRes: await HttpService.client.getFederatedInstances({}),
+     });
+   }
 -  static fetchInitialData(
++  static async fetchInitialData(
+     req: InitialFetchRequest
 -  ): Promise<RequestState<any>>[] {
 -    return [req.client.getFederatedInstances({})];
++  ): Promise<InstancesData> {
 +    return {
-       federatedInstancesResponse: client.getFederatedInstances(
-         {}
-       ) as Promise<GetFederatedInstancesResponse>,
++      federatedInstancesResponse: await req.client.getFederatedInstances({}),
 +    };
    }
  
    get documentTitle(): string {
      return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
    }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   renderInstances() {
+     switch (this.state.instancesRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const instances = this.state.instancesRes.data.federated_instances;
+         return instances ? (
+           <div className="row">
+             <div className="col-md-6">
+               <h5>{i18n.t("linked_instances")}</h5>
+               {this.itemList(instances.linked)}
+             </div>
+             {instances.allowed && instances.allowed.length > 0 && (
+               <div className="col-md-6">
+                 <h5>{i18n.t("allowed_instances")}</h5>
+                 {this.itemList(instances.allowed)}
+               </div>
+             )}
+             {instances.blocked && instances.blocked.length > 0 && (
+               <div className="col-md-6">
+                 <h5>{i18n.t("blocked_instances")}</h5>
+                 {this.itemList(instances.blocked)}
+               </div>
+             )}
+           </div>
+         ) : (
+           <></>
+         );
+       }
      }
    }
  
    render() {
-     let federated_instances = this.state.instancesRes?.federated_instances;
-     return federated_instances ? (
+     return (
        <div className="container-lg">
          <HtmlTags
            title={this.documentTitle}
            path={this.context.router.route.match.url}
          />
-         <div className="row">
-           <div className="col-md-6">
-             <h5>{i18n.t("linked_instances")}</h5>
-             {this.itemList(federated_instances.linked)}
-           </div>
-           {federated_instances.allowed &&
-             federated_instances.allowed.length > 0 && (
-               <div className="col-md-6">
-                 <h5>{i18n.t("allowed_instances")}</h5>
-                 {this.itemList(federated_instances.allowed)}
-               </div>
-             )}
-           {federated_instances.blocked &&
-             federated_instances.blocked.length > 0 && (
-               <div className="col-md-6">
-                 <h5>{i18n.t("blocked_instances")}</h5>
-                 {this.itemList(federated_instances.blocked)}
-               </div>
-             )}
-         </div>
+         {this.renderInstances()}
        </div>
-     ) : (
-       <></>
      );
    }
  
        <div>{i18n.t("none_found")}</div>
      );
    }
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       this.context.router.history.push("/");
-       this.setState({ loading: false });
-       return;
-     } else if (op == UserOperation.GetFederatedInstances) {
-       let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
-       this.setState({ loading: false, instancesRes: data });
-     }
-   }
  }
index cb4b37f4363d49f579fa72c295ebdbf6eea38481,d917f5f35ed675842e43169fa6da129b696335b6..48be10b6245849c0b511bc9ba9137599eb364566
@@@ -8,13 -8,11 +8,12 @@@ import 
    AdminPurgeCommunityView,
    AdminPurgePersonView,
    AdminPurgePostView,
-   CommunityModeratorView,
    GetCommunity,
    GetCommunityResponse,
    GetModlog,
    GetModlogResponse,
    GetPersonDetails,
 +  GetPersonDetailsResponse,
    ModAddCommunityView,
    ModAddView,
    ModBanFromCommunityView,
    ModTransferCommunityView,
    ModlogActionType,
    Person,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
  import moment from "moment";
- import { Subscription } from "rxjs";
  import { i18n } from "../i18next";
  import { InitialFetchRequest } from "../interfaces";
- import { WebSocketService } from "../services";
+ import { FirstLoadService } from "../services/FirstLoadService";
+ import { HttpService, RequestState } from "../services/HttpService";
  import {
    Choice,
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    amAdmin,
    amMod,
    debounce,
    getQueryParams,
    getQueryString,
    getUpdatedSearchId,
-   isBrowser,
    myAuth,
    personToChoice,
    setIsoData,
-   toast,
-   wsClient,
-   wsSubscribe,
  } from "../utils";
  import { HtmlTags } from "./common/html-tags";
  import { Icon, Spinner } from "./common/icon";
@@@ -84,13 -74,6 +76,13 @@@ type View 
    | AdminPurgePostView
    | AdminPurgeCommentView;
  
- interface ModlogData {
++type ModlogData = RouteDataResponse<{
 +  modlogResponse: GetModlogResponse;
 +  communityResponse?: GetCommunityResponse;
 +  modUserResponse?: GetPersonDetailsResponse;
 +  userResponse?: GetPersonDetailsResponse;
- }
++}>;
 +
  interface ModlogType {
    id: number;
    type_: ModlogActionType;
@@@ -108,10 -91,8 +100,8 @@@ const getModlogQueryParams = () =
    });
  
  interface ModlogState {
-   res?: GetModlogResponse;
-   communityMods?: CommunityModeratorView[];
-   communityName?: string;
-   loadingModlog: boolean;
+   res: RequestState<GetModlogResponse>;
+   communityRes: RequestState<GetCommunityResponse>;
    loadingModSearch: boolean;
    loadingUserSearch: boolean;
    modSearchOptions: Choice[];
@@@ -637,7 -618,7 +627,7 @@@ async function createNewOptions(
  
    if (text.length > 0) {
      newOptions.push(
-       ...(await fetchUsers(text)).users
+       ...(await fetchUsers(text))
          .slice(0, Number(fetchLimit))
          .map<Choice>(personToChoice)
      );
@@@ -650,11 -631,11 +640,11 @@@ export class Modlog extends Component
    RouteComponentProps<{ communityId?: string }>,
    ModlogState
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<ModlogData>(this.context);
-   private subscription?: Subscription;
  
    state: ModlogState = {
-     loadingModlog: true,
+     res: { state: "empty" },
+     communityRes: { state: "empty" },
      loadingModSearch: false,
      loadingUserSearch: false,
      userSearchOptions: [],
      this.handleUserChange = this.handleUserChange.bind(this);
      this.handleModChange = this.handleModChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
 -      const [res, communityRes, filteredModRes, filteredUserRes] =
 -        this.isoData.routeData;
 +      const {
-         modlogResponse,
-         communityResponse,
++        modlogResponse: res,
++        communityResponse: communityRes,
 +        modUserResponse,
 +        userResponse,
 +      } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
-         res: modlogResponse,
+         res,
 -        communityRes,
        };
  
-       // Getting the moderators
-       this.state = {
-         ...this.state,
-         communityMods: communityResponse?.moderators,
-       };
-       if (modUserResponse) {
 -      if (filteredModRes.state === "success") {
++      if (communityRes?.state === "success") {
          this.state = {
            ...this.state,
-           modSearchOptions: [personToChoice(modUserResponse.person_view)],
 -          modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
++          communityRes,
          };
        }
  
-       if (userResponse) {
 -      if (filteredUserRes.state === "success") {
++      if (modUserResponse?.state === "success") {
          this.state = {
            ...this.state,
-           userSearchOptions: [personToChoice(userResponse.person_view)],
 -          userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
++          modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
 +        };
 +      }
 +
-       this.state = { ...this.state, loadingModlog: false };
-     } else {
-       this.refetch();
-     }
-   }
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
++      if (userResponse?.state === "success") {
++        this.state = {
++          ...this.state,
++          userSearchOptions: [personToChoice(userResponse.data.person_view)],
+         };
+       }
      }
    }
  
    get combined() {
      const res = this.state.res;
-     const combined = res ? buildCombined(res) : [];
+     const combined = res.state == "success" ? buildCombined(res.data) : [];
  
      return (
        <tbody>
    }
  
    get amAdminOrMod(): boolean {
-     return amAdmin() || amMod(this.state.communityMods);
+     const amMod_ =
+       this.state.communityRes.state == "success" &&
+       amMod(this.state.communityRes.data.moderators);
+     return amAdmin() || amMod_;
    }
  
    modOrAdminText(person?: Person): string {
  
    render() {
      const {
-       communityName,
-       loadingModlog,
        loadingModSearch,
        loadingUserSearch,
        userSearchOptions,
        modSearchOptions,
      } = this.state;
-     const { actionType, page, modId, userId } = getModlogQueryParams();
+     const { actionType, modId, userId } = getModlogQueryParams();
  
      return (
        <div className="container-lg">
                #<strong>#</strong>#
              </T>
            </div>
-           <h5>
-             {communityName && (
-               <Link className="text-body" to={`/c/${communityName}`}>
-                 /c/{communityName}{" "}
+           {this.state.communityRes.state === "success" && (
+             <h5>
+               <Link
+                 className="text-body"
+                 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
+               >
+                 /c/{this.state.communityRes.data.community_view.community.name}{" "}
                </Link>
-             )}
-             <span>{i18n.t("modlog")}</span>
-           </h5>
+               <span>{i18n.t("modlog")}</span>
+             </h5>
+           )}
            <div className="form-row">
              <select
                value={actionType}
                />
              )}
            </div>
-           <div className="table-responsive">
-             {loadingModlog ? (
-               <h5>
-                 <Spinner large />
-               </h5>
-             ) : (
-               <table id="modlog_table" className="table table-sm table-hover">
-                 <thead className="pointer">
-                   <tr>
-                     <th> {i18n.t("time")}</th>
-                     <th>{i18n.t("mod")}</th>
-                     <th>{i18n.t("action")}</th>
-                   </tr>
-                 </thead>
-                 {this.combined}
-               </table>
-             )}
-             <Paginator page={page} onChange={this.handlePageChange} />
-           </div>
+           {this.renderModlogTable()}
          </div>
        </div>
      );
    }
  
+   renderModlogTable() {
+     switch (this.state.res.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const page = getModlogQueryParams().page;
+         return (
+           <div className="table-responsive">
+             <table id="modlog_table" className="table table-sm table-hover">
+               <thead className="pointer">
+                 <tr>
+                   <th> {i18n.t("time")}</th>
+                   <th>{i18n.t("mod")}</th>
+                   <th>{i18n.t("action")}</th>
+                 </tr>
+               </thead>
+               {this.combined}
+             </table>
+             <Paginator page={page} onChange={this.handlePageChange} />
+           </div>
+         );
+       }
+     }
+   }
    handleFilterActionChange(i: Modlog, event: any) {
      i.updateUrl({
        actionType: event.target.value as ModlogActionType,
      });
    });
  
-   updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
+   async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
      const {
        page: urlPage,
        actionType: urlActionType,
        )}`
      );
  
-     this.setState({
-       loadingModlog: true,
-       res: undefined,
-     });
-     this.refetch();
+     await this.refetch();
    }
  
-   refetch() {
-     const auth = myAuth(false);
+   async refetch() {
+     const auth = myAuth();
      const { actionType, page, modId, userId } = getModlogQueryParams();
      const { communityId: urlCommunityId } = this.props.match.params;
      const communityId = getIdFromString(urlCommunityId);
  
-     const modlogForm: GetModlog = {
-       community_id: communityId,
-       page,
-       limit: fetchLimit,
-       type_: actionType,
-       other_person_id: userId ?? undefined,
-       mod_person_id: !this.isoData.site_res.site_view.local_site
-         .hide_modlog_mod_names
-         ? modId ?? undefined
-         : undefined,
-       auth,
-     };
-     WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
-     if (communityId) {
-       const communityForm: GetCommunity = {
-         id: communityId,
+     this.setState({ res: { state: "loading" } });
+     this.setState({
+       res: await HttpService.client.getModlog({
+         community_id: communityId,
+         page,
+         limit: fetchLimit,
+         type_: actionType,
+         other_person_id: userId ?? undefined,
+         mod_person_id: !this.isoData.site_res.site_view.local_site
+           .hide_modlog_mod_names
+           ? modId ?? undefined
+           : undefined,
          auth,
-       };
+       }),
+     });
  
-       WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
+     if (communityId) {
+       this.setState({ communityRes: { state: "loading" } });
+       this.setState({
+         communityRes: await HttpService.client.getCommunity({
+           id: communityId,
+           auth,
+         }),
+       });
      }
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      path,
      query: { modId: urlModId, page, userId: urlUserId, actionType },
      auth,
      site,
-   }: InitialFetchRequest<
-     QueryParams<ModlogProps>
-   >): WithPromiseKeys<ModlogData> {
 -  }: 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,
      };
  
-     let communityResponse: Promise<GetCommunityResponse> | undefined =
 -    promises.push(client.getModlog(modlogForm));
++    let communityResponse: RequestState<GetCommunityResponse> | undefined =
 +      undefined;
  
      if (communityId) {
        const communityForm: GetCommunity = {
          id: communityId,
          auth,
        };
 -      promises.push(client.getCommunity(communityForm));
 -    } else {
 -      promises.push(Promise.resolve({ state: "empty" }));
 +
-       communityResponse = client.getCommunity(communityForm);
++      communityResponse = await client.getCommunity(communityForm);
      }
  
-     let modUserResponse: Promise<GetPersonDetailsResponse> | undefined =
++    let modUserResponse: RequestState<GetPersonDetailsResponse> | undefined =
 +      undefined;
 +
      if (modId) {
        const getPersonForm: GetPersonDetails = {
          person_id: modId,
          auth,
        };
  
-       modUserResponse = client.getPersonDetails(getPersonForm);
 -      promises.push(client.getPersonDetails(getPersonForm));
 -    } else {
 -      promises.push(Promise.resolve({ state: "empty" }));
++      modUserResponse = await client.getPersonDetails(getPersonForm);
      }
  
-     let userResponse: Promise<GetPersonDetailsResponse> | undefined = undefined;
++    let userResponse: RequestState<GetPersonDetailsResponse> | undefined =
++      undefined;
 +
      if (userId) {
        const getPersonForm: GetPersonDetails = {
          person_id: userId,
          auth,
        };
  
-       userResponse = client.getPersonDetails(getPersonForm);
 -      promises.push(client.getPersonDetails(getPersonForm));
 -    } else {
 -      promises.push(Promise.resolve({ state: "empty" }));
++      userResponse = await client.getPersonDetails(getPersonForm);
      }
  
 -    return promises;
 +    return {
-       modlogResponse: client.getModlog(modlogForm),
++      modlogResponse: await client.getModlog(modlogForm),
 +      communityResponse,
 +      modUserResponse,
 +      userResponse,
 +    };
    }
-   parseMessage(msg: any) {
-     const op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-     } else {
-       switch (op) {
-         case UserOperation.GetModlog: {
-           const res = wsJsonToRes<GetModlogResponse>(msg);
-           window.scrollTo(0, 0);
-           this.setState({ res, loadingModlog: false });
-           break;
-         }
-         case UserOperation.GetCommunity: {
-           const {
-             moderators,
-             community_view: {
-               community: { name },
-             },
-           } = wsJsonToRes<GetCommunityResponse>(msg);
-           this.setState({
-             communityMods: moderators,
-             communityName: name,
-           });
-           break;
-         }
-       }
-     }
-   }
  }
index 6393fc62083bd8a4d3e42d485ddeb6ee7b54a36b,731667c0ddef6108f8fe1a307a4b56fdc66d5338..b0550f2241a8fae938d61f5b3cf60b7fe5bce0d0
@@@ -1,50 -1,72 +1,70 @@@
  import { Component, linkEvent } from "inferno";
  import {
-   BlockPersonResponse,
+   AddAdmin,
+   AddModToCommunity,
+   BanFromCommunity,
+   BanFromCommunityResponse,
+   BanPerson,
+   BanPersonResponse,
+   BlockPerson,
+   CommentId,
    CommentReplyResponse,
    CommentReplyView,
    CommentReportResponse,
    CommentResponse,
    CommentSortType,
    CommentView,
-   GetPersonMentions,
+   CreateComment,
+   CreateCommentLike,
+   CreateCommentReport,
+   CreatePrivateMessage,
+   CreatePrivateMessageReport,
+   DeleteComment,
+   DeletePrivateMessage,
+   DistinguishComment,
+   EditComment,
+   EditPrivateMessage,
 -  GetPersonMentions,
    GetPersonMentionsResponse,
--  GetPrivateMessages,
--  GetReplies,
    GetRepliesResponse,
    GetSiteResponse,
+   MarkCommentReplyAsRead,
+   MarkPersonMentionAsRead,
+   MarkPrivateMessageAsRead,
    PersonMentionResponse,
    PersonMentionView,
-   PostReportResponse,
    PrivateMessageReportResponse,
    PrivateMessageResponse,
    PrivateMessageView,
    PrivateMessagesResponse,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   PurgeComment,
+   PurgeItemResponse,
+   PurgePerson,
+   PurgePost,
+   RemoveComment,
+   SaveComment,
+   TransferCommunity,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { CommentViewType, InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
-   WithPromiseKeys,
++  RouteDataResponse,
    commentsToFlatNodes,
-   createCommentLikeRes,
-   editCommentRes,
+   editCommentReply,
+   editMention,
+   editPrivateMessage,
+   editWith,
    enableDownvotes,
    fetchLimit,
-   isBrowser,
+   getCommentParentId,
    myAuth,
+   myAuthRequired,
    relTags,
-   saveCommentRes,
    setIsoData,
-   setupTippy,
    toast,
    updatePersonBlock,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { CommentNodes } from "../comment/comment-nodes";
  import { CommentSortSelect } from "../common/comment-sort-select";
@@@ -70,13 -92,6 +90,13 @@@ enum ReplyEnum 
    Mention,
    Message,
  }
- interface InboxData {
 +
- }
++type InboxData = RouteDataResponse<{
 +  repliesResponse: GetRepliesResponse;
 +  personMentionsResponse: GetPersonMentionsResponse;
 +  privateMessagesResponse: PrivateMessagesResponse;
++}>;
 +
  type ReplyType = {
    id: number;
    type_: ReplyEnum;
  interface InboxState {
    unreadOrAll: UnreadOrAll;
    messageType: MessageType;
-   replies: CommentReplyView[];
-   mentions: PersonMentionView[];
-   messages: PrivateMessageView[];
-   combined: ReplyType[];
+   repliesRes: RequestState<GetRepliesResponse>;
+   mentionsRes: RequestState<GetPersonMentionsResponse>;
+   messagesRes: RequestState<PrivateMessagesResponse>;
+   markAllAsReadRes: RequestState<GetRepliesResponse>;
    sort: CommentSortType;
    page: number;
    siteRes: GetSiteResponse;
-   loading: boolean;
+   finished: Map<CommentId, boolean | undefined>;
+   isIsomorphic: boolean;
  }
  
  export class Inbox extends Component<any, InboxState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<InboxData>(this.context);
-   private subscription?: Subscription;
    state: InboxState = {
      unreadOrAll: UnreadOrAll.Unread,
      messageType: MessageType.All,
-     replies: [],
-     mentions: [],
-     messages: [],
-     combined: [],
      sort: "New",
      page: 1,
      siteRes: this.isoData.site_res,
-     loading: true,
+     repliesRes: { state: "empty" },
+     mentionsRes: { state: "empty" },
+     messagesRes: { state: "empty" },
+     markAllAsReadRes: { state: "empty" },
+     finished: new Map(),
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      this.handleSortChange = this.handleSortChange.bind(this);
      this.handlePageChange = this.handlePageChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleCreateComment = this.handleCreateComment.bind(this);
+     this.handleEditComment = this.handleEditComment.bind(this);
+     this.handleSaveComment = this.handleSaveComment.bind(this);
+     this.handleBlockPerson = this.handleBlockPerson.bind(this);
+     this.handleDeleteComment = this.handleDeleteComment.bind(this);
+     this.handleRemoveComment = this.handleRemoveComment.bind(this);
+     this.handleCommentVote = this.handleCommentVote.bind(this);
+     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+     this.handleAddAdmin = this.handleAddAdmin.bind(this);
+     this.handlePurgePerson = this.handlePurgePerson.bind(this);
+     this.handlePurgeComment = this.handlePurgeComment.bind(this);
+     this.handleCommentReport = this.handleCommentReport.bind(this);
+     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+     this.handleBanPerson = this.handleBanPerson.bind(this);
+     this.handleDeleteMessage = this.handleDeleteMessage.bind(this);
+     this.handleMarkMessageAsRead = this.handleMarkMessageAsRead.bind(this);
+     this.handleMessageReport = this.handleMessageReport.bind(this);
+     this.handleCreateMessage = this.handleCreateMessage.bind(this);
+     this.handleEditMessage = this.handleEditMessage.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
 -      const [repliesRes, mentionsRes, messagesRes] = this.isoData.routeData;
 +      const {
-         personMentionsResponse,
-         privateMessagesResponse,
-         repliesResponse,
++        personMentionsResponse: mentionsRes,
++        privateMessagesResponse: messagesRes,
++        repliesResponse: repliesRes,
 +      } = this.isoData.routeData;
  
        this.state = {
          ...this.state,
-         replies: repliesResponse.replies ?? [],
-         mentions: personMentionsResponse.mentions ?? [],
-         messages: privateMessagesResponse.private_messages ?? [],
-         loading: false,
+         repliesRes,
+         mentionsRes,
+         messagesRes,
+         isIsomorphic: true,
        };
-       this.state = { ...this.state, combined: this.buildCombined() };
-     } else {
-       this.refetch();
      }
    }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.refetch();
      }
    }
  
    get documentTitle(): string {
-     let mui = UserService.Instance.myUserInfo;
+     const mui = UserService.Instance.myUserInfo;
      return mui
        ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
            this.state.siteRes.site_view.site.name
        : "";
    }
  
+   get hasUnreads(): boolean {
+     if (this.state.unreadOrAll == UnreadOrAll.Unread) {
+       const { repliesRes, mentionsRes, messagesRes } = this.state;
+       const replyCount =
+         repliesRes.state == "success" ? repliesRes.data.replies.length : 0;
+       const mentionCount =
+         mentionsRes.state == "success" ? mentionsRes.data.mentions.length : 0;
+       const messageCount =
+         messagesRes.state == "success"
+           ? messagesRes.data.private_messages.length
+           : 0;
+       return replyCount + mentionCount + messageCount > 0;
+     } else {
+       return false;
+     }
+   }
    render() {
-     let auth = myAuth();
-     let inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
+     const auth = myAuth();
+     const inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
      return (
        <div className="container-lg">
-         {this.state.loading ? (
-           <h5>
-             <Spinner large />
-           </h5>
-         ) : (
-           <div className="row">
-             <div className="col-12">
-               <HtmlTags
-                 title={this.documentTitle}
-                 path={this.context.router.route.match.url}
-               />
-               <h5 className="mb-2">
-                 {i18n.t("inbox")}
-                 {inboxRss && (
-                   <small>
-                     <a href={inboxRss} title="RSS" rel={relTags}>
-                       <Icon icon="rss" classes="ml-2 text-muted small" />
-                     </a>
-                     <link
-                       rel="alternate"
-                       type="application/atom+xml"
-                       href={inboxRss}
-                     />
-                   </small>
+         <div className="row">
+           <div className="col-12">
+             <HtmlTags
+               title={this.documentTitle}
+               path={this.context.router.route.match.url}
+             />
+             <h5 className="mb-2">
+               {i18n.t("inbox")}
+               {inboxRss && (
+                 <small>
+                   <a href={inboxRss} title="RSS" rel={relTags}>
+                     <Icon icon="rss" classes="ml-2 text-muted small" />
+                   </a>
+                   <link
+                     rel="alternate"
+                     type="application/atom+xml"
+                     href={inboxRss}
+                   />
+                 </small>
+               )}
+             </h5>
+             {this.hasUnreads && (
+               <button
+                 className="btn btn-secondary mb-2"
+                 onClick={linkEvent(this, this.handleMarkAllAsRead)}
+               >
+                 {this.state.markAllAsReadRes.state == "loading" ? (
+                   <Spinner />
+                 ) : (
+                   i18n.t("mark_all_as_read")
                  )}
-               </h5>
-               {this.state.replies.length +
-                 this.state.mentions.length +
-                 this.state.messages.length >
-                 0 &&
-                 this.state.unreadOrAll == UnreadOrAll.Unread && (
-                   <button
-                     className="btn btn-secondary mb-2"
-                     onClick={linkEvent(this, this.markAllAsRead)}
-                   >
-                     {i18n.t("mark_all_as_read")}
-                   </button>
-                 )}
-               {this.selects()}
-               {this.state.messageType == MessageType.All && this.all()}
-               {this.state.messageType == MessageType.Replies && this.replies()}
-               {this.state.messageType == MessageType.Mentions &&
-                 this.mentions()}
-               {this.state.messageType == MessageType.Messages &&
-                 this.messages()}
-               <Paginator
-                 page={this.state.page}
-                 onChange={this.handlePageChange}
-               />
-             </div>
+               </button>
+             )}
+             {this.selects()}
+             {this.section}
+             <Paginator
+               page={this.state.page}
+               onChange={this.handlePageChange}
+             />
            </div>
-         )}
+         </div>
        </div>
      );
    }
  
+   get section() {
+     switch (this.state.messageType) {
+       case MessageType.All: {
+         return this.all();
+       }
+       case MessageType.Replies: {
+         return this.replies();
+       }
+       case MessageType.Mentions: {
+         return this.mentions();
+       }
+       case MessageType.Messages: {
+         return this.messages();
+       }
+       default: {
+         return null;
+       }
+     }
+   }
    unreadOrAllRadios() {
      return (
        <div className="btn-group btn-group-toggle flex-wrap mb-2">
    }
  
    buildCombined(): ReplyType[] {
-     let replies: ReplyType[] = this.state.replies.map(r =>
-       this.replyToReplyType(r)
-     );
-     let mentions: ReplyType[] = this.state.mentions.map(r =>
-       this.mentionToReplyType(r)
-     );
-     let messages: ReplyType[] = this.state.messages.map(r =>
-       this.messageToReplyType(r)
-     );
+     const replies: ReplyType[] =
+       this.state.repliesRes.state == "success"
+         ? this.state.repliesRes.data.replies.map(this.replyToReplyType)
+         : [];
+     const mentions: ReplyType[] =
+       this.state.mentionsRes.state == "success"
+         ? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType)
+         : [];
+     const messages: ReplyType[] =
+       this.state.messagesRes.state == "success"
+         ? this.state.messagesRes.data.private_messages.map(
+             this.messageToReplyType
+           )
+         : [];
  
      return [...replies, ...mentions, ...messages].sort((a, b) =>
        b.published.localeCompare(a.published)
                { comment_view: i.view as CommentView, children: [], depth: 0 },
              ]}
              viewType={CommentViewType.Flat}
+             finished={this.state.finished}
              noIndent
              markable
              showCommunity
              enableDownvotes={enableDownvotes(this.state.siteRes)}
              allLanguages={this.state.siteRes.all_languages}
              siteLanguages={this.state.siteRes.discussion_languages}
+             onSaveComment={this.handleSaveComment}
+             onBlockPerson={this.handleBlockPerson}
+             onDeleteComment={this.handleDeleteComment}
+             onRemoveComment={this.handleRemoveComment}
+             onCommentVote={this.handleCommentVote}
+             onCommentReport={this.handleCommentReport}
+             onDistinguishComment={this.handleDistinguishComment}
+             onAddModToCommunity={this.handleAddModToCommunity}
+             onAddAdmin={this.handleAddAdmin}
+             onTransferCommunity={this.handleTransferCommunity}
+             onPurgeComment={this.handlePurgeComment}
+             onPurgePerson={this.handlePurgePerson}
+             onCommentReplyRead={this.handleCommentReplyRead}
+             onPersonMentionRead={this.handlePersonMentionRead}
+             onBanPersonFromCommunity={this.handleBanFromCommunity}
+             onBanPerson={this.handleBanPerson}
+             onCreateComment={this.handleCreateComment}
+             onEditComment={this.handleEditComment}
            />
          );
        case ReplyEnum.Mention:
                  depth: 0,
                },
              ]}
+             finished={this.state.finished}
              viewType={CommentViewType.Flat}
              noIndent
              markable
              enableDownvotes={enableDownvotes(this.state.siteRes)}
              allLanguages={this.state.siteRes.all_languages}
              siteLanguages={this.state.siteRes.discussion_languages}
+             onSaveComment={this.handleSaveComment}
+             onBlockPerson={this.handleBlockPerson}
+             onDeleteComment={this.handleDeleteComment}
+             onRemoveComment={this.handleRemoveComment}
+             onCommentVote={this.handleCommentVote}
+             onCommentReport={this.handleCommentReport}
+             onDistinguishComment={this.handleDistinguishComment}
+             onAddModToCommunity={this.handleAddModToCommunity}
+             onAddAdmin={this.handleAddAdmin}
+             onTransferCommunity={this.handleTransferCommunity}
+             onPurgeComment={this.handlePurgeComment}
+             onPurgePerson={this.handlePurgePerson}
+             onCommentReplyRead={this.handleCommentReplyRead}
+             onPersonMentionRead={this.handlePersonMentionRead}
+             onBanPersonFromCommunity={this.handleBanFromCommunity}
+             onBanPerson={this.handleBanPerson}
+             onCreateComment={this.handleCreateComment}
+             onEditComment={this.handleEditComment}
            />
          );
        case ReplyEnum.Message:
            <PrivateMessage
              key={i.id}
              private_message_view={i.view as PrivateMessageView}
+             onDelete={this.handleDeleteMessage}
+             onMarkRead={this.handleMarkMessageAsRead}
+             onReport={this.handleMessageReport}
+             onCreate={this.handleCreateMessage}
+             onEdit={this.handleEditMessage}
            />
          );
        default:
    }
  
    all() {
-     return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
+     if (
+       this.state.repliesRes.state == "loading" ||
+       this.state.mentionsRes.state == "loading" ||
+       this.state.messagesRes.state == "loading"
+     ) {
+       return (
+         <h5>
+           <Spinner large />
+         </h5>
+       );
+     } else {
+       return (
+         <div>{this.buildCombined().map(r => this.renderReplyType(r))}</div>
+       );
+     }
    }
  
    replies() {
-     return (
-       <div>
-         <CommentNodes
-           nodes={commentsToFlatNodes(this.state.replies)}
-           viewType={CommentViewType.Flat}
-           noIndent
-           markable
-           showCommunity
-           showContext
-           enableDownvotes={enableDownvotes(this.state.siteRes)}
-           allLanguages={this.state.siteRes.all_languages}
-           siteLanguages={this.state.siteRes.discussion_languages}
-         />
-       </div>
-     );
+     switch (this.state.repliesRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const replies = this.state.repliesRes.data.replies;
+         return (
+           <div>
+             <CommentNodes
+               nodes={commentsToFlatNodes(replies)}
+               viewType={CommentViewType.Flat}
+               finished={this.state.finished}
+               noIndent
+               markable
+               showCommunity
+               showContext
+               enableDownvotes={enableDownvotes(this.state.siteRes)}
+               allLanguages={this.state.siteRes.all_languages}
+               siteLanguages={this.state.siteRes.discussion_languages}
+               onSaveComment={this.handleSaveComment}
+               onBlockPerson={this.handleBlockPerson}
+               onDeleteComment={this.handleDeleteComment}
+               onRemoveComment={this.handleRemoveComment}
+               onCommentVote={this.handleCommentVote}
+               onCommentReport={this.handleCommentReport}
+               onDistinguishComment={this.handleDistinguishComment}
+               onAddModToCommunity={this.handleAddModToCommunity}
+               onAddAdmin={this.handleAddAdmin}
+               onTransferCommunity={this.handleTransferCommunity}
+               onPurgeComment={this.handlePurgeComment}
+               onPurgePerson={this.handlePurgePerson}
+               onCommentReplyRead={this.handleCommentReplyRead}
+               onPersonMentionRead={this.handlePersonMentionRead}
+               onBanPersonFromCommunity={this.handleBanFromCommunity}
+               onBanPerson={this.handleBanPerson}
+               onCreateComment={this.handleCreateComment}
+               onEditComment={this.handleEditComment}
+             />
+           </div>
+         );
+       }
+     }
    }
  
    mentions() {
-     return (
-       <div>
-         {this.state.mentions.map(umv => (
-           <CommentNodes
-             key={umv.person_mention.id}
-             nodes={[{ comment_view: umv, children: [], depth: 0 }]}
-             viewType={CommentViewType.Flat}
-             noIndent
-             markable
-             showCommunity
-             showContext
-             enableDownvotes={enableDownvotes(this.state.siteRes)}
-             allLanguages={this.state.siteRes.all_languages}
-             siteLanguages={this.state.siteRes.discussion_languages}
-           />
-         ))}
-       </div>
-     );
+     switch (this.state.mentionsRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const mentions = this.state.mentionsRes.data.mentions;
+         return (
+           <div>
+             {mentions.map(umv => (
+               <CommentNodes
+                 key={umv.person_mention.id}
+                 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
+                 viewType={CommentViewType.Flat}
+                 finished={this.state.finished}
+                 noIndent
+                 markable
+                 showCommunity
+                 showContext
+                 enableDownvotes={enableDownvotes(this.state.siteRes)}
+                 allLanguages={this.state.siteRes.all_languages}
+                 siteLanguages={this.state.siteRes.discussion_languages}
+                 onSaveComment={this.handleSaveComment}
+                 onBlockPerson={this.handleBlockPerson}
+                 onDeleteComment={this.handleDeleteComment}
+                 onRemoveComment={this.handleRemoveComment}
+                 onCommentVote={this.handleCommentVote}
+                 onCommentReport={this.handleCommentReport}
+                 onDistinguishComment={this.handleDistinguishComment}
+                 onAddModToCommunity={this.handleAddModToCommunity}
+                 onAddAdmin={this.handleAddAdmin}
+                 onTransferCommunity={this.handleTransferCommunity}
+                 onPurgeComment={this.handlePurgeComment}
+                 onPurgePerson={this.handlePurgePerson}
+                 onCommentReplyRead={this.handleCommentReplyRead}
+                 onPersonMentionRead={this.handlePersonMentionRead}
+                 onBanPersonFromCommunity={this.handleBanFromCommunity}
+                 onBanPerson={this.handleBanPerson}
+                 onCreateComment={this.handleCreateComment}
+                 onEditComment={this.handleEditComment}
+               />
+             ))}
+           </div>
+         );
+       }
+     }
    }
  
    messages() {
-     return (
-       <div>
-         {this.state.messages.map(pmv => (
-           <PrivateMessage
-             key={pmv.private_message.id}
-             private_message_view={pmv}
-           />
-         ))}
-       </div>
-     );
+     switch (this.state.messagesRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const messages = this.state.messagesRes.data.private_messages;
+         return (
+           <div>
+             {messages.map(pmv => (
+               <PrivateMessage
+                 key={pmv.private_message.id}
+                 private_message_view={pmv}
+                 onDelete={this.handleDeleteMessage}
+                 onMarkRead={this.handleMarkMessageAsRead}
+                 onReport={this.handleMessageReport}
+                 onCreate={this.handleCreateMessage}
+                 onEdit={this.handleEditMessage}
+               />
+             ))}
+           </div>
+         );
+       }
+     }
    }
  
-   handlePageChange(page: number) {
+   async handlePageChange(page: number) {
      this.setState({ page });
-     this.refetch();
+     await this.refetch();
    }
  
-   handleUnreadOrAllChange(i: Inbox, event: any) {
+   async handleUnreadOrAllChange(i: Inbox, event: any) {
      i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
-     i.refetch();
+     await i.refetch();
    }
  
-   handleMessageTypeChange(i: Inbox, event: any) {
+   async handleMessageTypeChange(i: Inbox, event: any) {
      i.setState({ messageType: Number(event.target.value), page: 1 });
-     i.refetch();
+     await i.refetch();
    }
  
--  static fetchInitialData({
-     auth,
++  static async fetchInitialData({
      client,
-   }: InitialFetchRequest): WithPromiseKeys<InboxData> {
+     auth,
 -  }: InitialFetchRequest): Promise<any>[] {
 -    const promises: Promise<RequestState<any>>[] = [];
 -
++  }: InitialFetchRequest): Promise<InboxData> {
      const sort: CommentSortType = "New";
  
-     // It can be /u/me, or /username/1
-     const repliesForm: GetReplies = {
-       sort,
-       unread_only: true,
-       page: 1,
-       limit: fetchLimit,
-       auth: auth as string,
-     };
 -    if (auth) {
 -      // It can be /u/me, or /username/1
 -      const repliesForm: GetReplies = {
 -        sort,
 -        unread_only: true,
 -        page: 1,
 -        limit: fetchLimit,
 -        auth,
 -      };
 -      promises.push(client.getReplies(repliesForm));
--
-     const personMentionsForm: GetPersonMentions = {
-       sort,
-       unread_only: true,
-       page: 1,
-       limit: fetchLimit,
-       auth: auth as string,
-     };
 -      const personMentionsForm: GetPersonMentions = {
 -        sort,
 -        unread_only: true,
 -        page: 1,
 -        limit: fetchLimit,
 -        auth,
 -      };
 -      promises.push(client.getPersonMentions(personMentionsForm));
--
-     const privateMessagesForm: GetPrivateMessages = {
-       unread_only: true,
-       page: 1,
-       limit: fetchLimit,
-       auth: auth as string,
-     };
 -      const privateMessagesForm: GetPrivateMessages = {
 -        unread_only: true,
 -        page: 1,
 -        limit: fetchLimit,
 -        auth,
 -      };
 -      promises.push(client.getPrivateMessages(privateMessagesForm));
 -    } else {
 -      promises.push(
 -        Promise.resolve({ state: "empty" }),
 -        Promise.resolve({ state: "empty" }),
 -        Promise.resolve({ state: "empty" })
 -      );
 -    }
--
 -    return promises;
 +    return {
-       privateMessagesResponse: client.getPrivateMessages(privateMessagesForm),
-       personMentionsResponse: client.getPersonMentions(personMentionsForm),
-       repliesResponse: client.getReplies(repliesForm),
++      personMentionsResponse: auth
++        ? await client.getPersonMentions({
++            sort,
++            unread_only: true,
++            page: 1,
++            limit: fetchLimit,
++            auth,
++          })
++        : { state: "empty" },
++      privateMessagesResponse: auth
++        ? await client.getPrivateMessages({
++            unread_only: true,
++            page: 1,
++            limit: fetchLimit,
++            auth,
++          })
++        : { state: "empty" },
++      repliesResponse: auth
++        ? await client.getReplies({
++            sort,
++            unread_only: true,
++            page: 1,
++            limit: fetchLimit,
++            auth,
++          })
++        : { state: "empty" },
 +    };
    }
  
-   refetch() {
-     const { sort, page, unreadOrAll } = this.state;
-     const unread_only = unreadOrAll === UnreadOrAll.Unread;
+   async refetch() {
+     const sort = this.state.sort;
+     const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+     const page = this.state.page;
      const limit = fetchLimit;
-     const auth = myAuth();
+     const auth = myAuthRequired();
  
-     if (auth) {
-       const repliesForm: GetReplies = {
+     this.setState({ repliesRes: { state: "loading" } });
+     this.setState({
+       repliesRes: await HttpService.client.getReplies({
          sort,
          unread_only,
          page,
          limit,
          auth,
-       };
-       WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
+       }),
+     });
  
-       const personMentionsForm: GetPersonMentions = {
+     this.setState({ mentionsRes: { state: "loading" } });
+     this.setState({
+       mentionsRes: await HttpService.client.getPersonMentions({
          sort,
          unread_only,
          page,
          limit,
          auth,
-       };
-       WebSocketService.Instance.send(
-         wsClient.getPersonMentions(personMentionsForm)
-       );
+       }),
+     });
  
-       const privateMessagesForm: GetPrivateMessages = {
+     this.setState({ messagesRes: { state: "loading" } });
+     this.setState({
+       messagesRes: await HttpService.client.getPrivateMessages({
          unread_only,
          page,
          limit,
          auth,
-       };
-       WebSocketService.Instance.send(
-         wsClient.getPrivateMessages(privateMessagesForm)
-       );
-     }
+       }),
+     });
    }
  
-   handleSortChange(val: CommentSortType) {
+   async handleSortChange(val: CommentSortType) {
      this.setState({ sort: val, page: 1 });
-     this.refetch();
+     await this.refetch();
    }
  
-   markAllAsRead(i: Inbox) {
-     let auth = myAuth();
-     if (auth) {
-       WebSocketService.Instance.send(
-         wsClient.markAllAsRead({
-           auth,
-         })
-       );
-       i.setState({ replies: [], mentions: [], messages: [] });
-       i.setState({ combined: i.buildCombined() });
-       UserService.Instance.unreadInboxCountSub.next(0);
-       window.scrollTo(0, 0);
-       i.setState(i.state);
+   async handleMarkAllAsRead(i: Inbox) {
+     i.setState({ markAllAsReadRes: { state: "loading" } });
+     i.setState({
+       markAllAsReadRes: await HttpService.client.markAllAsRead({
+         auth: myAuthRequired(),
+       }),
+     });
+     if (i.state.markAllAsReadRes.state == "success") {
+       i.setState({
+         repliesRes: { state: "empty" },
+         mentionsRes: { state: "empty" },
+         messagesRes: { state: "empty" },
+       });
      }
    }
  
-   sendUnreadCount(read: boolean) {
-     let urcs = UserService.Instance.unreadInboxCountSub;
-     if (read) {
-       urcs.next(urcs.getValue() - 1);
-     } else {
-       urcs.next(urcs.getValue() + 1);
+   async handleAddModToCommunity(form: AddModToCommunity) {
+     // TODO not sure what to do here
+     HttpService.client.addModToCommunity(form);
+   }
+   async handlePurgePerson(form: PurgePerson) {
+     const purgePersonRes = await HttpService.client.purgePerson(form);
+     this.purgeItem(purgePersonRes);
+   }
+   async handlePurgeComment(form: PurgeComment) {
+     const purgeCommentRes = await HttpService.client.purgeComment(form);
+     this.purgeItem(purgeCommentRes);
+   }
+   async handlePurgePost(form: PurgePost) {
+     const purgeRes = await HttpService.client.purgePost(form);
+     this.purgeItem(purgeRes);
+   }
+   async handleBlockPerson(form: BlockPerson) {
+     const blockPersonRes = await HttpService.client.blockPerson(form);
+     if (blockPersonRes.state == "success") {
+       updatePersonBlock(blockPersonRes.data);
      }
    }
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       return;
-     } else if (msg.reconnect) {
-       this.refetch();
-     } else if (op == UserOperation.GetReplies) {
-       let data = wsJsonToRes<GetRepliesResponse>(msg);
-       this.setState({ replies: data.replies });
-       this.setState({ combined: this.buildCombined(), loading: false });
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.GetPersonMentions) {
-       let data = wsJsonToRes<GetPersonMentionsResponse>(msg);
-       this.setState({ mentions: data.mentions });
-       this.setState({ combined: this.buildCombined() });
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.GetPrivateMessages) {
-       let data = wsJsonToRes<PrivateMessagesResponse>(msg);
-       this.setState({ messages: data.private_messages });
-       this.setState({ combined: this.buildCombined() });
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.EditPrivateMessage) {
-       let data = wsJsonToRes<PrivateMessageResponse>(msg);
-       let found = this.state.messages.find(
-         m =>
-           m.private_message.id === data.private_message_view.private_message.id
-       );
-       if (found) {
-         let combinedView = this.state.combined.find(
-           i => i.id == data.private_message_view.private_message.id
-         )?.view as PrivateMessageView | undefined;
-         if (combinedView) {
-           found.private_message.content = combinedView.private_message.content =
-             data.private_message_view.private_message.content;
-           found.private_message.updated = combinedView.private_message.updated =
-             data.private_message_view.private_message.updated;
-         }
+   async handleCreateComment(form: CreateComment) {
+     const res = await HttpService.client.createComment(form);
+     if (res.state === "success") {
+       toast(i18n.t("reply_sent"));
+       this.findAndUpdateComment(res);
+     }
+     return res;
+   }
+   async handleEditComment(form: EditComment) {
+     const res = await HttpService.client.editComment(form);
+     if (res.state === "success") {
+       toast(i18n.t("edit"));
+       this.findAndUpdateComment(res);
+     } else if (res.state === "failed") {
+       toast(res.msg, "danger");
+     }
+     return res;
+   }
+   async handleDeleteComment(form: DeleteComment) {
+     const res = await HttpService.client.deleteComment(form);
+     if (res.state == "success") {
+       toast(i18n.t("deleted"));
+       this.findAndUpdateComment(res);
+     }
+   }
+   async handleRemoveComment(form: RemoveComment) {
+     const res = await HttpService.client.removeComment(form);
+     if (res.state == "success") {
+       toast(i18n.t("remove_comment"));
+       this.findAndUpdateComment(res);
+     }
+   }
+   async handleSaveComment(form: SaveComment) {
+     const res = await HttpService.client.saveComment(form);
+     this.findAndUpdateComment(res);
+   }
+   async handleCommentVote(form: CreateCommentLike) {
+     const res = await HttpService.client.likeComment(form);
+     this.findAndUpdateComment(res);
+   }
+   async handleCommentReport(form: CreateCommentReport) {
+     const reportRes = await HttpService.client.createCommentReport(form);
+     this.reportToast(reportRes);
+   }
+   async handleDistinguishComment(form: DistinguishComment) {
+     const res = await HttpService.client.distinguishComment(form);
+     this.findAndUpdateComment(res);
+   }
+   async handleAddAdmin(form: AddAdmin) {
+     const addAdminRes = await HttpService.client.addAdmin(form);
+     if (addAdminRes.state === "success") {
+       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+     }
+   }
+   async handleTransferCommunity(form: TransferCommunity) {
+     await HttpService.client.transferCommunity(form);
+     toast(i18n.t("transfer_community"));
+   }
+   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+     const res = await HttpService.client.markCommentReplyAsRead(form);
+     this.findAndUpdateCommentReply(res);
+   }
+   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+     const res = await HttpService.client.markPersonMentionAsRead(form);
+     this.findAndUpdateMention(res);
+   }
+   async handleBanFromCommunity(form: BanFromCommunity) {
+     const banRes = await HttpService.client.banFromCommunity(form);
+     this.updateBanFromCommunity(banRes);
+   }
+   async handleBanPerson(form: BanPerson) {
+     const banRes = await HttpService.client.banPerson(form);
+     this.updateBan(banRes);
+   }
+   async handleDeleteMessage(form: DeletePrivateMessage) {
+     const res = await HttpService.client.deletePrivateMessage(form);
+     this.findAndUpdateMessage(res);
+   }
+   async handleEditMessage(form: EditPrivateMessage) {
+     const res = await HttpService.client.editPrivateMessage(form);
+     this.findAndUpdateMessage(res);
+   }
+   async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
+     const res = await HttpService.client.markPrivateMessageAsRead(form);
+     this.findAndUpdateMessage(res);
+   }
+   async handleMessageReport(form: CreatePrivateMessageReport) {
+     const res = await HttpService.client.createPrivateMessageReport(form);
+     this.reportToast(res);
+   }
+   async handleCreateMessage(form: CreatePrivateMessage) {
+     const res = await HttpService.client.createPrivateMessage(form);
+     this.setState(s => {
+       if (s.messagesRes.state == "success" && res.state == "success") {
+         s.messagesRes.data.private_messages.unshift(
+           res.data.private_message_view
+         );
        }
-       this.setState(this.state);
-     } else if (op == UserOperation.DeletePrivateMessage) {
-       let data = wsJsonToRes<PrivateMessageResponse>(msg);
-       let found = this.state.messages.find(
-         m =>
-           m.private_message.id === data.private_message_view.private_message.id
-       );
-       if (found) {
-         let combinedView = this.state.combined.find(
-           i => i.id == data.private_message_view.private_message.id
-         )?.view as PrivateMessageView | undefined;
-         if (combinedView) {
-           found.private_message.deleted = combinedView.private_message.deleted =
-             data.private_message_view.private_message.deleted;
-           found.private_message.updated = combinedView.private_message.updated =
-             data.private_message_view.private_message.updated;
-         }
+       return s;
+     });
+   }
+   findAndUpdateMessage(res: RequestState<PrivateMessageResponse>) {
+     this.setState(s => {
+       if (s.messagesRes.state === "success" && res.state === "success") {
+         s.messagesRes.data.private_messages = editPrivateMessage(
+           res.data.private_message_view,
+           s.messagesRes.data.private_messages
+         );
        }
-       this.setState(this.state);
-     } else if (op == UserOperation.MarkPrivateMessageAsRead) {
-       let data = wsJsonToRes<PrivateMessageResponse>(msg);
-       let found = this.state.messages.find(
-         m =>
-           m.private_message.id === data.private_message_view.private_message.id
-       );
+       return s;
+     });
+   }
  
-       if (found) {
-         let combinedView = this.state.combined.find(
-           i =>
-             i.id == data.private_message_view.private_message.id &&
-             i.type_ == ReplyEnum.Message
-         )?.view as PrivateMessageView | undefined;
-         if (combinedView) {
-           found.private_message.updated = combinedView.private_message.updated =
-             data.private_message_view.private_message.updated;
-           // If youre in the unread view, just remove it from the list
-           if (
-             this.state.unreadOrAll == UnreadOrAll.Unread &&
-             data.private_message_view.private_message.read
-           ) {
-             this.setState({
-               messages: this.state.messages.filter(
-                 r =>
-                   r.private_message.id !==
-                   data.private_message_view.private_message.id
-               ),
-             });
-             this.setState({
-               combined: this.state.combined.filter(
-                 r => r.id !== data.private_message_view.private_message.id
-               ),
-             });
-           } else {
-             found.private_message.read = combinedView.private_message.read =
-               data.private_message_view.private_message.read;
-           }
+   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.repliesRes.state == "success") {
+           s.repliesRes.data.replies
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
          }
-       }
-       this.sendUnreadCount(data.private_message_view.private_message.read);
-       this.setState(this.state);
-     } else if (op == UserOperation.MarkAllAsRead) {
-       // Moved to be instant
-     } else if (
-       op == UserOperation.EditComment ||
-       op == UserOperation.DeleteComment ||
-       op == UserOperation.RemoveComment
-     ) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       editCommentRes(data.comment_view, this.state.replies);
-       this.setState(this.state);
-     } else if (op == UserOperation.MarkCommentReplyAsRead) {
-       let data = wsJsonToRes<CommentReplyResponse>(msg);
-       let found = this.state.replies.find(
-         c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
-       );
+         if (s.mentionsRes.state == "success") {
+           s.mentionsRes.data.mentions
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
+         }
+         return s;
+       });
+     }
+   }
  
-       if (found) {
-         let combinedView = this.state.combined.find(
-           i =>
-             i.id == data.comment_reply_view.comment_reply.id &&
-             i.type_ == ReplyEnum.Reply
-         )?.view as CommentReplyView | undefined;
-         if (combinedView) {
-           found.comment.content = combinedView.comment.content =
-             data.comment_reply_view.comment.content;
-           found.comment.updated = combinedView.comment.updated =
-             data.comment_reply_view.comment.updated;
-           found.comment.removed = combinedView.comment.removed =
-             data.comment_reply_view.comment.removed;
-           found.comment.deleted = combinedView.comment.deleted =
-             data.comment_reply_view.comment.deleted;
-           found.counts.upvotes = combinedView.counts.upvotes =
-             data.comment_reply_view.counts.upvotes;
-           found.counts.downvotes = combinedView.counts.downvotes =
-             data.comment_reply_view.counts.downvotes;
-           found.counts.score = combinedView.counts.score =
-             data.comment_reply_view.counts.score;
-           // If youre in the unread view, just remove it from the list
-           if (
-             this.state.unreadOrAll == UnreadOrAll.Unread &&
-             data.comment_reply_view.comment_reply.read
-           ) {
-             this.setState({
-               replies: this.state.replies.filter(
-                 r =>
-                   r.comment_reply.id !==
-                   data.comment_reply_view.comment_reply.id
-               ),
-             });
-             this.setState({
-               combined: this.state.combined.filter(
-                 r => r.id !== data.comment_reply_view.comment_reply.id
-               ),
-             });
-           } else {
-             found.comment_reply.read = combinedView.comment_reply.read =
-               data.comment_reply_view.comment_reply.read;
-           }
+   updateBan(banRes: RequestState<BanPersonResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.repliesRes.state == "success") {
+           s.repliesRes.data.replies
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
          }
-       }
-       this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
-       this.setState(this.state);
-     } else if (op == UserOperation.MarkPersonMentionAsRead) {
-       let data = wsJsonToRes<PersonMentionResponse>(msg);
-       // TODO this might not be correct, it might need to use the comment id
-       let found = this.state.mentions.find(
-         c => c.person_mention.id == data.person_mention_view.person_mention.id
-       );
+         if (s.mentionsRes.state == "success") {
+           s.mentionsRes.data.mentions
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
+         }
+         return s;
+       });
+     }
+   }
+   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+     if (purgeRes.state == "success") {
+       toast(i18n.t("purge_success"));
+       this.context.router.history.push(`/`);
+     }
+   }
+   reportToast(
+     res: RequestState<PrivateMessageReportResponse | CommentReportResponse>
+   ) {
+     if (res.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-       if (found) {
-         let combinedView = this.state.combined.find(
-           i =>
-             i.id == data.person_mention_view.person_mention.id &&
-             i.type_ == ReplyEnum.Mention
-         )?.view as PersonMentionView | undefined;
-         if (combinedView) {
-           found.comment.content = combinedView.comment.content =
-             data.person_mention_view.comment.content;
-           found.comment.updated = combinedView.comment.updated =
-             data.person_mention_view.comment.updated;
-           found.comment.removed = combinedView.comment.removed =
-             data.person_mention_view.comment.removed;
-           found.comment.deleted = combinedView.comment.deleted =
-             data.person_mention_view.comment.deleted;
-           found.counts.upvotes = combinedView.counts.upvotes =
-             data.person_mention_view.counts.upvotes;
-           found.counts.downvotes = combinedView.counts.downvotes =
-             data.person_mention_view.counts.downvotes;
-           found.counts.score = combinedView.counts.score =
-             data.person_mention_view.counts.score;
-           // If youre in the unread view, just remove it from the list
-           if (
-             this.state.unreadOrAll == UnreadOrAll.Unread &&
-             data.person_mention_view.person_mention.read
-           ) {
-             this.setState({
-               mentions: this.state.mentions.filter(
-                 r =>
-                   r.person_mention.id !==
-                   data.person_mention_view.person_mention.id
-               ),
-             });
-             this.setState({
-               combined: this.state.combined.filter(
-                 r => r.id !== data.person_mention_view.person_mention.id
-               ),
-             });
-           } else {
-             // TODO test to make sure these mentions are getting marked as read
-             found.person_mention.read = combinedView.person_mention.read =
-               data.person_mention_view.person_mention.read;
-           }
+   // A weird case, since you have only replies and mentions, not comment responses
+   findAndUpdateComment(res: RequestState<CommentResponse>) {
+     if (res.state == "success") {
+       this.setState(s => {
+         if (s.repliesRes.state == "success") {
+           s.repliesRes.data.replies = editWith(
+             res.data.comment_view,
+             s.repliesRes.data.replies
+           );
          }
-       }
-       this.sendUnreadCount(data.person_mention_view.person_mention.read);
-       this.setState(this.state);
-     } else if (op == UserOperation.CreatePrivateMessage) {
-       let data = wsJsonToRes<PrivateMessageResponse>(msg);
-       let mui = UserService.Instance.myUserInfo;
-       if (
-         data.private_message_view.recipient.id == mui?.local_user_view.person.id
-       ) {
-         this.state.messages.unshift(data.private_message_view);
-         this.state.combined.unshift(
-           this.messageToReplyType(data.private_message_view)
+         if (s.mentionsRes.state == "success") {
+           s.mentionsRes.data.mentions = editWith(
+             res.data.comment_view,
+             s.mentionsRes.data.mentions
+           );
+         }
+         // Set finished for the parent
+         s.finished.set(
+           getCommentParentId(res.data.comment_view.comment) ?? 0,
+           true
          );
-         this.setState(this.state);
-       }
-     } else if (op == UserOperation.SaveComment) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       saveCommentRes(data.comment_view, this.state.replies);
-       this.setState(this.state);
-       setupTippy();
-     } else if (op == UserOperation.CreateCommentLike) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       createCommentLikeRes(data.comment_view, this.state.replies);
-       this.setState(this.state);
-     } else if (op == UserOperation.BlockPerson) {
-       let data = wsJsonToRes<BlockPersonResponse>(msg);
-       updatePersonBlock(data);
-     } else if (op == UserOperation.CreatePostReport) {
-       let data = wsJsonToRes<PostReportResponse>(msg);
-       if (data) {
-         toast(i18n.t("report_created"));
-       }
-     } else if (op == UserOperation.CreateCommentReport) {
-       let data = wsJsonToRes<CommentReportResponse>(msg);
-       if (data) {
-         toast(i18n.t("report_created"));
-       }
-     } else if (op == UserOperation.CreatePrivateMessageReport) {
-       let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
-       if (data) {
-         toast(i18n.t("report_created"));
-       }
+         return s;
+       });
      }
    }
  
-   isMention(view: any): view is PersonMentionView {
-     return (view as PersonMentionView).person_mention !== undefined;
+   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+     this.setState(s => {
+       if (s.repliesRes.state == "success" && res.state == "success") {
+         s.repliesRes.data.replies = editCommentReply(
+           res.data.comment_reply_view,
+           s.repliesRes.data.replies
+         );
+       }
+       return s;
+     });
    }
  
-   isReply(view: any): view is CommentReplyView {
-     return (view as CommentReplyView).comment_reply !== undefined;
+   findAndUpdateMention(res: RequestState<PersonMentionResponse>) {
+     this.setState(s => {
+       if (s.mentionsRes.state == "success" && res.state == "success") {
+         s.mentionsRes.data.mentions = editMention(
+           res.data.person_mention_view,
+           s.mentionsRes.data.mentions
+         );
+       }
+       return s;
+     });
    }
  }
index 00a441970a6af6421492f2942b902bada80cc1cd,f80d5b907a2f7f1972dc0b62d1bebb51d06df1fe..5466bc5fcb746b2d388a7642725208e007201857
@@@ -4,42 -4,66 +4,67 @@@ import { Component, linkEvent } from "i
  import { Link } from "inferno-router";
  import { RouteComponentProps } from "inferno-router/dist/Route";
  import {
-   AddAdminResponse,
+   AddAdmin,
+   AddModToCommunity,
+   BanFromCommunity,
+   BanFromCommunityResponse,
    BanPerson,
    BanPersonResponse,
    BlockPerson,
-   BlockPersonResponse,
+   CommentId,
+   CommentReplyResponse,
    CommentResponse,
    Community,
    CommunityModeratorView,
+   CreateComment,
+   CreateCommentLike,
+   CreateCommentReport,
+   CreatePostLike,
+   CreatePostReport,
+   DeleteComment,
+   DeletePost,
+   DistinguishComment,
+   EditComment,
+   EditPost,
+   FeaturePost,
    GetPersonDetails,
    GetPersonDetailsResponse,
    GetSiteResponse,
+   LockPost,
+   MarkCommentReplyAsRead,
+   MarkPersonMentionAsRead,
+   PersonView,
    PostResponse,
+   PurgeComment,
    PurgeItemResponse,
+   PurgePerson,
+   PurgePost,
+   RemoveComment,
+   RemovePost,
+   SaveComment,
+   SavePost,
    SortType,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   TransferCommunity,
  } from "lemmy-js-client";
  import moment from "moment";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    canMod,
    capitalizeFirstLetter,
-   createCommentLikeRes,
-   createPostLikeFindRes,
-   editCommentRes,
-   editPostFindRes,
+   editComment,
+   editPost,
+   editWith,
    enableDownvotes,
    enableNsfw,
    fetchLimit,
    futureDaysToUnixTime,
+   getCommentParentId,
    getPageFromString,
    getQueryParams,
    getQueryString,
    isBanned,
    mdToHtml,
    myAuth,
+   myAuthRequired,
    numToSI,
    relTags,
    restoreScrollPosition,
-   saveCommentRes,
    saveScrollPosition,
    setIsoData,
    setupTippy,
    toast,
    updatePersonBlock,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { BannerIconHeader } from "../common/banner-icon-header";
  import { HtmlTags } from "../common/html-tags";
@@@ -68,19 -90,16 +91,20 @@@ import { CommunityLink } from "../commu
  import { PersonDetails } from "./person-details";
  import { PersonListing } from "./person-listing";
  
- interface ProfileData {
++type ProfileData = RouteDataResponse<{
 +  personResponse: GetPersonDetailsResponse;
- }
++}>;
 +
  interface ProfileState {
-   personRes?: GetPersonDetailsResponse;
-   loading: boolean;
+   personRes: RequestState<GetPersonDetailsResponse>;
    personBlocked: boolean;
    banReason?: string;
    banExpireDays?: number;
    showBanDialog: boolean;
    removeData: boolean;
    siteRes: GetSiteResponse;
+   finished: Map<CommentId, boolean | undefined>;
+   isIsomorphic: boolean;
  }
  
  interface ProfileProps {
@@@ -107,26 -126,6 +131,6 @@@ function getViewFromProps(view?: string
      : PersonDetailsView.Overview;
  }
  
- function toggleBlockPerson(recipientId: number, block: boolean) {
-   const auth = myAuth();
-   if (auth) {
-     const blockUserForm: BlockPerson = {
-       person_id: recipientId,
-       block,
-       auth,
-     };
-     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
-   }
- }
- const handleUnblockPerson = (personId: number) =>
-   toggleBlockPerson(personId, false);
- const handleBlockPerson = (personId: number) =>
-   toggleBlockPerson(personId, true);
  const getCommunitiesListing = (
    translationKey: NoOptionI18nKeys,
    communityViews?: { community: Community }[]
@@@ -157,14 -156,15 +161,15 @@@ export class Profile extends Component
    RouteComponentProps<{ username: string }>,
    ProfileState
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<ProfileData>(this.context);
-   private subscription?: Subscription;
    state: ProfileState = {
-     loading: true,
+     personRes: { state: "empty" },
      personBlocked: false,
      siteRes: this.isoData.site_res,
      showBanDialog: false,
      removeData: false,
+     finished: new Map(),
+     isIsomorphic: false,
    };
  
    constructor(props: RouteComponentProps<{ username: string }>, context: any) {
      this.handleSortChange = this.handleSortChange.bind(this);
      this.handlePageChange = this.handlePageChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleBlockPerson = this.handleBlockPerson.bind(this);
+     this.handleUnblockPerson = this.handleUnblockPerson.bind(this);
+     this.handleCreateComment = this.handleCreateComment.bind(this);
+     this.handleEditComment = this.handleEditComment.bind(this);
+     this.handleSaveComment = this.handleSaveComment.bind(this);
+     this.handleBlockPersonAlt = this.handleBlockPersonAlt.bind(this);
+     this.handleDeleteComment = this.handleDeleteComment.bind(this);
+     this.handleRemoveComment = this.handleRemoveComment.bind(this);
+     this.handleCommentVote = this.handleCommentVote.bind(this);
+     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+     this.handleAddAdmin = this.handleAddAdmin.bind(this);
+     this.handlePurgePerson = this.handlePurgePerson.bind(this);
+     this.handlePurgeComment = this.handlePurgeComment.bind(this);
+     this.handleCommentReport = this.handleCommentReport.bind(this);
+     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+     this.handleBanPerson = this.handleBanPerson.bind(this);
+     this.handlePostVote = this.handlePostVote.bind(this);
+     this.handlePostEdit = this.handlePostEdit.bind(this);
+     this.handlePostReport = this.handlePostReport.bind(this);
+     this.handleLockPost = this.handleLockPost.bind(this);
+     this.handleDeletePost = this.handleDeletePost.bind(this);
+     this.handleRemovePost = this.handleRemovePost.bind(this);
+     this.handleSavePost = this.handleSavePost.bind(this);
+     this.handlePurgePost = this.handlePurgePost.bind(this);
+     this.handleFeaturePost = this.handleFeaturePost.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
        this.state = {
          ...this.state,
 -        personRes: this.isoData.routeData[0],
 +        personRes: this.isoData.routeData.personResponse,
-         loading: false,
+         isIsomorphic: true,
        };
-     } else {
-       this.fetchUserData();
      }
    }
  
-   fetchUserData() {
-     const { page, sort, view } = getProfileQueryParams();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.fetchUserData();
+     }
+     setupTippy();
+   }
  
-     const form: GetPersonDetails = {
-       username: this.props.match.params.username,
-       sort,
-       saved_only: view === PersonDetailsView.Saved,
-       page,
-       limit: fetchLimit,
-       auth: myAuth(false),
-     };
+   componentWillUnmount() {
+     saveScrollPosition(this.context);
+   }
+   async fetchUserData() {
+     const { page, sort, view } = getProfileQueryParams();
  
-     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+     this.setState({ personRes: { state: "empty" } });
+     this.setState({
+       personRes: await HttpService.client.getPersonDetails({
+         username: this.props.match.params.username,
+         sort,
+         saved_only: view === PersonDetailsView.Saved,
+         page,
+         limit: fetchLimit,
+         auth: myAuth(),
+       }),
+     });
+     restoreScrollPosition(this.context);
+     this.setPersonBlock();
    }
  
    get amCurrentUser() {
-     return (
-       UserService.Instance.myUserInfo?.local_user_view.person.id ===
-       this.state.personRes?.person_view.person.id
-     );
+     if (this.state.personRes.state === "success") {
+       return (
+         UserService.Instance.myUserInfo?.local_user_view.person.id ===
+         this.state.personRes.data.person_view.person.id
+       );
+     } else {
+       return false;
+     }
    }
  
    setPersonBlock() {
      const mui = UserService.Instance.myUserInfo;
      const res = this.state.personRes;
  
-     if (mui && res) {
+     if (mui && res.state === "success") {
        this.setState({
          personBlocked: mui.person_blocks.some(
-           ({ target: { id } }) => id === res.person_view.person.id
+           ({ target: { id } }) => id === res.data.person_view.person.id
          ),
        });
      }
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      path,
      query: { page, sort, view: urlView },
      auth,
-   }: InitialFetchRequest<
-     QueryParams<ProfileProps>
-   >): WithPromiseKeys<ProfileData> {
 -  }: 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: client.getPersonDetails(form),
++      personResponse: await client.getPersonDetails(form),
 +    };
    }
  
-   componentDidMount() {
-     this.setPersonBlock();
-     setupTippy();
-   }
-   componentWillUnmount() {
-     this.subscription?.unsubscribe();
-     saveScrollPosition(this.context);
-   }
    get documentTitle(): string {
+     const siteName = this.state.siteRes.site_view.site.name;
      const res = this.state.personRes;
-     return res
-       ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
-       : "";
+     return res.state == "success"
+       ? `@${res.data.person_view.person.name} - ${siteName}`
+       : siteName;
    }
  
-   render() {
-     const { personRes, loading, siteRes } = this.state;
-     const { page, sort, view } = getProfileQueryParams();
-     return (
-       <div className="container-lg">
-         {loading ? (
+   renderPersonRes() {
+     switch (this.state.personRes.state) {
+       case "loading":
+         return (
            <h5>
              <Spinner large />
            </h5>
-         ) : (
-           personRes && (
-             <div className="row">
-               <div className="col-12 col-md-8">
-                 <HtmlTags
-                   title={this.documentTitle}
-                   path={this.context.router.route.match.url}
-                   description={personRes.person_view.person.bio}
-                   image={personRes.person_view.person.avatar}
-                 />
-                 {this.userInfo}
-                 <hr />
-                 {this.selects}
-                 <PersonDetails
-                   personRes={personRes}
-                   admins={siteRes.admins}
-                   sort={sort}
-                   page={page}
-                   limit={fetchLimit}
-                   enableDownvotes={enableDownvotes(siteRes)}
-                   enableNsfw={enableNsfw(siteRes)}
-                   view={view}
-                   onPageChange={this.handlePageChange}
-                   allLanguages={siteRes.all_languages}
-                   siteLanguages={siteRes.discussion_languages}
-                 />
-               </div>
+         );
+       case "success": {
+         const siteRes = this.state.siteRes;
+         const personRes = this.state.personRes.data;
+         const { page, sort, view } = getProfileQueryParams();
+         return (
+           <div className="row">
+             <div className="col-12 col-md-8">
+               <HtmlTags
+                 title={this.documentTitle}
+                 path={this.context.router.route.match.url}
+                 description={personRes.person_view.person.bio}
+                 image={personRes.person_view.person.avatar}
+               />
+               {this.userInfo(personRes.person_view)}
+               <hr />
+               {this.selects}
+               <PersonDetails
+                 personRes={personRes}
+                 admins={siteRes.admins}
+                 sort={sort}
+                 page={page}
+                 limit={fetchLimit}
+                 finished={this.state.finished}
+                 enableDownvotes={enableDownvotes(siteRes)}
+                 enableNsfw={enableNsfw(siteRes)}
+                 view={view}
+                 onPageChange={this.handlePageChange}
+                 allLanguages={siteRes.all_languages}
+                 siteLanguages={siteRes.discussion_languages}
+                 // TODO all the forms here
+                 onSaveComment={this.handleSaveComment}
+                 onBlockPerson={this.handleBlockPersonAlt}
+                 onDeleteComment={this.handleDeleteComment}
+                 onRemoveComment={this.handleRemoveComment}
+                 onCommentVote={this.handleCommentVote}
+                 onCommentReport={this.handleCommentReport}
+                 onDistinguishComment={this.handleDistinguishComment}
+                 onAddModToCommunity={this.handleAddModToCommunity}
+                 onAddAdmin={this.handleAddAdmin}
+                 onTransferCommunity={this.handleTransferCommunity}
+                 onPurgeComment={this.handlePurgeComment}
+                 onPurgePerson={this.handlePurgePerson}
+                 onCommentReplyRead={this.handleCommentReplyRead}
+                 onPersonMentionRead={this.handlePersonMentionRead}
+                 onBanPersonFromCommunity={this.handleBanFromCommunity}
+                 onBanPerson={this.handleBanPerson}
+                 onCreateComment={this.handleCreateComment}
+                 onEditComment={this.handleEditComment}
+                 onPostEdit={this.handlePostEdit}
+                 onPostVote={this.handlePostVote}
+                 onPostReport={this.handlePostReport}
+                 onLockPost={this.handleLockPost}
+                 onDeletePost={this.handleDeletePost}
+                 onRemovePost={this.handleRemovePost}
+                 onSavePost={this.handleSavePost}
+                 onPurgePost={this.handlePurgePost}
+                 onFeaturePost={this.handleFeaturePost}
+               />
+             </div>
  
-               <div className="col-12 col-md-4">
-                 <Moderates moderates={personRes.moderates} />
-                 {this.amCurrentUser && <Follows />}
-               </div>
+             <div className="col-12 col-md-4">
+               <Moderates moderates={personRes.moderates} />
+               {this.amCurrentUser && <Follows />}
              </div>
-           )
-         )}
-       </div>
-     );
+           </div>
+         );
+       }
+     }
+   }
+   render() {
+     return <div className="container-lg">{this.renderPersonRes()}</div>;
    }
  
    get viewRadios() {
          {this.getRadio(PersonDetailsView.Overview)}
          {this.getRadio(PersonDetailsView.Comments)}
          {this.getRadio(PersonDetailsView.Posts)}
-         {this.getRadio(PersonDetailsView.Saved)}
+         {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
        </div>
      );
    }
      );
    }
  
-   get userInfo() {
-     const pv = this.state.personRes?.person_view;
+   userInfo(pv: PersonView) {
      const {
        personBlocked,
        siteRes: { admins },
                      )}
                    </ul>
                  </div>
-                 {this.banDialog}
+                 {this.banDialog(pv)}
                  <div className="flex-grow-1 unselectable pointer mx-2"></div>
                  {!this.amCurrentUser && UserService.Instance.myUserInfo && (
                    <>
                          className={
                            "d-flex align-self-start btn btn-secondary mr-2"
                          }
-                         onClick={linkEvent(pv.person.id, handleUnblockPerson)}
+                         onClick={linkEvent(
+                           pv.person.id,
+                           this.handleUnblockPerson
+                         )}
                        >
                          {i18n.t("unblock_user")}
                        </button>
                          className={
                            "d-flex align-self-start btn btn-secondary mr-2"
                          }
-                         onClick={linkEvent(pv.person.id, handleBlockPerson)}
+                         onClick={linkEvent(
+                           pv.person.id,
+                           this.handleBlockPerson
+                         )}
                        >
                          {i18n.t("block_user")}
                        </button>
      );
    }
  
-   get banDialog() {
-     const pv = this.state.personRes?.person_view;
+   banDialog(pv: PersonView) {
      const { showBanDialog } = this.state;
  
      return (
-       pv && (
-         <>
-           {showBanDialog && (
-             <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
-               <div className="form-group row col-12">
-                 <label className="col-form-label" htmlFor="profile-ban-reason">
-                   {i18n.t("reason")}
-                 </label>
-                 <input
-                   type="text"
-                   id="profile-ban-reason"
-                   className="form-control mr-2"
-                   placeholder={i18n.t("reason")}
-                   value={this.state.banReason}
-                   onInput={linkEvent(this, this.handleModBanReasonChange)}
-                 />
-                 <label className="col-form-label" htmlFor={`mod-ban-expires`}>
-                   {i18n.t("expires")}
-                 </label>
+       showBanDialog && (
+         <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+           <div className="form-group row col-12">
+             <label className="col-form-label" htmlFor="profile-ban-reason">
+               {i18n.t("reason")}
+             </label>
+             <input
+               type="text"
+               id="profile-ban-reason"
+               className="form-control mr-2"
+               placeholder={i18n.t("reason")}
+               value={this.state.banReason}
+               onInput={linkEvent(this, this.handleModBanReasonChange)}
+             />
+             <label className="col-form-label" htmlFor={`mod-ban-expires`}>
+               {i18n.t("expires")}
+             </label>
+             <input
+               type="number"
+               id={`mod-ban-expires`}
+               className="form-control mr-2"
+               placeholder={i18n.t("number_of_days")}
+               value={this.state.banExpireDays}
+               onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+             />
+             <div className="form-group">
+               <div className="form-check">
                  <input
-                   type="number"
-                   id={`mod-ban-expires`}
-                   className="form-control mr-2"
-                   placeholder={i18n.t("number_of_days")}
-                   value={this.state.banExpireDays}
-                   onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+                   className="form-check-input"
+                   id="mod-ban-remove-data"
+                   type="checkbox"
+                   checked={this.state.removeData}
+                   onChange={linkEvent(this, this.handleModRemoveDataChange)}
                  />
-                 <div className="form-group">
-                   <div className="form-check">
-                     <input
-                       className="form-check-input"
-                       id="mod-ban-remove-data"
-                       type="checkbox"
-                       checked={this.state.removeData}
-                       onChange={linkEvent(this, this.handleModRemoveDataChange)}
-                     />
-                     <label
-                       className="form-check-label"
-                       htmlFor="mod-ban-remove-data"
-                       title={i18n.t("remove_content_more")}
-                     >
-                       {i18n.t("remove_content")}
-                     </label>
-                   </div>
-                 </div>
-               </div>
-               {/* TODO hold off on expires until later */}
-               {/* <div class="form-group row"> */}
-               {/*   <label class="col-form-label">Expires</label> */}
-               {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
-               {/* </div> */}
-               <div className="form-group row">
-                 <button
-                   type="reset"
-                   className="btn btn-secondary mr-2"
-                   aria-label={i18n.t("cancel")}
-                   onClick={linkEvent(this, this.handleModBanSubmitCancel)}
-                 >
-                   {i18n.t("cancel")}
-                 </button>
-                 <button
-                   type="submit"
-                   className="btn btn-secondary"
-                   aria-label={i18n.t("ban")}
+                 <label
+                   className="form-check-label"
+                   htmlFor="mod-ban-remove-data"
+                   title={i18n.t("remove_content_more")}
                  >
-                   {i18n.t("ban")} {pv.person.name}
-                 </button>
+                   {i18n.t("remove_content")}
+                 </label>
                </div>
-             </form>
-           )}
-         </>
+             </div>
+           </div>
+           {/* TODO hold off on expires until later */}
+           {/* <div class="form-group row"> */}
+           {/*   <label class="col-form-label">Expires</label> */}
+           {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+           {/* </div> */}
+           <div className="form-group row">
+             <button
+               type="reset"
+               className="btn btn-secondary mr-2"
+               aria-label={i18n.t("cancel")}
+               onClick={linkEvent(this, this.handleModBanSubmitCancel)}
+             >
+               {i18n.t("cancel")}
+             </button>
+             <button
+               type="submit"
+               className="btn btn-secondary"
+               aria-label={i18n.t("ban")}
+             >
+               {i18n.t("ban")} {pv.person.name}
+             </button>
+           </div>
+         </form>
        )
      );
    }
  
-   updateUrl({ page, sort, view }: Partial<ProfileProps>) {
+   async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
      const {
        page: urlPage,
        sort: urlSort,
      const { username } = this.props.match.params;
  
      this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
-     this.setState({ loading: true });
-     this.fetchUserData();
+     await this.fetchUserData();
    }
  
    handlePageChange(page: number) {
      i.setState({ removeData: event.target.checked });
    }
  
-   handleModBanSubmitCancel(i: Profile, event?: any) {
-     event.preventDefault();
+   handleModBanSubmitCancel(i: Profile) {
      i.setState({ showBanDialog: false });
    }
  
-   handleModBanSubmit(i: Profile, event?: any) {
-     if (event) event.preventDefault();
-     const { personRes, removeData, banReason, banExpireDays } = i.state;
+   async handleModBanSubmit(i: Profile, event: any) {
+     event.preventDefault();
+     const { removeData, banReason, banExpireDays } = i.state;
  
-     const person = personRes?.person_view.person;
-     const auth = myAuth();
+     const personRes = i.state.personRes;
  
-     if (person && auth) {
+     if (personRes.state == "success") {
+       const person = personRes.data.person_view.person;
        const ban = !person.banned;
  
        // If its an unban, restore all their data
          i.setState({ removeData: false });
        }
  
-       const form: BanPerson = {
+       const res = await HttpService.client.banPerson({
          person_id: person.id,
          ban,
          remove_data: removeData,
          reason: banReason,
          expires: futureDaysToUnixTime(banExpireDays),
-         auth,
-       };
-       WebSocketService.Instance.send(wsClient.banPerson(form));
+         auth: myAuthRequired(),
+       });
+       // TODO
+       this.updateBan(res);
        i.setState({ showBanDialog: false });
      }
    }
  
-   parseMessage(msg: any) {
-     const op = wsUserOp(msg);
-     console.log(msg);
+   async toggleBlockPerson(recipientId: number, block: boolean) {
+     const res = await HttpService.client.blockPerson({
+       person_id: recipientId,
+       block,
+       auth: myAuthRequired(),
+     });
+     if (res.state == "success") {
+       updatePersonBlock(res.data);
+     }
+   }
  
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
+   handleUnblockPerson(personId: number) {
+     this.toggleBlockPerson(personId, false);
+   }
  
-       if (msg.error === "couldnt_find_that_username_or_email") {
-         this.context.router.history.push("/");
-       }
-     } else if (msg.reconnect) {
-       this.fetchUserData();
-     } else {
-       switch (op) {
-         case UserOperation.GetPersonDetails: {
-           // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
-           // and set the parent state if it is not set or differs
-           // TODO this might need to get abstracted
-           const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
-           this.setState({ personRes: data, loading: false });
-           this.setPersonBlock();
-           restoreScrollPosition(this.context);
-           break;
-         }
+   handleBlockPerson(personId: number) {
+     this.toggleBlockPerson(personId, true);
+   }
  
-         case UserOperation.AddAdmin: {
-           const { admins } = wsJsonToRes<AddAdminResponse>(msg);
-           this.setState(s => ((s.siteRes.admins = admins), s));
+   async handleAddModToCommunity(form: AddModToCommunity) {
+     // TODO not sure what to do here
+     await HttpService.client.addModToCommunity(form);
+   }
  
-           break;
-         }
+   async handlePurgePerson(form: PurgePerson) {
+     const purgePersonRes = await HttpService.client.purgePerson(form);
+     this.purgeItem(purgePersonRes);
+   }
  
-         case UserOperation.CreateCommentLike: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           createCommentLikeRes(comment_view, this.state.personRes?.comments);
-           this.setState(this.state);
+   async handlePurgeComment(form: PurgeComment) {
+     const purgeCommentRes = await HttpService.client.purgeComment(form);
+     this.purgeItem(purgeCommentRes);
+   }
  
-           break;
-         }
+   async handlePurgePost(form: PurgePost) {
+     const purgeRes = await HttpService.client.purgePost(form);
+     this.purgeItem(purgeRes);
+   }
+   async handleBlockPersonAlt(form: BlockPerson) {
+     const blockPersonRes = await HttpService.client.blockPerson(form);
+     if (blockPersonRes.state === "success") {
+       updatePersonBlock(blockPersonRes.data);
+     }
+   }
  
-         case UserOperation.EditComment:
-         case UserOperation.DeleteComment:
-         case UserOperation.RemoveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           editCommentRes(comment_view, this.state.personRes?.comments);
-           this.setState(this.state);
+   async handleCreateComment(form: CreateComment) {
+     const createCommentRes = await HttpService.client.createComment(form);
+     this.createAndUpdateComments(createCommentRes);
  
-           break;
-         }
+     return createCommentRes;
+   }
  
-         case UserOperation.CreateComment: {
-           const {
-             comment_view: {
-               creator: { id },
-             },
-           } = wsJsonToRes<CommentResponse>(msg);
-           const mui = UserService.Instance.myUserInfo;
+   async handleEditComment(form: EditComment) {
+     const editCommentRes = await HttpService.client.editComment(form);
+     this.findAndUpdateComment(editCommentRes);
  
-           if (id === mui?.local_user_view.person.id) {
-             toast(i18n.t("reply_sent"));
-           }
+     return editCommentRes;
+   }
  
-           break;
-         }
+   async handleDeleteComment(form: DeleteComment) {
+     const deleteCommentRes = await HttpService.client.deleteComment(form);
+     this.findAndUpdateComment(deleteCommentRes);
+   }
  
-         case UserOperation.SaveComment: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           saveCommentRes(comment_view, this.state.personRes?.comments);
-           this.setState(this.state);
+   async handleDeletePost(form: DeletePost) {
+     const deleteRes = await HttpService.client.deletePost(form);
+     this.findAndUpdatePost(deleteRes);
+   }
  
-           break;
-         }
+   async handleRemovePost(form: RemovePost) {
+     const removeRes = await HttpService.client.removePost(form);
+     this.findAndUpdatePost(removeRes);
+   }
  
-         case UserOperation.EditPost:
-         case UserOperation.DeletePost:
-         case UserOperation.RemovePost:
-         case UserOperation.LockPost:
-         case UserOperation.FeaturePost:
-         case UserOperation.SavePost: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           editPostFindRes(post_view, this.state.personRes?.posts);
-           this.setState(this.state);
-           break;
-         }
+   async handleRemoveComment(form: RemoveComment) {
+     const removeCommentRes = await HttpService.client.removeComment(form);
+     this.findAndUpdateComment(removeCommentRes);
+   }
+   async handleSaveComment(form: SaveComment) {
+     const saveCommentRes = await HttpService.client.saveComment(form);
+     this.findAndUpdateComment(saveCommentRes);
+   }
+   async handleSavePost(form: SavePost) {
+     const saveRes = await HttpService.client.savePost(form);
+     this.findAndUpdatePost(saveRes);
+   }
+   async handleFeaturePost(form: FeaturePost) {
+     const featureRes = await HttpService.client.featurePost(form);
+     this.findAndUpdatePost(featureRes);
+   }
+   async handleCommentVote(form: CreateCommentLike) {
+     const voteRes = await HttpService.client.likeComment(form);
+     this.findAndUpdateComment(voteRes);
+   }
+   async handlePostVote(form: CreatePostLike) {
+     const voteRes = await HttpService.client.likePost(form);
+     this.findAndUpdatePost(voteRes);
+   }
+   async handlePostEdit(form: EditPost) {
+     const res = await HttpService.client.editPost(form);
+     this.findAndUpdatePost(res);
+   }
+   async handleCommentReport(form: CreateCommentReport) {
+     const reportRes = await HttpService.client.createCommentReport(form);
+     if (reportRes.state === "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
  
-         case UserOperation.CreatePostLike: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           createPostLikeFindRes(post_view, this.state.personRes?.posts);
-           this.setState(this.state);
+   async handlePostReport(form: CreatePostReport) {
+     const reportRes = await HttpService.client.createPostReport(form);
+     if (reportRes.state === "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
+   async handleLockPost(form: LockPost) {
+     const lockRes = await HttpService.client.lockPost(form);
+     this.findAndUpdatePost(lockRes);
+   }
+   async handleDistinguishComment(form: DistinguishComment) {
+     const distinguishRes = await HttpService.client.distinguishComment(form);
+     this.findAndUpdateComment(distinguishRes);
+   }
  
-           break;
+   async handleAddAdmin(form: AddAdmin) {
+     const addAdminRes = await HttpService.client.addAdmin(form);
+     if (addAdminRes.state == "success") {
+       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+     }
+   }
+   async handleTransferCommunity(form: TransferCommunity) {
+     await HttpService.client.transferCommunity(form);
+     toast(i18n.t("transfer_community"));
+   }
+   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+     const readRes = await HttpService.client.markCommentReplyAsRead(form);
+     this.findAndUpdateCommentReply(readRes);
+   }
+   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+     await HttpService.client.markPersonMentionAsRead(form);
+   }
+   async handleBanFromCommunity(form: BanFromCommunity) {
+     const banRes = await HttpService.client.banFromCommunity(form);
+     this.updateBanFromCommunity(banRes);
+   }
+   async handleBanPerson(form: BanPerson) {
+     const banRes = await HttpService.client.banPerson(form);
+     this.updateBan(banRes);
+   }
+   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+     // Maybe not necessary
+     if (banRes.state === "success") {
+       this.setState(s => {
+         if (s.personRes.state == "success") {
+           s.personRes.data.posts
+             .filter(c => c.creator.id === banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
+           s.personRes.data.comments
+             .filter(c => c.creator.id === banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
          }
+         return s;
+       });
+     }
+   }
  
-         case UserOperation.BanPerson: {
-           const data = wsJsonToRes<BanPersonResponse>(msg);
-           const res = this.state.personRes;
-           res?.comments
-             .filter(c => c.creator.id === data.person_view.person.id)
-             .forEach(c => (c.creator.banned = data.banned));
-           res?.posts
-             .filter(c => c.creator.id === data.person_view.person.id)
-             .forEach(c => (c.creator.banned = data.banned));
-           const pv = res?.person_view;
-           if (pv?.person.id === data.person_view.person.id) {
-             pv.person.banned = data.banned;
-           }
-           this.setState(this.state);
-           break;
+   updateBan(banRes: RequestState<BanPersonResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (s.personRes.state == "success") {
+           s.personRes.data.posts
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
+           s.personRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
          }
+         return s;
+       });
+     }
+   }
  
-         case UserOperation.BlockPerson: {
-           const data = wsJsonToRes<BlockPersonResponse>(msg);
-           updatePersonBlock(data);
-           this.setPersonBlock();
+   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+     if (purgeRes.state == "success") {
+       toast(i18n.t("purge_success"));
+       this.context.router.history.push(`/`);
+     }
+   }
  
-           break;
-         }
+   findAndUpdateComment(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.personRes.state == "success" && res.state == "success") {
+         s.personRes.data.comments = editComment(
+           res.data.comment_view,
+           s.personRes.data.comments
+         );
+         s.finished.set(res.data.comment_view.comment.id, true);
+       }
+       return s;
+     });
+   }
  
-         case UserOperation.PurgePerson:
-         case UserOperation.PurgePost:
-         case UserOperation.PurgeComment:
-         case UserOperation.PurgeCommunity: {
-           const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+   createAndUpdateComments(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.personRes.state == "success" && res.state == "success") {
+         s.personRes.data.comments.unshift(res.data.comment_view);
+         // Set finished for the parent
+         s.finished.set(
+           getCommentParentId(res.data.comment_view.comment) ?? 0,
+           true
+         );
+       }
+       return s;
+     });
+   }
  
-           if (success) {
-             toast(i18n.t("purge_success"));
-             this.context.router.history.push(`/`);
-           }
-         }
+   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+     this.setState(s => {
+       if (s.personRes.state == "success" && res.state == "success") {
+         s.personRes.data.comments = editWith(
+           res.data.comment_reply_view,
+           s.personRes.data.comments
+         );
        }
-     }
+       return s;
+     });
+   }
+   findAndUpdatePost(res: RequestState<PostResponse>) {
+     this.setState(s => {
+       if (s.personRes.state == "success" && res.state == "success") {
+         s.personRes.data.posts = editPost(
+           res.data.post_view,
+           s.personRes.data.posts
+         );
+       }
+       return s;
+     });
    }
  }
index 72ac5016a910fba17f0972249604afd5f5ae98fb,17b2a02585f8cce401170a24c1c58f70ef6499a0..be1eb2c11da0b976d43e4d7546f6485ce95d22d1
@@@ -1,28 -1,22 +1,22 @@@
  import { Component, linkEvent } from "inferno";
  import {
+   ApproveRegistrationApplication,
    GetSiteResponse,
--  ListRegistrationApplications,
    ListRegistrationApplicationsResponse,
-   RegistrationApplicationResponse,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   RegistrationApplicationView,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
-   WithPromiseKeys,
++  RouteDataResponse,
+   editRegistrationApplication,
    fetchLimit,
-   isBrowser,
-   myAuth,
+   myAuthRequired,
    setIsoData,
    setupTippy,
-   toast,
-   updateRegistrationApplicationRes,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
@@@ -34,64 -28,52 +28,56 @@@ enum UnreadOrAll 
    All,
  }
  
- interface RegistrationApplicationsData {
++type RegistrationApplicationsData = RouteDataResponse<{
 +  listRegistrationApplicationsResponse: ListRegistrationApplicationsResponse;
- }
++}>;
 +
  interface RegistrationApplicationsState {
-   listRegistrationApplicationsResponse?: ListRegistrationApplicationsResponse;
+   appsRes: RequestState<ListRegistrationApplicationsResponse>;
    siteRes: GetSiteResponse;
    unreadOrAll: UnreadOrAll;
    page: number;
-   loading: boolean;
+   isIsomorphic: boolean;
  }
  
  export class RegistrationApplications extends Component<
    any,
    RegistrationApplicationsState
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<RegistrationApplicationsData>(this.context);
-   private subscription?: Subscription;
    state: RegistrationApplicationsState = {
+     appsRes: { state: "empty" },
      siteRes: this.isoData.site_res,
      unreadOrAll: UnreadOrAll.Unread,
      page: 1,
-     loading: true,
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      super(props, context);
  
      this.handlePageChange = this.handlePageChange.bind(this);
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleApproveApplication = this.handleApproveApplication.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
        this.state = {
          ...this.state,
-         listRegistrationApplicationsResponse:
-           this.isoData.routeData.listRegistrationApplicationsResponse,
-         loading: false,
 -        appsRes: this.isoData.routeData[0],
++        appsRes: this.isoData.routeData.listRegistrationApplicationsResponse,
+         isIsomorphic: true,
        };
-     } else {
-       this.refetch();
      }
    }
  
-   componentDidMount() {
-     setupTippy();
-   }
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.refetch();
      }
+     setupTippy();
    }
  
    get documentTitle(): string {
-     let mui = UserService.Instance.myUserInfo;
+     const mui = UserService.Instance.myUserInfo;
      return mui
        ? `@${mui.local_user_view.person.name} ${i18n.t(
            "registration_applications"
        : "";
    }
  
-   render() {
-     return (
-       <div className="container-lg">
-         {this.state.loading ? (
+   renderApps() {
+     switch (this.state.appsRes.state) {
+       case "loading":
+         return (
            <h5>
              <Spinner large />
            </h5>
-         ) : (
+         );
+       case "success": {
+         const apps = this.state.appsRes.data.registration_applications;
+         return (
            <div className="row">
              <div className="col-12">
                <HtmlTags
                />
                <h5 className="mb-2">{i18n.t("registration_applications")}</h5>
                {this.selects()}
-               {this.applicationList()}
+               {this.applicationList(apps)}
                <Paginator
                  page={this.state.page}
                  onChange={this.handlePageChange}
                />
              </div>
            </div>
-         )}
-       </div>
-     );
+         );
+       }
+     }
+   }
+   render() {
+     return <div className="container-lg">{this.renderApps()}</div>;
    }
  
    unreadOrAllRadios() {
      );
    }
  
-   applicationList() {
-     let res = this.state.listRegistrationApplicationsResponse;
+   applicationList(apps: RegistrationApplicationView[]) {
      return (
-       res && (
-         <div>
-           {res.registration_applications.map(ra => (
-             <>
-               <hr />
-               <RegistrationApplication
-                 key={ra.registration_application.id}
-                 application={ra}
-               />
-             </>
-           ))}
-         </div>
-       )
+       <div>
+         {apps.map(ra => (
+           <>
+             <hr />
+             <RegistrationApplication
+               key={ra.registration_application.id}
+               application={ra}
+               onApproveApplication={this.handleApproveApplication}
+             />
+           </>
+         ))}
+       </div>
      );
    }
  
      this.refetch();
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      auth,
      client,
-   }: InitialFetchRequest): WithPromiseKeys<RegistrationApplicationsData> {
-     const form: ListRegistrationApplications = {
-       unread_only: true,
-       page: 1,
-       limit: fetchLimit,
-       auth: auth as string,
-     };
 -  }: InitialFetchRequest): Promise<any>[] {
 -    const promises: Promise<RequestState<any>>[] = [];
 -
 -    if (auth) {
 -      const form: ListRegistrationApplications = {
 -        unread_only: true,
 -        page: 1,
 -        limit: fetchLimit,
 -        auth,
 -      };
 -      promises.push(client.listRegistrationApplications(form));
 -    } else {
 -      promises.push(Promise.resolve({ state: "empty" }));
 -    }
--
 -    return promises;
++  }: InitialFetchRequest): Promise<RegistrationApplicationsData> {
 +    return {
-       listRegistrationApplicationsResponse:
-         client.listRegistrationApplications(form),
++      listRegistrationApplicationsResponse: auth
++        ? await client.listRegistrationApplications({
++            unread_only: true,
++            page: 1,
++            limit: fetchLimit,
++            auth: auth as string,
++          })
++        : { state: "empty" },
 +    };
    }
  
-   refetch() {
-     let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
-     let auth = myAuth();
-     if (auth) {
-       let form: ListRegistrationApplications = {
+   async refetch() {
+     const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+     this.setState({
+       appsRes: { state: "loading" },
+     });
+     this.setState({
+       appsRes: await HttpService.client.listRegistrationApplications({
          unread_only: unread_only,
          page: this.state.page,
          limit: fetchLimit,
-         auth,
-       };
-       WebSocketService.Instance.send(
-         wsClient.listRegistrationApplications(form)
-       );
-     }
+         auth: myAuthRequired(),
+       }),
+     });
    }
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       return;
-     } else if (msg.reconnect) {
-       this.refetch();
-     } else if (op == UserOperation.ListRegistrationApplications) {
-       let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg);
-       this.setState({
-         listRegistrationApplicationsResponse: data,
-         loading: false,
-       });
-       window.scrollTo(0, 0);
-     } else if (op == UserOperation.ApproveRegistrationApplication) {
-       let data = wsJsonToRes<RegistrationApplicationResponse>(msg);
-       updateRegistrationApplicationRes(
-         data.registration_application,
-         this.state.listRegistrationApplicationsResponse
-           ?.registration_applications
-       );
-       let uacs = UserService.Instance.unreadApplicationCountSub;
-       // Minor bug, where if the application switches from deny to approve, the count will still go down
-       uacs.next(uacs.getValue() - 1);
-       this.setState(this.state);
-     }
+   async handleApproveApplication(form: ApproveRegistrationApplication) {
+     const approveRes = await HttpService.client.approveRegistrationApplication(
+       form
+     );
+     this.setState(s => {
+       if (s.appsRes.state == "success" && approveRes.state == "success") {
+         s.appsRes.data.registration_applications = editRegistrationApplication(
+           approveRes.data.registration_application,
+           s.appsRes.data.registration_applications
+         );
+       }
+       return s;
+     });
    }
  }
index 51b41c92800e4ea78241bab4069e1b3a67d03b9f,29daa3ff6c92dbad9d621d8c2ed9be2e6945985c..fb8e8b831870bacc68daada998c165759753ed69
@@@ -13,28 -13,23 +13,24 @@@ import 
    PostReportView,
    PrivateMessageReportResponse,
    PrivateMessageReportView,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   ResolveCommentReport,
+   ResolvePostReport,
+   ResolvePrivateMessageReport,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { HttpService, UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { RequestState } from "../../services/HttpService";
  import {
-   WithPromiseKeys,
++  RouteDataResponse,
    amAdmin,
+   editCommentReport,
+   editPostReport,
+   editPrivateMessageReport,
    fetchLimit,
-   isBrowser,
-   myAuth,
+   myAuthRequired,
    setIsoData,
-   setupTippy,
-   toast,
-   updateCommentReportRes,
-   updatePostReportRes,
-   updatePrivateMessageReportRes,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { CommentReport } from "../comment/comment-report";
  import { HtmlTags } from "../common/html-tags";
@@@ -61,12 -56,6 +57,12 @@@ enum MessageEnum 
    PrivateMessageReport,
  }
  
- interface ReportsData {
++type ReportsData = RouteDataResponse<{
 +  commentReportsResponse: ListCommentReportsResponse;
 +  postReportsResponse: ListPostReportsResponse;
 +  privateMessageReportsResponse?: ListPrivateMessageReportsResponse;
- }
++}>;
 +
  type ItemType = {
    id: number;
    type_: MessageEnum;
  };
  
  interface ReportsState {
-   listCommentReportsResponse?: ListCommentReportsResponse;
-   listPostReportsResponse?: ListPostReportsResponse;
-   listPrivateMessageReportsResponse?: ListPrivateMessageReportsResponse;
+   commentReportsRes: RequestState<ListCommentReportsResponse>;
+   postReportsRes: RequestState<ListPostReportsResponse>;
+   messageReportsRes: RequestState<ListPrivateMessageReportsResponse>;
    unreadOrAll: UnreadOrAll;
    messageType: MessageType;
-   combined: ItemType[];
    siteRes: GetSiteResponse;
    page: number;
-   loading: boolean;
+   isIsomorphic: boolean;
  }
  
  export class Reports extends Component<any, ReportsState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<ReportsData>(this.context);
-   private subscription?: Subscription;
    state: ReportsState = {
+     commentReportsRes: { state: "empty" },
+     postReportsRes: { state: "empty" },
+     messageReportsRes: { state: "empty" },
      unreadOrAll: UnreadOrAll.Unread,
      messageType: MessageType.All,
-     combined: [],
      page: 1,
      siteRes: this.isoData.site_res,
-     loading: true,
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      super(props, context);
  
      this.handlePageChange = this.handlePageChange.bind(this);
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleResolveCommentReport =
+       this.handleResolveCommentReport.bind(this);
+     this.handleResolvePostReport = this.handleResolvePostReport.bind(this);
+     this.handleResolvePrivateMessageReport =
+       this.handleResolvePrivateMessageReport.bind(this);
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
 -      const [commentReportsRes, postReportsRes, messageReportsRes] =
 -        this.isoData.routeData;
 +      const {
-         commentReportsResponse,
-         postReportsResponse,
-         privateMessageReportsResponse,
++        commentReportsResponse: commentReportsRes,
++        postReportsResponse: postReportsRes,
++        privateMessageReportsResponse: messageReportsRes,
 +      } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
-         listCommentReportsResponse: commentReportsResponse,
-         listPostReportsResponse: postReportsResponse,
-         listPrivateMessageReportsResponse: privateMessageReportsResponse,
+         commentReportsRes,
+         postReportsRes,
+         isIsomorphic: true,
        };
  
-       this.state = {
-         ...this.state,
-         combined: this.buildCombined(),
-         loading: false,
-       };
-     } else {
-       this.refetch();
+       if (amAdmin()) {
+         this.state = {
+           ...this.state,
 -          messageReportsRes,
++          messageReportsRes: messageReportsRes ?? { state: "empty" },
+         };
+       }
      }
    }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.refetch();
      }
    }
  
    get documentTitle(): string {
-     let mui = UserService.Instance.myUserInfo;
+     const mui = UserService.Instance.myUserInfo;
      return mui
        ? `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
            this.state.siteRes.site_view.site.name
    render() {
      return (
        <div className="container-lg">
-         {this.state.loading ? (
-           <h5>
-             <Spinner large />
-           </h5>
-         ) : (
-           <div className="row">
-             <div className="col-12">
-               <HtmlTags
-                 title={this.documentTitle}
-                 path={this.context.router.route.match.url}
-               />
-               <h5 className="mb-2">{i18n.t("reports")}</h5>
-               {this.selects()}
-               {this.state.messageType == MessageType.All && this.all()}
-               {this.state.messageType == MessageType.CommentReport &&
-                 this.commentReports()}
-               {this.state.messageType == MessageType.PostReport &&
-                 this.postReports()}
-               {this.state.messageType == MessageType.PrivateMessageReport &&
-                 this.privateMessageReports()}
-               <Paginator
-                 page={this.state.page}
-                 onChange={this.handlePageChange}
-               />
-             </div>
+         <div className="row">
+           <div className="col-12">
+             <HtmlTags
+               title={this.documentTitle}
+               path={this.context.router.route.match.url}
+             />
+             <h5 className="mb-2">{i18n.t("reports")}</h5>
+             {this.selects()}
+             {this.section}
+             <Paginator
+               page={this.state.page}
+               onChange={this.handlePageChange}
+             />
            </div>
-         )}
+         </div>
        </div>
      );
    }
  
+   get section() {
+     switch (this.state.messageType) {
+       case MessageType.All: {
+         return this.all();
+       }
+       case MessageType.CommentReport: {
+         return this.commentReports();
+       }
+       case MessageType.PostReport: {
+         return this.postReports();
+       }
+       case MessageType.PrivateMessageReport: {
+         return this.privateMessageReports();
+       }
+       default: {
+         return null;
+       }
+     }
+   }
    unreadOrAllRadios() {
      return (
        <div className="btn-group btn-group-toggle flex-wrap mb-2">
      };
    }
  
-   buildCombined(): ItemType[] {
-     // let comments: ItemType[] = this.state.listCommentReportsResponse
-     //   .map(r => r.comment_reports)
-     //   .unwrapOr([])
-     //   .map(r => this.commentReportToItemType(r));
-     let comments =
-       this.state.listCommentReportsResponse?.comment_reports.map(
-         this.commentReportToItemType
-       ) ?? [];
-     let posts =
-       this.state.listPostReportsResponse?.post_reports.map(
-         this.postReportToItemType
-       ) ?? [];
-     let privateMessages =
-       this.state.listPrivateMessageReportsResponse?.private_message_reports.map(
-         this.privateMessageReportToItemType
-       ) ?? [];
+   get buildCombined(): ItemType[] {
+     const commentRes = this.state.commentReportsRes;
+     const comments =
+       commentRes.state == "success"
+         ? commentRes.data.comment_reports.map(this.commentReportToItemType)
+         : [];
+     const postRes = this.state.postReportsRes;
+     const posts =
+       postRes.state == "success"
+         ? postRes.data.post_reports.map(this.postReportToItemType)
+         : [];
+     const pmRes = this.state.messageReportsRes;
+     const privateMessages =
+       pmRes.state == "success"
+         ? pmRes.data.private_message_reports.map(
+             this.privateMessageReportToItemType
+           )
+         : [];
  
      return [...comments, ...posts, ...privateMessages].sort((a, b) =>
        b.published.localeCompare(a.published)
      switch (i.type_) {
        case MessageEnum.CommentReport:
          return (
-           <CommentReport key={i.id} report={i.view as CommentReportView} />
+           <CommentReport
+             key={i.id}
+             report={i.view as CommentReportView}
+             onResolveReport={this.handleResolveCommentReport}
+           />
          );
        case MessageEnum.PostReport:
-         return <PostReport key={i.id} report={i.view as PostReportView} />;
+         return (
+           <PostReport
+             key={i.id}
+             report={i.view as PostReportView}
+             onResolveReport={this.handleResolvePostReport}
+           />
+         );
        case MessageEnum.PrivateMessageReport:
          return (
            <PrivateMessageReport
              key={i.id}
              report={i.view as PrivateMessageReportView}
+             onResolveReport={this.handleResolvePrivateMessageReport}
            />
          );
        default:
    all() {
      return (
        <div>
-         {this.state.combined.map(i => (
+         {this.buildCombined.map(i => (
            <>
              <hr />
              {this.renderItemType(i)}
    }
  
    commentReports() {
-     let reports = this.state.listCommentReportsResponse?.comment_reports;
-     return (
-       reports && (
-         <div>
-           {reports.map(cr => (
-             <>
-               <hr />
-               <CommentReport key={cr.comment_report.id} report={cr} />
-             </>
-           ))}
-         </div>
-       )
-     );
+     const res = this.state.commentReportsRes;
+     switch (res.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const reports = res.data.comment_reports;
+         return (
+           <div>
+             {reports.map(cr => (
+               <>
+                 <hr />
+                 <CommentReport
+                   key={cr.comment_report.id}
+                   report={cr}
+                   onResolveReport={this.handleResolveCommentReport}
+                 />
+               </>
+             ))}
+           </div>
+         );
+       }
+     }
    }
  
    postReports() {
-     let reports = this.state.listPostReportsResponse?.post_reports;
-     return (
-       reports && (
-         <div>
-           {reports.map(pr => (
-             <>
-               <hr />
-               <PostReport key={pr.post_report.id} report={pr} />
-             </>
-           ))}
-         </div>
-       )
-     );
+     const res = this.state.postReportsRes;
+     switch (res.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const reports = res.data.post_reports;
+         return (
+           <div>
+             {reports.map(pr => (
+               <>
+                 <hr />
+                 <PostReport
+                   key={pr.post_report.id}
+                   report={pr}
+                   onResolveReport={this.handleResolvePostReport}
+                 />
+               </>
+             ))}
+           </div>
+         );
+       }
+     }
    }
  
    privateMessageReports() {
-     let reports =
-       this.state.listPrivateMessageReportsResponse?.private_message_reports;
-     return (
-       reports && (
-         <div>
-           {reports.map(pmr => (
-             <>
-               <hr />
-               <PrivateMessageReport
-                 key={pmr.private_message_report.id}
-                 report={pmr}
-               />
-             </>
-           ))}
-         </div>
-       )
-     );
+     const res = this.state.messageReportsRes;
+     switch (res.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const reports = res.data.private_message_reports;
+         return (
+           <div>
+             {reports.map(pmr => (
+               <>
+                 <hr />
+                 <PrivateMessageReport
+                   key={pmr.private_message_report.id}
+                   report={pmr}
+                   onResolveReport={this.handleResolvePrivateMessageReport}
+                 />
+               </>
+             ))}
+           </div>
+         );
+       }
+     }
    }
  
-   handlePageChange(page: number) {
+   async handlePageChange(page: number) {
      this.setState({ page });
-     this.refetch();
+     await this.refetch();
    }
  
-   handleUnreadOrAllChange(i: Reports, event: any) {
+   async handleUnreadOrAllChange(i: Reports, event: any) {
      i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
-     i.refetch();
+     await i.refetch();
    }
  
-   handleMessageTypeChange(i: Reports, event: any) {
+   async handleMessageTypeChange(i: Reports, event: any) {
      i.setState({ messageType: Number(event.target.value), page: 1 });
-     i.refetch();
+     await i.refetch();
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      auth,
      client,
-   }: InitialFetchRequest): WithPromiseKeys<ReportsData> {
 -  }: 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: WithPromiseKeys<ReportsData> = {
-       commentReportsResponse: client.listCommentReports(commentReportsForm),
-       postReportsResponse: client.listPostReports(postReportsForm),
++    const data: ReportsData = {
++      commentReportsResponse: await client.listCommentReports(
++        commentReportsForm
++      ),
++      postReportsResponse: await client.listPostReports(postReportsForm),
 +    };
 +
 +    if (amAdmin()) {
 +      const privateMessageReportsForm: ListPrivateMessageReports = {
          unresolved_only,
          page,
          limit,
 -        auth,
 +        auth: auth as string,
        };
 -      promises.push(client.listPostReports(postReportsForm));
  
-       data.privateMessageReportsResponse = client.listPrivateMessageReports(
-         privateMessageReportsForm
 -      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.privateMessageReportsResponse =
++        await client.listPrivateMessageReports(privateMessageReportsForm);
      }
  
 -    return promises;
 +    return data;
    }
  
-   refetch() {
-     const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
+   async refetch() {
+     const unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
      const page = this.state.page;
      const limit = fetchLimit;
-     const auth = myAuth();
+     const auth = myAuthRequired();
+     this.setState({
+       commentReportsRes: { state: "loading" },
+       postReportsRes: { state: "loading" },
+       messageReportsRes: { state: "loading" },
+     });
+     const form:
+       | ListCommentReports
+       | ListPostReports
+       | ListPrivateMessageReports = {
+       unresolved_only,
+       page,
+       limit,
+       auth,
+     };
  
-     if (auth) {
-       const commentReportsForm: ListCommentReports = {
-         unresolved_only,
-         page,
-         limit,
-         auth,
-       };
+     this.setState({
+       commentReportsRes: await HttpService.client.listCommentReports(form),
+       postReportsRes: await HttpService.client.listPostReports(form),
+     });
  
-       WebSocketService.Instance.send(
-         wsClient.listCommentReports(commentReportsForm)
-       );
+     if (amAdmin()) {
+       this.setState({
+         messageReportsRes: await HttpService.client.listPrivateMessageReports(
+           form
+         ),
+       });
+     }
+   }
  
-       const postReportsForm: ListPostReports = {
-         unresolved_only,
-         page,
-         limit,
-         auth,
-       };
+   async handleResolveCommentReport(form: ResolveCommentReport) {
+     const res = await HttpService.client.resolveCommentReport(form);
+     this.findAndUpdateCommentReport(res);
+   }
  
-       WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
+   async handleResolvePostReport(form: ResolvePostReport) {
+     const res = await HttpService.client.resolvePostReport(form);
+     this.findAndUpdatePostReport(res);
+   }
  
-       if (amAdmin()) {
-         const privateMessageReportsForm: ListPrivateMessageReports = {
-           unresolved_only,
-           page,
-           limit,
-           auth,
-         };
-         WebSocketService.Instance.send(
-           wsClient.listPrivateMessageReports(privateMessageReportsForm)
+   async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) {
+     const res = await HttpService.client.resolvePrivateMessageReport(form);
+     this.findAndUpdatePrivateMessageReport(res);
+   }
+   findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) {
+     this.setState(s => {
+       if (s.commentReportsRes.state == "success" && res.state == "success") {
+         s.commentReportsRes.data.comment_reports = editCommentReport(
+           res.data.comment_report_view,
+           s.commentReportsRes.data.comment_reports
          );
        }
-     }
+       return s;
+     });
    }
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       return;
-     } else if (msg.reconnect) {
-       this.refetch();
-     } else if (op == UserOperation.ListCommentReports) {
-       let data = wsJsonToRes<ListCommentReportsResponse>(msg);
-       this.setState({ listCommentReportsResponse: data });
-       this.setState({ combined: this.buildCombined(), loading: false });
-       // this.sendUnreadCount();
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.ListPostReports) {
-       let data = wsJsonToRes<ListPostReportsResponse>(msg);
-       this.setState({ listPostReportsResponse: data });
-       this.setState({ combined: this.buildCombined(), loading: false });
-       // this.sendUnreadCount();
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.ListPrivateMessageReports) {
-       let data = wsJsonToRes<ListPrivateMessageReportsResponse>(msg);
-       this.setState({ listPrivateMessageReportsResponse: data });
-       this.setState({ combined: this.buildCombined(), loading: false });
-       // this.sendUnreadCount();
-       window.scrollTo(0, 0);
-       setupTippy();
-     } else if (op == UserOperation.ResolvePostReport) {
-       let data = wsJsonToRes<PostReportResponse>(msg);
-       updatePostReportRes(
-         data.post_report_view,
-         this.state.listPostReportsResponse?.post_reports
-       );
-       let urcs = UserService.Instance.unreadReportCountSub;
-       if (data.post_report_view.post_report.resolved) {
-         urcs.next(urcs.getValue() - 1);
-       } else {
-         urcs.next(urcs.getValue() + 1);
-       }
-       this.setState(this.state);
-     } else if (op == UserOperation.ResolveCommentReport) {
-       let data = wsJsonToRes<CommentReportResponse>(msg);
-       updateCommentReportRes(
-         data.comment_report_view,
-         this.state.listCommentReportsResponse?.comment_reports
-       );
-       let urcs = UserService.Instance.unreadReportCountSub;
-       if (data.comment_report_view.comment_report.resolved) {
-         urcs.next(urcs.getValue() - 1);
-       } else {
-         urcs.next(urcs.getValue() + 1);
+   findAndUpdatePostReport(res: RequestState<PostReportResponse>) {
+     this.setState(s => {
+       if (s.postReportsRes.state == "success" && res.state == "success") {
+         s.postReportsRes.data.post_reports = editPostReport(
+           res.data.post_report_view,
+           s.postReportsRes.data.post_reports
+         );
        }
-       this.setState(this.state);
-     } else if (op == UserOperation.ResolvePrivateMessageReport) {
-       let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
-       updatePrivateMessageReportRes(
-         data.private_message_report_view,
-         this.state.listPrivateMessageReportsResponse?.private_message_reports
-       );
-       let urcs = UserService.Instance.unreadReportCountSub;
-       if (data.private_message_report_view.private_message_report.resolved) {
-         urcs.next(urcs.getValue() - 1);
-       } else {
-         urcs.next(urcs.getValue() + 1);
+       return s;
+     });
+   }
+   findAndUpdatePrivateMessageReport(
+     res: RequestState<PrivateMessageReportResponse>
+   ) {
+     this.setState(s => {
+       if (s.messageReportsRes.state == "success" && res.state == "success") {
+         s.messageReportsRes.data.private_message_reports =
+           editPrivateMessageReport(
+             res.data.private_message_report_view,
+             s.messageReportsRes.data.private_message_reports
+           );
        }
-       this.setState(this.state);
-     }
+       return s;
+     });
    }
  }
index 684dda2fc3b1a194a8bf67c1e9abc7c62be4dea3,71fac79aed8668bc9f694df463ee884628eb8419..bb39cdadbdab3dae9a162c0325997b43f25a34e0
@@@ -1,32 -1,28 +1,30 @@@
  import { Component } from "inferno";
  import { RouteComponentProps } from "inferno-router/dist/Route";
  import {
+   CreatePost as CreatePostI,
    GetCommunity,
 +  GetCommunityResponse,
    GetSiteResponse,
-   PostView,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   ListCommunitiesResponse,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
- import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
  import { i18n } from "../../i18next";
- import { WebSocketService } from "../../services";
+ import { InitialFetchRequest, PostFormParams } from "../../interfaces";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import {
+   HttpService,
+   RequestState,
+   WrappedLemmyHttp,
+ } from "../../services/HttpService";
  import {
    Choice,
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    enableDownvotes,
    enableNsfw,
    getIdFromString,
    getQueryParams,
-   isBrowser,
    myAuth,
    setIsoData,
-   toast,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
@@@ -36,31 -32,34 +34,39 @@@ export interface CreatePostProps 
    communityId?: number;
  }
  
- interface CreatePostData {
++type CreatePostData = RouteDataResponse<{
 +  communityResponse?: GetCommunityResponse;
- }
++  initialCommunitiesRes: ListCommunitiesResponse;
++}>;
 +
  function getCreatePostQueryParams() {
    return getQueryParams<CreatePostProps>({
      communityId: getIdFromString,
    });
  }
  
+ function fetchCommunitiesForOptions(client: WrappedLemmyHttp) {
+   return client.listCommunities({ limit: 30, sort: "TopMonth", type_: "All" });
+ }
  interface CreatePostState {
    siteRes: GetSiteResponse;
    loading: boolean;
    selectedCommunityChoice?: Choice;
+   initialCommunitiesRes: RequestState<ListCommunitiesResponse>;
+   isIsomorphic: boolean;
  }
  
  export class CreatePost extends Component<
    RouteComponentProps<Record<string, never>>,
    CreatePostState
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<CreatePostData>(this.context);
-   private subscription?: Subscription;
    state: CreatePostState = {
      siteRes: this.isoData.site_res,
      loading: true,
+     initialCommunitiesRes: { state: "empty" },
+     isIsomorphic: false,
    };
  
    constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
      this.handleSelectedCommunityChange =
        this.handleSelectedCommunityChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
-       const { communityResponse } = this.isoData.routeData;
+     if (FirstLoadService.isFirstLoad) {
 -      const [communityRes, listCommunitiesRes] = this.isoData.routeData;
++      const { communityResponse: communityRes, initialCommunitiesRes } =
++        this.isoData.routeData;
  
-       if (communityResponse) {
+       if (communityRes?.state === "success") {
          const communityChoice: Choice = {
-           label: communityResponse.community_view.community.title,
-           value: communityResponse.community_view.community.id.toString(),
+           label: communityRes.data.community_view.community.title,
+           value: communityRes.data.community_view.community.id.toString(),
          };
  
          this.state = {
        this.state = {
          ...this.state,
          loading: false,
 -        initialCommunitiesRes: listCommunitiesRes,
++        initialCommunitiesRes,
+         isIsomorphic: true,
        };
-     } else {
-       this.fetchCommunity();
      }
    }
  
-   fetchCommunity() {
+   async fetchCommunity() {
      const { communityId } = getCreatePostQueryParams();
-     const auth = myAuth(false);
+     const auth = myAuth();
  
      if (communityId) {
-       const form: GetCommunity = {
+       const res = await HttpService.client.getCommunity({
          id: communityId,
          auth,
-       };
-       WebSocketService.Instance.send(wsClient.getCommunity(form));
+       });
+       if (res.state === "success") {
+         this.setState({
+           selectedCommunityChoice: {
+             label: res.data.community_view.community.name,
+             value: res.data.community_view.community.id.toString(),
+           },
+           loading: false,
+         });
+       }
      }
    }
  
-   componentDidMount(): void {
-     const { communityId } = getCreatePostQueryParams();
+   async componentDidMount() {
+     // TODO test this
+     if (!this.state.isIsomorphic) {
+       const { communityId } = getCreatePostQueryParams();
+       const initialCommunitiesRes = await fetchCommunitiesForOptions(
+         HttpService.client
+       );
  
-     if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
-       this.fetchCommunity();
-     } else if (!communityId) {
        this.setState({
-         selectedCommunityChoice: undefined,
-         loading: false,
+         initialCommunitiesRes,
        });
-     }
-   }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+       if (
+         communityId?.toString() !== this.state.selectedCommunityChoice?.value
+       ) {
+         await this.fetchCommunity();
+       } else if (!communityId) {
+         this.setState({
+           selectedCommunityChoice: undefined,
+           loading: false,
+         });
+       }
      }
    }
  
                  siteLanguages={this.state.siteRes.discussion_languages}
                  selectedCommunityChoice={selectedCommunityChoice}
                  onSelectCommunity={this.handleSelectedCommunityChange}
+                 initialCommunities={
+                   this.state.initialCommunitiesRes.state === "success"
+                     ? this.state.initialCommunitiesRes.data.communities
+                     : []
+                 }
                />
              </div>
            </div>
      );
    }
  
-   updateUrl({ communityId }: Partial<CreatePostProps>) {
+   async updateUrl({ communityId }: Partial<CreatePostProps>) {
      const { communityId: urlCommunityId } = getCreatePostQueryParams();
  
      const locationState = this.props.history.location.state as
  
      history.replaceState(locationState, "", url);
  
-     this.fetchCommunity();
+     await this.fetchCommunity();
    }
  
    handleSelectedCommunityChange(choice: Choice) {
      });
    }
  
-   handlePostCreate(post_view: PostView) {
-     this.props.history.replace(`/post/${post_view.post.id}`);
+   async handlePostCreate(form: CreatePostI) {
+     const res = await HttpService.client.createPost(form);
+     if (res.state === "success") {
+       const postId = res.data.post_view.post.id;
+       this.props.history.replace(`/post/${postId}`);
+     }
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      query: { communityId },
      auth,
 -  }: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
 -    RequestState<any>
 -  >[] {
 -    const promises: Promise<RequestState<any>>[] = [];
 +  }: InitialFetchRequest<
 +    QueryParams<CreatePostProps>
-   >): WithPromiseKeys<CreatePostData> {
-     const data: WithPromiseKeys<CreatePostData> = {};
++  >): Promise<CreatePostData> {
++    const data: CreatePostData = {
++      initialCommunitiesRes: await fetchCommunitiesForOptions(client),
++    };
  
      if (communityId) {
        const form: GetCommunity = {
          id: getIdFromString(communityId),
        };
  
-       data.communityResponse = client.getCommunity(form);
 -      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;
    }
-   parseMessage(msg: any) {
-     const op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       return;
-     }
-     if (op === UserOperation.GetCommunity) {
-       const {
-         community_view: {
-           community: { title, id },
-         },
-       } = wsJsonToRes<GetCommunityResponse>(msg);
-       this.setState({
-         selectedCommunityChoice: { label: title, value: id.toString() },
-         loading: false,
-       });
-     }
-   }
  }
index fc8245de054eb7b0cd5fddd4a76c7132428237d5,9c68532bee5122c816ae12d4a48f12375b7c265e..501c06dcb675011132bf4a05181e72ef0e766483
@@@ -1,68 -1,88 +1,89 @@@
  import autosize from "autosize";
  import { Component, createRef, linkEvent, RefObject } from "inferno";
  import {
-   AddAdminResponse,
+   AddAdmin,
+   AddModToCommunity,
    AddModToCommunityResponse,
+   BanFromCommunity,
    BanFromCommunityResponse,
+   BanPerson,
    BanPersonResponse,
-   BlockPersonResponse,
-   CommentReportResponse,
+   BlockCommunity,
+   BlockPerson,
+   CommentId,
+   CommentReplyResponse,
    CommentResponse,
    CommentSortType,
    CommunityResponse,
+   CreateComment,
+   CreateCommentLike,
+   CreateCommentReport,
+   CreatePostLike,
+   CreatePostReport,
+   DeleteComment,
+   DeleteCommunity,
+   DeletePost,
+   DistinguishComment,
+   EditComment,
+   EditCommunity,
+   EditPost,
+   FeaturePost,
+   FollowCommunity,
    GetComments,
    GetCommentsResponse,
    GetCommunityResponse,
    GetPost,
    GetPostResponse,
    GetSiteResponse,
-   PostReportResponse,
+   LockPost,
+   MarkCommentReplyAsRead,
+   MarkPersonMentionAsRead,
    PostResponse,
-   PostView,
+   PurgeComment,
+   PurgeCommunity,
    PurgeItemResponse,
-   Search,
-   SearchResponse,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
+   PurgePerson,
+   PurgePost,
+   RemoveComment,
+   RemoveCommunity,
+   RemovePost,
+   SaveComment,
+   SavePost,
+   TransferCommunity,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import {
    CommentNodeI,
    CommentViewType,
    InitialFetchRequest,
  } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
    buildCommentsTree,
    commentsToFlatNodes,
    commentTreeMaxDepth,
-   createCommentLikeRes,
-   createPostLikeRes,
    debounce,
-   editCommentRes,
+   editComment,
+   editWith,
    enableDownvotes,
    enableNsfw,
    getCommentIdFromProps,
    getCommentParentId,
    getDepthFromComment,
    getIdFromProps,
-   insertCommentIntoTree,
    isBrowser,
    isImage,
    myAuth,
    restoreScrollPosition,
-   saveCommentRes,
++  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
    toast,
-   trendingFetchLimit,
+   updateCommunityBlock,
    updatePersonBlock,
-   WithPromiseKeys,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { CommentForm } from "../comment/comment-form";
  import { CommentNodes } from "../comment/comment-nodes";
@@@ -73,144 -93,143 +94,147 @@@ import { PostListing } from "./post-lis
  
  const commentsShownInterval = 15;
  
- interface PostData {
-   postResponse: GetPostResponse;
-   commentsResponse: GetCommentsResponse;
- }
++type PostData = RouteDataResponse<{
++  postRes: GetPostResponse;
++  commentsRes: GetCommentsResponse;
++}>;
 +
  interface PostState {
    postId?: number;
    commentId?: number;
-   postRes?: GetPostResponse;
-   commentsRes?: GetCommentsResponse;
-   commentTree: CommentNodeI[];
+   postRes: RequestState<GetPostResponse>;
+   commentsRes: RequestState<GetCommentsResponse>;
    commentSort: CommentSortType;
    commentViewType: CommentViewType;
    scrolled?: boolean;
-   loading: boolean;
-   crossPosts?: PostView[];
    siteRes: GetSiteResponse;
    commentSectionRef?: RefObject<HTMLDivElement>;
    showSidebarMobile: boolean;
    maxCommentsShown: number;
+   finished: Map<CommentId, boolean | undefined>;
+   isIsomorphic: boolean;
  }
  
  export class Post extends Component<any, PostState> {
-   private subscription?: Subscription;
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<PostData>(this.context);
    private commentScrollDebounced: () => void;
    state: PostState = {
+     postRes: { state: "empty" },
+     commentsRes: { state: "empty" },
      postId: getIdFromProps(this.props),
      commentId: getCommentIdFromProps(this.props),
-     commentTree: [],
      commentSort: "Hot",
      commentViewType: CommentViewType.Tree,
      scrolled: false,
-     loading: true,
      siteRes: this.isoData.site_res,
      showSidebarMobile: false,
      maxCommentsShown: commentsShownInterval,
+     finished: new Map(),
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      super(props, context);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
+     this.handleDeleteCommunityClick =
+       this.handleDeleteCommunityClick.bind(this);
+     this.handleEditCommunity = this.handleEditCommunity.bind(this);
+     this.handleFollow = this.handleFollow.bind(this);
+     this.handleModRemoveCommunity = this.handleModRemoveCommunity.bind(this);
+     this.handleCreateComment = this.handleCreateComment.bind(this);
+     this.handleEditComment = this.handleEditComment.bind(this);
+     this.handleSaveComment = this.handleSaveComment.bind(this);
+     this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+     this.handleBlockPerson = this.handleBlockPerson.bind(this);
+     this.handleDeleteComment = this.handleDeleteComment.bind(this);
+     this.handleRemoveComment = this.handleRemoveComment.bind(this);
+     this.handleCommentVote = this.handleCommentVote.bind(this);
+     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+     this.handleAddAdmin = this.handleAddAdmin.bind(this);
+     this.handlePurgePerson = this.handlePurgePerson.bind(this);
+     this.handlePurgeComment = this.handlePurgeComment.bind(this);
+     this.handleCommentReport = this.handleCommentReport.bind(this);
+     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+     this.handleFetchChildren = this.handleFetchChildren.bind(this);
+     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+     this.handleBanPerson = this.handleBanPerson.bind(this);
+     this.handlePostEdit = this.handlePostEdit.bind(this);
+     this.handlePostVote = this.handlePostVote.bind(this);
+     this.handlePostReport = this.handlePostReport.bind(this);
+     this.handleLockPost = this.handleLockPost.bind(this);
+     this.handleDeletePost = this.handleDeletePost.bind(this);
+     this.handleRemovePost = this.handleRemovePost.bind(this);
+     this.handleSavePost = this.handleSavePost.bind(this);
+     this.handlePurgePost = this.handlePurgePost.bind(this);
+     this.handleFeaturePost = this.handleFeaturePost.bind(this);
  
      this.state = { ...this.state, commentSectionRef: createRef() };
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
-       const { commentsResponse, postResponse } = this.isoData.routeData;
+     if (FirstLoadService.isFirstLoad) {
 -      const [postRes, commentsRes] = this.isoData.routeData;
++      const { commentsRes, postRes } = this.isoData.routeData;
  
        this.state = {
          ...this.state,
-         postRes: postResponse,
-         commentsRes: commentsResponse,
+         postRes,
+         commentsRes,
+         isIsomorphic: true,
        };
  
-       if (this.state.commentsRes) {
-         this.state = {
-           ...this.state,
-           commentTree: buildCommentsTree(
-             this.state.commentsRes.comments,
-             !!this.state.commentId
-           ),
-         };
-       }
-       this.state = { ...this.state, loading: false };
        if (isBrowser()) {
-         if (this.state.postRes) {
-           WebSocketService.Instance.send(
-             wsClient.communityJoin({
-               community_id: this.state.postRes.community_view.community.id,
-             })
-           );
-         }
-         if (this.state.postId) {
-           WebSocketService.Instance.send(
-             wsClient.postJoin({ post_id: this.state.postId })
-           );
-         }
-         this.fetchCrossPosts();
          if (this.checkScrollIntoCommentsParam) {
            this.scrollIntoCommentSection();
          }
        }
-     } else {
-       this.fetchPost();
      }
    }
  
-   fetchPost() {
-     let auth = myAuth(false);
-     let postForm: GetPost = {
-       id: this.state.postId,
-       comment_id: this.state.commentId,
-       auth,
-     };
-     WebSocketService.Instance.send(wsClient.getPost(postForm));
+   async fetchPost() {
+     this.setState({
+       postRes: { state: "loading" },
+       commentsRes: { state: "loading" },
+     });
  
-     let commentsForm: GetComments = {
-       post_id: this.state.postId,
-       parent_id: this.state.commentId,
-       max_depth: commentTreeMaxDepth,
-       sort: this.state.commentSort,
-       type_: "All",
-       saved_only: false,
-       auth,
-     };
-     WebSocketService.Instance.send(wsClient.getComments(commentsForm));
-   }
-   fetchCrossPosts() {
-     let q = this.state.postRes?.post_view.post.url;
-     if (q) {
-       let form: Search = {
-         q,
-         type_: "Url",
-         sort: "TopAll",
-         listing_type: "All",
-         page: 1,
-         limit: trendingFetchLimit,
-         auth: myAuth(false),
-       };
-       WebSocketService.Instance.send(wsClient.search(form));
+     const auth = myAuth();
+     this.setState({
+       postRes: await HttpService.client.getPost({
+         id: this.state.postId,
+         comment_id: this.state.commentId,
+         auth,
+       }),
+       commentsRes: await HttpService.client.getComments({
+         post_id: this.state.postId,
+         parent_id: this.state.commentId,
+         max_depth: commentTreeMaxDepth,
+         sort: this.state.commentSort,
+         type_: "All",
+         saved_only: false,
+         auth,
+       }),
+     });
+     setupTippy();
+     if (!this.state.commentId) restoreScrollPosition(this.context);
+     if (this.checkScrollIntoCommentsParam) {
+       this.scrollIntoCommentSection();
      }
    }
  
-   static fetchInitialData(req: InitialFetchRequest): WithPromiseKeys<PostData> {
-     const pathSplit = req.path.split("/");
 -  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;
-     const auth = req.auth;
  
      const postForm: GetPost = {
        auth,
        commentsForm.parent_id = id;
      }
  
 -    promises.push(client.getPost(postForm));
 -    promises.push(client.getComments(commentsForm));
 -
 -    return promises;
 +    return {
-       postResponse: req.client.getPost(postForm),
-       commentsResponse: req.client.getComments(commentsForm),
++      postRes: await client.getPost(postForm),
++      commentsRes: await client.getComments(commentsForm),
 +    };
    }
  
    componentWillUnmount() {
-     this.subscription?.unsubscribe();
      document.removeEventListener("scroll", this.commentScrollDebounced);
  
      saveScrollPosition(this.context);
    }
  
-   componentDidMount() {
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.fetchPost();
+     }
      autosize(document.querySelectorAll("textarea"));
  
      this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
      document.addEventListener("scroll", this.commentScrollDebounced);
    }
  
-   componentDidUpdate(_lastProps: any) {
+   async componentDidUpdate(_lastProps: any) {
      // Necessary if you are on a post and you click another post (same route)
      if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
-       // TODO Couldnt get a refresh working. This does for now.
-       location.reload();
-       // let currentId = this.props.match.params.id;
-       // WebSocketService.Instance.getPost(currentId);
-       // this.context.refresh();
-       // this.context.router.history.push(_lastProps.location.pathname);
+       await this.fetchPost();
      }
    }
  
    trackCommentsBoxScrolling = () => {
      const wrappedElement = document.getElementsByClassName("comments")[0];
      if (wrappedElement && this.isBottom(wrappedElement)) {
-       this.setState({
-         maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
-       });
+       const commentCount =
+         this.state.commentsRes.state == "success"
+           ? this.state.commentsRes.data.comments.length
+           : 0;
+       if (this.state.maxCommentsShown < commentCount) {
+         this.setState({
+           maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
+         });
+       }
      }
    };
  
    get documentTitle(): string {
-     let name_ = this.state.postRes?.post_view.post.name;
-     let siteName = this.state.siteRes.site_view.site.name;
-     return name_ ? `${name_} - ${siteName}` : "";
+     const siteName = this.state.siteRes.site_view.site.name;
+     return this.state.postRes.state == "success"
+       ? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
+       : siteName;
    }
  
    get imageTag(): string | undefined {
-     let post = this.state.postRes?.post_view.post;
-     let thumbnail = post?.thumbnail_url;
-     let url = post?.url;
-     return thumbnail || (url && isImage(url) ? url : undefined);
+     if (this.state.postRes.state == "success") {
+       const post = this.state.postRes.data.post_view.post;
+       const thumbnail = post.thumbnail_url;
+       const url = post.url;
+       return thumbnail || (url && isImage(url) ? url : undefined);
+     } else return undefined;
    }
  
-   render() {
-     let res = this.state.postRes;
-     let description = res?.post_view.post.body;
-     return (
-       <div className="container-lg">
-         {this.state.loading ? (
+   renderPostRes() {
+     switch (this.state.postRes.state) {
+       case "loading":
+         return (
            <h5>
              <Spinner large />
            </h5>
-         ) : (
-           res && (
-             <div className="row">
-               <div className="col-12 col-md-8 mb-3">
-                 <HtmlTags
-                   title={this.documentTitle}
-                   path={this.context.router.route.match.url}
-                   image={this.imageTag}
-                   description={description}
-                 />
-                 <PostListing
-                   post_view={res.post_view}
-                   duplicates={this.state.crossPosts}
-                   showBody
-                   showCommunity
-                   moderators={res.moderators}
-                   admins={this.state.siteRes.admins}
-                   enableDownvotes={enableDownvotes(this.state.siteRes)}
-                   enableNsfw={enableNsfw(this.state.siteRes)}
-                   allLanguages={this.state.siteRes.all_languages}
-                   siteLanguages={this.state.siteRes.discussion_languages}
-                 />
-                 <div ref={this.state.commentSectionRef} className="mb-2" />
-                 <CommentForm
-                   node={res.post_view.post.id}
-                   disabled={res.post_view.post.locked}
-                   allLanguages={this.state.siteRes.all_languages}
-                   siteLanguages={this.state.siteRes.discussion_languages}
-                 />
-                 <div className="d-block d-md-none">
-                   <button
-                     className="btn btn-secondary d-inline-block mb-2 mr-3"
-                     onClick={linkEvent(this, this.handleShowSidebarMobile)}
-                   >
-                     {i18n.t("sidebar")}{" "}
-                     <Icon
-                       icon={
-                         this.state.showSidebarMobile
-                           ? `minus-square`
-                           : `plus-square`
-                       }
-                       classes="icon-inline"
-                     />
-                   </button>
-                   {this.state.showSidebarMobile && this.sidebar()}
-                 </div>
-                 {this.sortRadios()}
-                 {this.state.commentViewType == CommentViewType.Tree &&
-                   this.commentsTree()}
-                 {this.state.commentViewType == CommentViewType.Flat &&
-                   this.commentsFlat()}
+         );
+       case "success": {
+         const res = this.state.postRes.data;
+         return (
+           <div className="row">
+             <div className="col-12 col-md-8 mb-3">
+               <HtmlTags
+                 title={this.documentTitle}
+                 path={this.context.router.route.match.url}
+                 image={this.imageTag}
+                 description={res.post_view.post.body}
+               />
+               <PostListing
+                 post_view={res.post_view}
+                 crossPosts={res.cross_posts}
+                 showBody
+                 showCommunity
+                 moderators={res.moderators}
+                 admins={this.state.siteRes.admins}
+                 enableDownvotes={enableDownvotes(this.state.siteRes)}
+                 enableNsfw={enableNsfw(this.state.siteRes)}
+                 allLanguages={this.state.siteRes.all_languages}
+                 siteLanguages={this.state.siteRes.discussion_languages}
+                 onBlockPerson={this.handleBlockPerson}
+                 onPostEdit={this.handlePostEdit}
+                 onPostVote={this.handlePostVote}
+                 onPostReport={this.handlePostReport}
+                 onLockPost={this.handleLockPost}
+                 onDeletePost={this.handleDeletePost}
+                 onRemovePost={this.handleRemovePost}
+                 onSavePost={this.handleSavePost}
+                 onPurgePerson={this.handlePurgePerson}
+                 onPurgePost={this.handlePurgePost}
+                 onBanPerson={this.handleBanPerson}
+                 onBanPersonFromCommunity={this.handleBanFromCommunity}
+                 onAddModToCommunity={this.handleAddModToCommunity}
+                 onAddAdmin={this.handleAddAdmin}
+                 onTransferCommunity={this.handleTransferCommunity}
+                 onFeaturePost={this.handleFeaturePost}
+               />
+               <div ref={this.state.commentSectionRef} className="mb-2" />
+               <CommentForm
+                 node={res.post_view.post.id}
+                 disabled={res.post_view.post.locked}
+                 allLanguages={this.state.siteRes.all_languages}
+                 siteLanguages={this.state.siteRes.discussion_languages}
+                 onUpsertComment={this.handleCreateComment}
+                 finished={this.state.finished.get(0)}
+               />
+               <div className="d-block d-md-none">
+                 <button
+                   className="btn btn-secondary d-inline-block mb-2 mr-3"
+                   onClick={linkEvent(this, this.handleShowSidebarMobile)}
+                 >
+                   {i18n.t("sidebar")}{" "}
+                   <Icon
+                     icon={
+                       this.state.showSidebarMobile
+                         ? `minus-square`
+                         : `plus-square`
+                     }
+                     classes="icon-inline"
+                   />
+                 </button>
+                 {this.state.showSidebarMobile && this.sidebar()}
                </div>
-               <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
+               {this.sortRadios()}
+               {this.state.commentViewType == CommentViewType.Tree &&
+                 this.commentsTree()}
+               {this.state.commentViewType == CommentViewType.Flat &&
+                 this.commentsFlat()}
              </div>
-           )
-         )}
-       </div>
-     );
+             <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
+           </div>
+         );
+       }
+     }
+   }
+   render() {
+     return <div className="container-lg">{this.renderPostRes()}</div>;
    }
  
    sortRadios() {
  
    commentsFlat() {
      // These are already sorted by new
-     let commentsRes = this.state.commentsRes;
-     let postRes = this.state.postRes;
-     return (
-       commentsRes &&
-       postRes && (
+     const commentsRes = this.state.commentsRes;
+     const postRes = this.state.postRes;
+     if (commentsRes.state == "success" && postRes.state == "success") {
+       return (
          <div>
            <CommentNodes
-             nodes={commentsToFlatNodes(commentsRes.comments)}
+             nodes={commentsToFlatNodes(commentsRes.data.comments)}
              viewType={this.state.commentViewType}
              maxCommentsShown={this.state.maxCommentsShown}
              noIndent
-             locked={postRes.post_view.post.locked}
-             moderators={postRes.moderators}
+             locked={postRes.data.post_view.post.locked}
+             moderators={postRes.data.moderators}
              admins={this.state.siteRes.admins}
              enableDownvotes={enableDownvotes(this.state.siteRes)}
              showContext
+             finished={this.state.finished}
              allLanguages={this.state.siteRes.all_languages}
              siteLanguages={this.state.siteRes.discussion_languages}
+             onSaveComment={this.handleSaveComment}
+             onBlockPerson={this.handleBlockPerson}
+             onDeleteComment={this.handleDeleteComment}
+             onRemoveComment={this.handleRemoveComment}
+             onCommentVote={this.handleCommentVote}
+             onCommentReport={this.handleCommentReport}
+             onDistinguishComment={this.handleDistinguishComment}
+             onAddModToCommunity={this.handleAddModToCommunity}
+             onAddAdmin={this.handleAddAdmin}
+             onTransferCommunity={this.handleTransferCommunity}
+             onFetchChildren={this.handleFetchChildren}
+             onPurgeComment={this.handlePurgeComment}
+             onPurgePerson={this.handlePurgePerson}
+             onCommentReplyRead={this.handleCommentReplyRead}
+             onPersonMentionRead={this.handlePersonMentionRead}
+             onBanPersonFromCommunity={this.handleBanFromCommunity}
+             onBanPerson={this.handleBanPerson}
+             onCreateComment={this.handleCreateComment}
+             onEditComment={this.handleEditComment}
            />
          </div>
-       )
-     );
+       );
+     }
    }
  
    sidebar() {
-     let res = this.state.postRes;
-     return (
-       res && (
+     const res = this.state.postRes;
+     if (res.state === "success") {
+       return (
          <div className="mb-3">
            <Sidebar
-             community_view={res.community_view}
-             moderators={res.moderators}
+             community_view={res.data.community_view}
+             moderators={res.data.moderators}
              admins={this.state.siteRes.admins}
-             online={res.online}
+             online={res.data.online}
              enableNsfw={enableNsfw(this.state.siteRes)}
              showIcon
              allLanguages={this.state.siteRes.all_languages}
              siteLanguages={this.state.siteRes.discussion_languages}
+             onDeleteCommunity={this.handleDeleteCommunityClick}
+             onLeaveModTeam={this.handleAddModToCommunity}
+             onFollowCommunity={this.handleFollow}
+             onRemoveCommunity={this.handleModRemoveCommunity}
+             onPurgeCommunity={this.handlePurgeCommunity}
+             onBlockCommunity={this.handleBlockCommunity}
+             onEditCommunity={this.handleEditCommunity}
            />
          </div>
-       )
-     );
-   }
-   handleCommentSortChange(i: Post, event: any) {
-     i.setState({
-       commentSort: event.target.value as CommentSortType,
-       commentViewType: CommentViewType.Tree,
-       commentsRes: undefined,
-       postRes: undefined,
-     });
-     i.fetchPost();
-   }
-   handleCommentViewTypeChange(i: Post, event: any) {
-     let comments = i.state.commentsRes?.comments;
-     if (comments) {
-       i.setState({
-         commentViewType: Number(event.target.value),
-         commentSort: "New",
-         commentTree: buildCommentsTree(comments, !!i.state.commentId),
-       });
-     }
-   }
-   handleShowSidebarMobile(i: Post) {
-     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
-   }
-   handleViewPost(i: Post) {
-     let id = i.state.postRes?.post_view.post.id;
-     if (id) {
-       i.context.router.history.push(`/post/${id}`);
-     }
-   }
-   handleViewContext(i: Post) {
-     let parentId = getCommentParentId(
-       i.state.commentsRes?.comments?.at(0)?.comment
-     );
-     if (parentId) {
-       i.context.router.history.push(`/comment/${parentId}`);
+       );
      }
    }
  
    commentsTree() {
-     let res = this.state.postRes;
-     let firstComment = this.state.commentTree.at(0)?.comment_view.comment;
-     let depth = getDepthFromComment(firstComment);
-     let showContextButton = depth ? depth > 0 : false;
+     const res = this.state.postRes;
+     const firstComment = this.commentTree().at(0)?.comment_view.comment;
+     const depth = getDepthFromComment(firstComment);
+     const showContextButton = depth ? depth > 0 : false;
  
      return (
-       res && (
+       res.state == "success" && (
          <div>
            {!!this.state.commentId && (
              <>
              </>
            )}
            <CommentNodes
-             nodes={this.state.commentTree}
+             nodes={this.commentTree()}
              viewType={this.state.commentViewType}
              maxCommentsShown={this.state.maxCommentsShown}
-             locked={res.post_view.post.locked}
-             moderators={res.moderators}
+             locked={res.data.post_view.post.locked}
+             moderators={res.data.moderators}
              admins={this.state.siteRes.admins}
              enableDownvotes={enableDownvotes(this.state.siteRes)}
+             finished={this.state.finished}
              allLanguages={this.state.siteRes.all_languages}
              siteLanguages={this.state.siteRes.discussion_languages}
+             onSaveComment={this.handleSaveComment}
+             onBlockPerson={this.handleBlockPerson}
+             onDeleteComment={this.handleDeleteComment}
+             onRemoveComment={this.handleRemoveComment}
+             onCommentVote={this.handleCommentVote}
+             onCommentReport={this.handleCommentReport}
+             onDistinguishComment={this.handleDistinguishComment}
+             onAddModToCommunity={this.handleAddModToCommunity}
+             onAddAdmin={this.handleAddAdmin}
+             onTransferCommunity={this.handleTransferCommunity}
+             onFetchChildren={this.handleFetchChildren}
+             onPurgeComment={this.handlePurgeComment}
+             onPurgePerson={this.handlePurgePerson}
+             onCommentReplyRead={this.handleCommentReplyRead}
+             onPersonMentionRead={this.handlePersonMentionRead}
+             onBanPersonFromCommunity={this.handleBanFromCommunity}
+             onBanPerson={this.handleBanPerson}
+             onCreateComment={this.handleCreateComment}
+             onEditComment={this.handleEditComment}
            />
          </div>
        )
      );
    }
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       return;
-     } else if (msg.reconnect) {
-       let post_id = this.state.postRes?.post_view.post.id;
-       if (post_id) {
-         WebSocketService.Instance.send(wsClient.postJoin({ post_id }));
-         WebSocketService.Instance.send(
-           wsClient.getPost({
-             id: post_id,
-             auth: myAuth(false),
-           })
-         );
-       }
-     } else if (op == UserOperation.GetPost) {
-       let data = wsJsonToRes<GetPostResponse>(msg);
-       this.setState({ postRes: data });
-       // join the rooms
-       WebSocketService.Instance.send(
-         wsClient.postJoin({ post_id: data.post_view.post.id })
-       );
-       WebSocketService.Instance.send(
-         wsClient.communityJoin({
-           community_id: data.community_view.community.id,
-         })
+   commentTree(): CommentNodeI[] {
+     if (this.state.commentsRes.state == "success") {
+       return buildCommentsTree(
+         this.state.commentsRes.data.comments,
+         !!this.state.commentId
        );
+     } else {
+       return [];
+     }
+   }
+   async handleCommentSortChange(i: Post, event: any) {
+     i.setState({
+       commentSort: event.target.value as CommentSortType,
+       commentViewType: CommentViewType.Tree,
+       commentsRes: { state: "loading" },
+       postRes: { state: "loading" },
+     });
+     await i.fetchPost();
+   }
+   handleCommentViewTypeChange(i: Post, event: any) {
+     i.setState({
+       commentViewType: Number(event.target.value),
+       commentSort: "New",
+     });
+   }
  
-       // Get cross-posts
-       // TODO move this into initial fetch and refetch
-       this.fetchCrossPosts();
-       setupTippy();
-       if (!this.state.commentId) restoreScrollPosition(this.context);
+   handleShowSidebarMobile(i: Post) {
+     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
+   }
+   handleViewPost(i: Post) {
+     if (i.state.postRes.state == "success") {
+       const id = i.state.postRes.data.post_view.post.id;
+       i.context.router.history.push(`/post/${id}`);
+     }
+   }
  
-       if (this.checkScrollIntoCommentsParam) {
-         this.scrollIntoCommentSection();
+   handleViewContext(i: Post) {
+     if (i.state.commentsRes.state == "success") {
+       const parentId = getCommentParentId(
+         i.state.commentsRes.data.comments.at(0)?.comment
+       );
+       if (parentId) {
+         i.context.router.history.push(`/comment/${parentId}`);
        }
-     } else if (op == UserOperation.GetComments) {
-       let data = wsJsonToRes<GetCommentsResponse>(msg);
-       // This section sets the comments res
-       let comments = this.state.commentsRes?.comments;
-       if (comments) {
-         // You might need to append here, since this could be building more comments from a tree fetch
-         // Remove the first comment, since it is the parent
-         let newComments = data.comments;
-         newComments.shift();
-         comments.push(...newComments);
-       } else {
-         this.setState({ commentsRes: data });
+     }
+   }
+   async handleDeleteCommunityClick(form: DeleteCommunity) {
+     const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
+     this.updateCommunity(deleteCommunityRes);
+   }
+   async handleAddModToCommunity(form: AddModToCommunity) {
+     const addModRes = await HttpService.client.addModToCommunity(form);
+     this.updateModerators(addModRes);
+   }
+   async handleFollow(form: FollowCommunity) {
+     const followCommunityRes = await HttpService.client.followCommunity(form);
+     this.updateCommunity(followCommunityRes);
+     // Update myUserInfo
+     if (followCommunityRes.state === "success") {
+       const communityId = followCommunityRes.data.community_view.community.id;
+       const mui = UserService.Instance.myUserInfo;
+       if (mui) {
+         mui.follows = mui.follows.filter(i => i.community.id != communityId);
        }
+     }
+   }
  
-       let cComments = this.state.commentsRes?.comments ?? [];
-       this.setState({
-         commentTree: buildCommentsTree(cComments, !!this.state.commentId),
-         loading: false,
-       });
-     } else if (op == UserOperation.CreateComment) {
-       let data = wsJsonToRes<CommentResponse>(msg);
+   async handlePurgeCommunity(form: PurgeCommunity) {
+     const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
+     this.purgeItem(purgeCommunityRes);
+   }
  
-       // Don't get comments from the post room, if the creator is blocked
-       let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
-         .map(pb => pb.target.id)
-         .includes(data.comment_view.creator.id);
+   async handlePurgePerson(form: PurgePerson) {
+     const purgePersonRes = await HttpService.client.purgePerson(form);
+     this.purgeItem(purgePersonRes);
+   }
+   async handlePurgeComment(form: PurgeComment) {
+     const purgeCommentRes = await HttpService.client.purgeComment(form);
+     this.purgeItem(purgeCommentRes);
+   }
+   async handlePurgePost(form: PurgePost) {
+     const purgeRes = await HttpService.client.purgePost(form);
+     this.purgeItem(purgeRes);
+   }
  
-       // Necessary since it might be a user reply, which has the recipients, to avoid double
-       let postRes = this.state.postRes;
-       let commentsRes = this.state.commentsRes;
+   async handleBlockCommunity(form: BlockCommunity) {
+     const blockCommunityRes = await HttpService.client.blockCommunity(form);
+     // TODO Probably isn't necessary
+     this.setState(s => {
        if (
-         data.recipient_ids.length == 0 &&
-         !creatorBlocked &&
-         postRes &&
-         data.comment_view.post.id == postRes.post_view.post.id &&
-         commentsRes
+         s.postRes.state == "success" &&
+         blockCommunityRes.state == "success"
        ) {
-         commentsRes.comments.unshift(data.comment_view);
-         insertCommentIntoTree(
-           this.state.commentTree,
-           data.comment_view,
-           !!this.state.commentId
-         );
-         postRes.post_view.counts.comments++;
-         this.setState(this.state);
-         setupTippy();
+         s.postRes.data.community_view = blockCommunityRes.data.community_view;
        }
-     } else if (
-       op == UserOperation.EditComment ||
-       op == UserOperation.DeleteComment ||
-       op == UserOperation.RemoveComment
-     ) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       editCommentRes(data.comment_view, this.state.commentsRes?.comments);
-       this.setState(this.state);
-       setupTippy();
-     } else if (op == UserOperation.SaveComment) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
-       this.setState(this.state);
-       setupTippy();
-     } else if (op == UserOperation.CreateCommentLike) {
-       let data = wsJsonToRes<CommentResponse>(msg);
-       createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
-       this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let data = wsJsonToRes<PostResponse>(msg);
-       createPostLikeRes(data.post_view, this.state.postRes?.post_view);
-       this.setState(this.state);
-     } else if (
-       op == UserOperation.EditPost ||
-       op == UserOperation.DeletePost ||
-       op == UserOperation.RemovePost ||
-       op == UserOperation.LockPost ||
-       op == UserOperation.FeaturePost ||
-       op == UserOperation.SavePost
-     ) {
-       let data = wsJsonToRes<PostResponse>(msg);
-       let res = this.state.postRes;
-       if (res) {
-         res.post_view = data.post_view;
-         this.setState(this.state);
-         setupTippy();
-       }
-     } else if (
-       op == UserOperation.EditCommunity ||
-       op == UserOperation.DeleteCommunity ||
-       op == UserOperation.RemoveCommunity ||
-       op == UserOperation.FollowCommunity
+       return s;
+     });
+     if (blockCommunityRes.state == "success") {
+       updateCommunityBlock(blockCommunityRes.data);
+     }
+   }
+   async handleBlockPerson(form: BlockPerson) {
+     const blockPersonRes = await HttpService.client.blockPerson(form);
+     if (blockPersonRes.state == "success") {
+       updatePersonBlock(blockPersonRes.data);
+     }
+   }
+   async handleModRemoveCommunity(form: RemoveCommunity) {
+     const removeCommunityRes = await HttpService.client.removeCommunity(form);
+     this.updateCommunity(removeCommunityRes);
+   }
+   async handleEditCommunity(form: EditCommunity) {
+     const res = await HttpService.client.editCommunity(form);
+     this.updateCommunity(res);
+     return res;
+   }
+   async handleCreateComment(form: CreateComment) {
+     const createCommentRes = await HttpService.client.createComment(form);
+     this.createAndUpdateComments(createCommentRes);
+     return createCommentRes;
+   }
+   async handleEditComment(form: EditComment) {
+     const editCommentRes = await HttpService.client.editComment(form);
+     this.findAndUpdateComment(editCommentRes);
+     return editCommentRes;
+   }
+   async handleDeleteComment(form: DeleteComment) {
+     const deleteCommentRes = await HttpService.client.deleteComment(form);
+     this.findAndUpdateComment(deleteCommentRes);
+   }
+   async handleDeletePost(form: DeletePost) {
+     const deleteRes = await HttpService.client.deletePost(form);
+     this.updatePost(deleteRes);
+   }
+   async handleRemovePost(form: RemovePost) {
+     const removeRes = await HttpService.client.removePost(form);
+     this.updatePost(removeRes);
+   }
+   async handleRemoveComment(form: RemoveComment) {
+     const removeCommentRes = await HttpService.client.removeComment(form);
+     this.findAndUpdateComment(removeCommentRes);
+   }
+   async handleSaveComment(form: SaveComment) {
+     const saveCommentRes = await HttpService.client.saveComment(form);
+     this.findAndUpdateComment(saveCommentRes);
+   }
+   async handleSavePost(form: SavePost) {
+     const saveRes = await HttpService.client.savePost(form);
+     this.updatePost(saveRes);
+   }
+   async handleFeaturePost(form: FeaturePost) {
+     const featureRes = await HttpService.client.featurePost(form);
+     this.updatePost(featureRes);
+   }
+   async handleCommentVote(form: CreateCommentLike) {
+     const voteRes = await HttpService.client.likeComment(form);
+     this.findAndUpdateComment(voteRes);
+   }
+   async handlePostVote(form: CreatePostLike) {
+     const voteRes = await HttpService.client.likePost(form);
+     this.updatePost(voteRes);
+   }
+   async handlePostEdit(form: EditPost) {
+     const res = await HttpService.client.editPost(form);
+     this.updatePost(res);
+   }
+   async handleCommentReport(form: CreateCommentReport) {
+     const reportRes = await HttpService.client.createCommentReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
+   async handlePostReport(form: CreatePostReport) {
+     const reportRes = await HttpService.client.createPostReport(form);
+     if (reportRes.state == "success") {
+       toast(i18n.t("report_created"));
+     }
+   }
+   async handleLockPost(form: LockPost) {
+     const lockRes = await HttpService.client.lockPost(form);
+     this.updatePost(lockRes);
+   }
+   async handleDistinguishComment(form: DistinguishComment) {
+     const distinguishRes = await HttpService.client.distinguishComment(form);
+     this.findAndUpdateComment(distinguishRes);
+   }
+   async handleAddAdmin(form: AddAdmin) {
+     const addAdminRes = await HttpService.client.addAdmin(form);
+     if (addAdminRes.state === "success") {
+       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+     }
+   }
+   async handleTransferCommunity(form: TransferCommunity) {
+     const transferCommunityRes = await HttpService.client.transferCommunity(
+       form
+     );
+     this.updateCommunityFull(transferCommunityRes);
+   }
+   async handleFetchChildren(form: GetComments) {
+     const moreCommentsRes = await HttpService.client.getComments(form);
+     if (
+       this.state.commentsRes.state == "success" &&
+       moreCommentsRes.state == "success"
      ) {
-       let data = wsJsonToRes<CommunityResponse>(msg);
-       let res = this.state.postRes;
-       if (res) {
-         res.community_view = data.community_view;
-         res.post_view.community = data.community_view.community;
-         this.setState(this.state);
-       }
-     } else if (op == UserOperation.BanFromCommunity) {
-       let data = wsJsonToRes<BanFromCommunityResponse>(msg);
+       const newComments = moreCommentsRes.data.comments;
+       // Remove the first comment, since it is the parent
+       newComments.shift();
+       const newRes = this.state.commentsRes;
+       newRes.data.comments.push(...newComments);
+       this.setState({ commentsRes: newRes });
+     }
+   }
+   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+     const readRes = await HttpService.client.markCommentReplyAsRead(form);
+     this.findAndUpdateCommentReply(readRes);
+   }
  
-       let res = this.state.postRes;
-       if (res) {
-         if (res.post_view.creator.id == data.person_view.person.id) {
-           res.post_view.creator_banned_from_community = data.banned;
+   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+     await HttpService.client.markPersonMentionAsRead(form);
+   }
+   async handleBanFromCommunity(form: BanFromCommunity) {
+     const banRes = await HttpService.client.banFromCommunity(form);
+     this.updateBan(banRes);
+   }
+   async handleBanPerson(form: BanPerson) {
+     const banRes = await HttpService.client.banPerson(form);
+     this.updateBan(banRes);
+   }
+   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (
+           s.postRes.state == "success" &&
+           s.postRes.data.post_view.creator.id ==
+             banRes.data.person_view.person.id
+         ) {
+           s.postRes.data.post_view.creator_banned_from_community =
+             banRes.data.banned;
+         }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(
+               c => (c.creator_banned_from_community = banRes.data.banned)
+             );
+         }
+         return s;
+       });
+     }
+   }
+   updateBan(banRes: RequestState<BanPersonResponse>) {
+     // Maybe not necessary
+     if (banRes.state == "success") {
+       this.setState(s => {
+         if (
+           s.postRes.state == "success" &&
+           s.postRes.data.post_view.creator.id ==
+             banRes.data.person_view.person.id
+         ) {
+           s.postRes.data.post_view.creator.banned = banRes.data.banned;
          }
+         if (s.commentsRes.state == "success") {
+           s.commentsRes.data.comments
+             .filter(c => c.creator.id == banRes.data.person_view.person.id)
+             .forEach(c => (c.creator.banned = banRes.data.banned));
+         }
+         return s;
+       });
+     }
+   }
+   updateCommunity(communityRes: RequestState<CommunityResponse>) {
+     this.setState(s => {
+       if (s.postRes.state == "success" && communityRes.state == "success") {
+         s.postRes.data.community_view = communityRes.data.community_view;
        }
+       return s;
+     });
+   }
  
-       this.state.commentsRes?.comments
-         .filter(c => c.creator.id == data.person_view.person.id)
-         .forEach(c => (c.creator_banned_from_community = data.banned));
-       this.setState(this.state);
-     } else if (op == UserOperation.AddModToCommunity) {
-       let data = wsJsonToRes<AddModToCommunityResponse>(msg);
-       let res = this.state.postRes;
-       if (res) {
-         res.moderators = data.moderators;
-         this.setState(this.state);
+   updateCommunityFull(res: RequestState<GetCommunityResponse>) {
+     this.setState(s => {
+       if (s.postRes.state == "success" && res.state == "success") {
+         s.postRes.data.community_view = res.data.community_view;
+         s.postRes.data.moderators = res.data.moderators;
        }
-     } else if (op == UserOperation.BanPerson) {
-       let data = wsJsonToRes<BanPersonResponse>(msg);
-       this.state.commentsRes?.comments
-         .filter(c => c.creator.id == data.person_view.person.id)
-         .forEach(c => (c.creator.banned = data.banned));
-       let res = this.state.postRes;
-       if (res) {
-         if (res.post_view.creator.id == data.person_view.person.id) {
-           res.post_view.creator.banned = data.banned;
-         }
+       return s;
+     });
+   }
+   updatePost(post: RequestState<PostResponse>) {
+     this.setState(s => {
+       if (s.postRes.state == "success" && post.state == "success") {
+         s.postRes.data.post_view = post.data.post_view;
        }
-       this.setState(this.state);
-     } else if (op == UserOperation.AddAdmin) {
-       let data = wsJsonToRes<AddAdminResponse>(msg);
-       this.setState(s => ((s.siteRes.admins = data.admins), s));
-     } else if (op == UserOperation.Search) {
-       let data = wsJsonToRes<SearchResponse>(msg);
-       let xPosts = data.posts.filter(
-         p => p.post.ap_id != this.state.postRes?.post_view.post.ap_id
-       );
-       this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
-     } else if (op == UserOperation.LeaveAdmin) {
-       let data = wsJsonToRes<GetSiteResponse>(msg);
-       this.setState({ siteRes: data });
-     } else if (op == UserOperation.TransferCommunity) {
-       let data = wsJsonToRes<GetCommunityResponse>(msg);
-       let res = this.state.postRes;
-       if (res) {
-         res.community_view = data.community_view;
-         res.post_view.community = data.community_view.community;
-         res.moderators = data.moderators;
-         this.setState(this.state);
+       return s;
+     });
+   }
+   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+     if (purgeRes.state == "success") {
+       toast(i18n.t("purge_success"));
+       this.context.router.history.push(`/`);
+     }
+   }
+   createAndUpdateComments(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state === "success" && res.state === "success") {
+         s.commentsRes.data.comments.unshift(res.data.comment_view);
+         // Set finished for the parent
+         s.finished.set(
+           getCommentParentId(res.data.comment_view.comment) ?? 0,
+           true
+         );
        }
-     } else if (op == UserOperation.BlockPerson) {
-       let data = wsJsonToRes<BlockPersonResponse>(msg);
-       updatePersonBlock(data);
-     } else if (op == UserOperation.CreatePostReport) {
-       let data = wsJsonToRes<PostReportResponse>(msg);
-       if (data) {
-         toast(i18n.t("report_created"));
+       return s;
+     });
+   }
+   findAndUpdateComment(res: RequestState<CommentResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editComment(
+           res.data.comment_view,
+           s.commentsRes.data.comments
+         );
+         s.finished.set(res.data.comment_view.comment.id, true);
        }
-     } else if (op == UserOperation.CreateCommentReport) {
-       let data = wsJsonToRes<CommentReportResponse>(msg);
-       if (data) {
-         toast(i18n.t("report_created"));
+       return s;
+     });
+   }
+   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+     this.setState(s => {
+       if (s.commentsRes.state == "success" && res.state == "success") {
+         s.commentsRes.data.comments = editWith(
+           res.data.comment_reply_view,
+           s.commentsRes.data.comments
+         );
        }
-     } else if (
-       op == UserOperation.PurgePerson ||
-       op == UserOperation.PurgePost ||
-       op == UserOperation.PurgeComment ||
-       op == UserOperation.PurgeCommunity
-     ) {
-       let data = wsJsonToRes<PurgeItemResponse>(msg);
-       if (data.success) {
-         toast(i18n.t("purge_success"));
-         this.context.router.history.push(`/`);
+       return s;
+     });
+   }
+   updateModerators(res: RequestState<AddModToCommunityResponse>) {
+     // Update the moderators
+     this.setState(s => {
+       if (s.postRes.state == "success" && res.state == "success") {
+         s.postRes.data.moderators = res.data.moderators;
        }
-     }
+       return s;
+     });
    }
  }
index c897c44e6bd8c8927acccd2183339af4ebf0087f,817cfd880d001181958b56edce97c340af245040..5b9eb9819840a20d54850d4d08d123b774484988
@@@ -1,51 -1,41 +1,46 @@@
  import { Component } from "inferno";
  import {
+   CreatePrivateMessage as CreatePrivateMessageI,
    GetPersonDetails,
    GetPersonDetailsResponse,
    GetSiteResponse,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../../i18next";
  import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
  import {
-   WithPromiseKeys,
++  RouteDataResponse,
    getRecipientIdFromProps,
-   isBrowser,
    myAuth,
    setIsoData,
    toast,
-   wsClient,
-   wsSubscribe,
  } from "../../utils";
  import { HtmlTags } from "../common/html-tags";
  import { Spinner } from "../common/icon";
  import { PrivateMessageForm } from "./private-message-form";
  
- interface CreatePrivateMessageData {
++type CreatePrivateMessageData = RouteDataResponse<{
 +  recipientDetailsResponse: GetPersonDetailsResponse;
- }
++}>;
 +
  interface CreatePrivateMessageState {
    siteRes: GetSiteResponse;
-   recipientDetailsRes?: GetPersonDetailsResponse;
-   recipient_id: number;
-   loading: boolean;
+   recipientRes: RequestState<GetPersonDetailsResponse>;
+   recipientId: number;
+   isIsomorphic: boolean;
  }
  
  export class CreatePrivateMessage extends Component<
    any,
    CreatePrivateMessageState
  > {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<CreatePrivateMessageData>(this.context);
-   private subscription?: Subscription;
    state: CreatePrivateMessageState = {
      siteRes: this.isoData.site_res,
-     recipient_id: getRecipientIdFromProps(this.props),
-     loading: true,
+     recipientRes: { state: "empty" },
+     recipientId: getRecipientIdFromProps(this.props),
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
      this.handlePrivateMessageCreate =
        this.handlePrivateMessageCreate.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
        this.state = {
          ...this.state,
-         recipientDetailsRes: this.isoData.routeData.recipientDetailsResponse,
-         loading: false,
 -        recipientRes: this.isoData.routeData[0],
++        recipientRes: this.isoData.routeData.recipientDetailsResponse,
+         isIsomorphic: true,
        };
-     } else {
-       this.fetchPersonDetails();
      }
    }
  
-   fetchPersonDetails() {
-     let form: GetPersonDetails = {
-       person_id: this.state.recipient_id,
-       sort: "New",
-       saved_only: false,
-       auth: myAuth(false),
-     };
-     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       await this.fetchPersonDetails();
+     }
    }
  
-   static fetchInitialData(
-     req: InitialFetchRequest
-   ): WithPromiseKeys<CreatePrivateMessageData> {
-     const person_id = Number(req.path.split("/").pop());
++  static async fetchInitialData({
++    client,
++    path,
++    auth,
++  }: InitialFetchRequest): Promise<CreatePrivateMessageData> {
++    const person_id = Number(path.split("/").pop());
 +
 +    const form: GetPersonDetails = {
 +      person_id,
 +      sort: "New",
 +      saved_only: false,
-       auth: req.auth,
++      auth,
 +    };
 +
 +    return {
-       recipientDetailsResponse: req.client.getPersonDetails(form),
++      recipientDetailsResponse: await client.getPersonDetails(form),
 +    };
 +  }
 +
+   async fetchPersonDetails() {
+     this.setState({
+       recipientRes: { state: "loading" },
+     });
+     this.setState({
+       recipientRes: await HttpService.client.getPersonDetails({
+         person_id: this.state.recipientId,
+         sort: "New",
+         saved_only: false,
+         auth: myAuth(),
+       }),
+     });
+   }
 -  static fetchInitialData(
 -    req: InitialFetchRequest
 -  ): Promise<RequestState<any>>[] {
 -    const person_id = Number(req.path.split("/").pop());
 -    const form: GetPersonDetails = {
 -      person_id,
 -      sort: "New",
 -      saved_only: false,
 -      auth: req.auth,
 -    };
 -    return [req.client.getPersonDetails(form)];
 -  }
 -
    get documentTitle(): string {
-     let name_ = this.state.recipientDetailsRes?.person_view.person.name;
-     return name_ ? `${i18n.t("create_private_message")} - ${name_}` : "";
+     if (this.state.recipientRes.state == "success") {
+       const name_ = this.state.recipientRes.data.person_view.person.name;
+       return `${i18n.t("create_private_message")} - ${name_}`;
+     } else {
+       return "";
+     }
    }
  
-   componentWillUnmount() {
-     if (isBrowser()) {
-       this.subscription?.unsubscribe();
+   renderRecipientRes() {
+     switch (this.state.recipientRes.state) {
+       case "loading":
+         return (
+           <h5>
+             <Spinner large />
+           </h5>
+         );
+       case "success": {
+         const res = this.state.recipientRes.data;
+         return (
+           <div className="row">
+             <div className="col-12 col-lg-6 offset-lg-3 mb-4">
+               <h5>{i18n.t("create_private_message")}</h5>
+               <PrivateMessageForm
+                 onCreate={this.handlePrivateMessageCreate}
+                 recipient={res.person_view.person}
+               />
+             </div>
+           </div>
+         );
+       }
      }
    }
  
    render() {
-     let res = this.state.recipientDetailsRes;
      return (
        <div className="container-lg">
          <HtmlTags
            title={this.documentTitle}
            path={this.context.router.route.match.url}
          />
-         {this.state.loading ? (
-           <h5>
-             <Spinner large />
-           </h5>
-         ) : (
-           res && (
-             <div className="row">
-               <div className="col-12 col-lg-6 offset-lg-3 mb-4">
-                 <h5>{i18n.t("create_private_message")}</h5>
-                 <PrivateMessageForm
-                   onCreate={this.handlePrivateMessageCreate}
-                   recipient={res.person_view.person}
-                 />
-               </div>
-             </div>
-           )
-         )}
+         {this.renderRecipientRes()}
        </div>
      );
    }
  
-   handlePrivateMessageCreate() {
-     toast(i18n.t("message_sent"));
+   async handlePrivateMessageCreate(form: CreatePrivateMessageI) {
+     const res = await HttpService.client.createPrivateMessage(form);
  
-     // Navigate to the front
-     this.context.router.history.push("/");
-   }
+     if (res.state == "success") {
+       toast(i18n.t("message_sent"));
  
-   parseMessage(msg: any) {
-     let op = wsUserOp(msg);
-     console.log(msg);
-     if (msg.error) {
-       toast(i18n.t(msg.error), "danger");
-       this.setState({ loading: false });
-       return;
-     } else if (op == UserOperation.GetPersonDetails) {
-       let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
-       this.setState({ recipientDetailsRes: data, loading: false });
+       // Navigate to the front
+       this.context.router.history.push("/");
      }
    }
  }
index c62c7a98413542d7220ee7c875579927bb5ad332,8097dbde433b7ac7ef3811e47cc44cd6d4ffe76b..9f466730a8c952cd5cf03fcefb3efe44a1f3e1d1
@@@ -1,7 -1,6 +1,6 @@@
  import type { NoOptionI18nKeys } from "i18next";
  import { Component, linkEvent } from "inferno";
  import {
-   CommentResponse,
    CommentView,
    CommunityView,
    GetCommunity,
@@@ -13,7 -12,6 +12,6 @@@
    ListCommunitiesResponse,
    ListingType,
    PersonView,
-   PostResponse,
    PostView,
    ResolveObject,
    ResolveObjectResponse,
    SearchResponse,
    SearchType,
    SortType,
-   UserOperation,
-   wsJsonToRes,
-   wsUserOp,
  } from "lemmy-js-client";
- import { Subscription } from "rxjs";
  import { i18n } from "../i18next";
  import { CommentViewType, InitialFetchRequest } from "../interfaces";
- import { WebSocketService } from "../services";
+ import { FirstLoadService } from "../services/FirstLoadService";
+ import { HttpService, RequestState } from "../services/HttpService";
  import {
    Choice,
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    capitalizeFirstLetter,
    commentsToFlatNodes,
    communityToChoice,
-   createCommentLikeRes,
-   createPostLikeFindRes,
    debounce,
    enableDownvotes,
    enableNsfw,
@@@ -56,9 -48,6 +49,6 @@@
    saveScrollPosition,
    setIsoData,
    showLocal,
-   toast,
-   wsClient,
-   wsSubscribe,
  } from "../utils";
  import { CommentNodes } from "./comment/comment-nodes";
  import { HtmlTags } from "./common/html-tags";
@@@ -81,28 -70,21 +71,29 @@@ interface SearchProps 
    page: number;
  }
  
- interface SearchData {
++type SearchData = RouteDataResponse<{
 +  communityResponse?: GetCommunityResponse;
 +  listCommunitiesResponse?: ListCommunitiesResponse;
 +  creatorDetailsResponse?: GetPersonDetailsResponse;
 +  searchResponse?: SearchResponse;
 +  resolveObjectResponse?: ResolveObjectResponse;
- }
++}>;
 +
  type FilterType = "creator" | "community";
  
  interface SearchState {
-   searchResponse?: SearchResponse;
-   communities: CommunityView[];
-   creatorDetails?: GetPersonDetailsResponse;
-   searchLoading: boolean;
-   searchCommunitiesLoading: boolean;
-   searchCreatorLoading: boolean;
+   searchRes: RequestState<SearchResponse>;
+   resolveObjectRes: RequestState<ResolveObjectResponse>;
+   creatorDetailsRes: RequestState<GetPersonDetailsResponse>;
+   communitiesRes: RequestState<ListCommunitiesResponse>;
+   communityRes: RequestState<GetCommunityResponse>;
    siteRes: GetSiteResponse;
    searchText?: string;
-   resolveObjectResponse?: ResolveObjectResponse;
    communitySearchOptions: Choice[];
    creatorSearchOptions: Choice[];
+   searchCreatorLoading: boolean;
+   searchCommunitiesLoading: boolean;
+   isIsomorphic: boolean;
  }
  
  interface Combined {
@@@ -246,16 -228,19 +237,20 @@@ function getListing
  }
  
  export class Search extends Component<any, SearchState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<SearchData>(this.context);
-   private subscription?: Subscription;
++
    state: SearchState = {
-     searchLoading: false,
+     resolveObjectRes: { state: "empty" },
+     creatorDetailsRes: { state: "empty" },
+     communitiesRes: { state: "empty" },
+     communityRes: { state: "empty" },
      siteRes: this.isoData.site_res,
-     communities: [],
-     searchCommunitiesLoading: false,
-     searchCreatorLoading: false,
      creatorSearchOptions: [],
      communitySearchOptions: [],
+     searchRes: { state: "empty" },
+     searchCreatorLoading: false,
+     searchCommunitiesLoading: false,
+     isIsomorphic: false,
    };
  
    constructor(props: any, context: any) {
        this.handleCommunityFilterChange.bind(this);
      this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
  
-     this.parseMessage = this.parseMessage.bind(this);
-     this.subscription = wsSubscribe(this.parseMessage);
      const { q } = getSearchQueryParams();
  
      this.state = {
      };
  
      // Only fetch the data if coming from another route
-     if (this.isoData.path === this.context.router.route.match.url) {
+     if (FirstLoadService.isFirstLoad) {
 -      const [
 -        communityRes,
 -        communitiesRes,
 -        creatorDetailsRes,
 -        searchRes,
 -        resolveObjectRes,
 -      ] = this.isoData.routeData;
 +      const {
-         communityResponse,
-         creatorDetailsResponse,
-         listCommunitiesResponse,
-         resolveObjectResponse,
-         searchResponse,
++        communityResponse: communityRes,
++        creatorDetailsResponse: creatorDetailsRes,
++        listCommunitiesResponse: communitiesRes,
++        resolveObjectResponse: resolveObjectRes,
++        searchResponse: searchRes,
 +      } = this.isoData.routeData;
  
-       // This can be single or multiple communities given
-       if (listCommunitiesResponse) {
+       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,
-           communities: listCommunitiesResponse.communities,
++          creatorSearchOptions:
++            creatorDetailsRes?.state === "success"
++              ? [personToChoice(creatorDetailsRes.data.person_view)]
++              : [],
++          creatorDetailsRes,
 +        };
 +      }
-       if (communityResponse) {
++
++      if (communitiesRes?.state === "success") {
          this.state = {
            ...this.state,
-           communities: [communityResponse.community_view],
--          communitySearchOptions: [
-             communityToChoice(communityResponse.community_view),
 -            communityToChoice(communityRes.data.community_view),
--          ],
++          communitiesRes,
          };
        }
  
-       this.state = {
-         ...this.state,
-         creatorDetails: creatorDetailsResponse,
-         creatorSearchOptions: creatorDetailsResponse
-           ? [personToChoice(creatorDetailsResponse.person_view)]
-           : [],
-       };
 -      if (q) {
++      if (communityRes?.state === "success") {
+         this.state = {
+           ...this.state,
 -          searchRes,
 -          resolveObjectRes,
++          communityRes,
+         };
+       }
 +
 +      if (q !== "") {
 +        this.state = {
 +          ...this.state,
-           searchResponse,
-           resolveObjectResponse,
-           searchLoading: false,
 +        };
-       } else {
-         this.search();
-       }
-     } else {
-       const listCommunitiesForm: ListCommunities = {
-         type_: defaultListingType,
-         sort: defaultSortType,
-         limit: fetchLimit,
-         auth: myAuth(false),
-       };
 +
-       WebSocketService.Instance.send(
-         wsClient.listCommunities(listCommunitiesForm)
-       );
++        if (searchRes?.state === "success") {
++          this.state = {
++            ...this.state,
++            searchRes,
++          };
++        }
 +
-       if (q) {
-         this.search();
++        if (resolveObjectRes?.state === "success") {
++          this.state = {
++            ...this.state,
++            resolveObjectRes,
++          };
++        }
 +      }
      }
    }
  
+   async componentDidMount() {
+     if (!this.state.isIsomorphic) {
+       const promises = [this.fetchCommunities()];
+       if (this.state.searchText) {
+         promises.push(this.search());
+       }
+       await Promise.all(promises);
+     }
+   }
+   async fetchCommunities() {
+     this.setState({ communitiesRes: { state: "loading" } });
+     this.setState({
+       communitiesRes: await HttpService.client.listCommunities({
+         type_: defaultListingType,
+         sort: defaultSortType,
+         limit: fetchLimit,
+         auth: myAuth(),
+       }),
+     });
+   }
    componentWillUnmount() {
-     this.subscription?.unsubscribe();
      saveScrollPosition(this.context);
    }
  
--  static fetchInitialData({
++  static async fetchInitialData({
      client,
      auth,
      query: { communityId, creatorId, q, type, sort, listingType, page },
-   }: InitialFetchRequest<
-     QueryParams<SearchProps>
-   >): WithPromiseKeys<SearchData> {
 -  }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
 -    RequestState<any>
 -  >[] {
 -    const promises: Promise<RequestState<any>>[] = [];
 -
++  }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
      const community_id = getIdFromString(communityId);
-     let communityResponse: Promise<GetCommunityResponse> | undefined =
-       undefined;
-     let listCommunitiesResponse: Promise<ListCommunitiesResponse> | undefined =
++    let communityResponse: RequestState<GetCommunityResponse> | undefined =
 +      undefined;
++    let listCommunitiesResponse:
++      | RequestState<ListCommunitiesResponse>
++      | undefined = undefined;
      if (community_id) {
        const getCommunityForm: GetCommunity = {
          id: community_id,
          auth,
        };
 -      promises.push(client.getCommunity(getCommunityForm));
 -      promises.push(Promise.resolve({ state: "empty" }));
 +
-       communityResponse = client.getCommunity(getCommunityForm);
++      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 = client.listCommunities(listCommunitiesForm);
++      listCommunitiesResponse = await client.listCommunities(
++        listCommunitiesForm
++      );
      }
  
      const creator_id = getIdFromString(creatorId);
-     let creatorDetailsResponse: Promise<GetPersonDetailsResponse> | undefined =
-       undefined;
++    let creatorDetailsResponse:
++      | RequestState<GetPersonDetailsResponse>
++      | undefined = undefined;
      if (creator_id) {
        const getCreatorForm: GetPersonDetails = {
          person_id: creator_id,
          auth,
        };
 -      promises.push(client.getPersonDetails(getCreatorForm));
 -    } else {
 -      promises.push(Promise.resolve({ state: "empty" }));
 +
-       creatorDetailsResponse = client.getPersonDetails(getCreatorForm);
++      creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
      }
  
      const query = getSearchQueryFromQuery(q);
  
-     let searchResponse: Promise<SearchResponse> | undefined = undefined;
-     let resolveObjectResponse:
-       | Promise<ResolveObjectResponse | undefined>
-       | undefined = undefined;
++    let searchResponse: RequestState<SearchResponse> | undefined = undefined;
++    let resolveObjectResponse: RequestState<ResolveObjectResponse> | undefined =
++      undefined;
 +
      if (query) {
        const form: SearchForm = {
          q: query,
        };
  
        if (query !== "") {
-         searchResponse = client.search(form);
 -        promises.push(client.search(form));
++        searchResponse = await client.search(form);
          if (auth) {
            const resolveObjectForm: ResolveObject = {
              q: query,
              auth,
            };
-           resolveObjectResponse = client
 -          promises.push(client.resolveObject(resolveObjectForm));
++          resolveObjectResponse = await client
 +            .resolveObject(resolveObjectForm)
 +            .catch(() => undefined);
          }
 -      } else {
 -        promises.push(Promise.resolve({ state: "empty" }));
 -        promises.push(Promise.resolve({ state: "empty" }));
        }
      }
  
 -    return promises;
 +    return {
 +      communityResponse,
 +      creatorDetailsResponse,
 +      listCommunitiesResponse,
 +      resolveObjectResponse,
 +      searchResponse,
 +    };
    }
  
    get documentTitle(): string {
          {this.selects}
          {this.searchForm}
          {this.displayResults(type)}
-         {this.resultsCount === 0 && !this.state.searchLoading && (
-           <span>{i18n.t("no_results")}</span>
-         )}
+         {this.resultsCount === 0 &&
+           this.state.searchRes.state === "success" && (
+             <span>{i18n.t("no_results")}</span>
+           )}
          <Paginator page={page} onChange={this.handlePageChange} />
        </div>
      );
            minLength={1}
          />
          <button type="submit" className="btn btn-secondary mr-2 mb-2">
-           {this.state.searchLoading ? (
 -          {this.state.searchRes.state == "loading" ? (
++          {this.state.searchRes.state === "loading" ? (
              <Spinner />
            ) : (
              <span>{i18n.t("search")}</span>
        creatorSearchOptions,
        searchCommunitiesLoading,
        searchCreatorLoading,
+       communitiesRes,
      } = this.state;
  
+     const hasCommunities =
+       communitiesRes.state == "success" &&
+       communitiesRes.data.communities.length > 0;
      return (
        <div className="mb-2">
          <select
            />
          </span>
          <div className="form-row">
-           {this.state.communities.length > 0 && (
+           {hasCommunities && (
              <Filter
                filterType="community"
                onChange={this.handleCommunityFilterChange}
                onSearch={this.handleCommunitySearch}
                options={communitySearchOptions}
-               loading={searchCommunitiesLoading}
                value={communityId}
+               loading={searchCommunitiesLoading}
              />
            )}
            <Filter
              onChange={this.handleCreatorFilterChange}
              onSearch={this.handleCreatorSearch}
              options={creatorSearchOptions}
-             loading={searchCreatorLoading}
              value={creatorId}
+             loading={searchCreatorLoading}
            />
          </div>
        </div>
  
    buildCombined(): Combined[] {
      const combined: Combined[] = [];
-     const { resolveObjectResponse, searchResponse } = this.state;
+     const {
+       resolveObjectRes: resolveObjectResponse,
+       searchRes: searchResponse,
+     } = this.state;
  
      // Push the possible resolve / federated objects first
-     if (resolveObjectResponse) {
-       const { comment, post, community, person } = resolveObjectResponse;
+     if (resolveObjectResponse.state == "success") {
+       const { comment, post, community, person } = resolveObjectResponse.data;
  
        if (comment) {
          combined.push(commentViewToCombined(comment));
      }
  
      // Push the search results
-     if (searchResponse) {
-       const { comments, posts, communities, users } = searchResponse;
+     if (searchResponse.state === "success") {
+       const { comments, posts, communities, users } = searchResponse.data;
  
        combined.push(
          ...[
                    allLanguages={this.state.siteRes.all_languages}
                    siteLanguages={this.state.siteRes.discussion_languages}
                    viewOnly
+                   // All of these are unused, since its view only
+                   onPostEdit={() => {}}
+                   onPostVote={() => {}}
+                   onPostReport={() => {}}
+                   onBlockPerson={() => {}}
+                   onLockPost={() => {}}
+                   onDeletePost={() => {}}
+                   onRemovePost={() => {}}
+                   onSavePost={() => {}}
+                   onFeaturePost={() => {}}
+                   onPurgePerson={() => {}}
+                   onPurgePost={() => {}}
+                   onBanPersonFromCommunity={() => {}}
+                   onBanPerson={() => {}}
+                   onAddModToCommunity={() => {}}
+                   onAddAdmin={() => {}}
+                   onTransferCommunity={() => {}}
                  />
                )}
                {i.type_ === "comments" && (
                    enableDownvotes={enableDownvotes(this.state.siteRes)}
                    allLanguages={this.state.siteRes.all_languages}
                    siteLanguages={this.state.siteRes.discussion_languages}
+                   // All of these are unused, since its viewonly
+                   finished={new Map()}
+                   onSaveComment={() => {}}
+                   onBlockPerson={() => {}}
+                   onDeleteComment={() => {}}
+                   onRemoveComment={() => {}}
+                   onCommentVote={() => {}}
+                   onCommentReport={() => {}}
+                   onDistinguishComment={() => {}}
+                   onAddModToCommunity={() => {}}
+                   onAddAdmin={() => {}}
+                   onTransferCommunity={() => {}}
+                   onPurgeComment={() => {}}
+                   onPurgePerson={() => {}}
+                   onCommentReplyRead={() => {}}
+                   onPersonMentionRead={() => {}}
+                   onBanPersonFromCommunity={() => {}}
+                   onBanPerson={() => {}}
+                   onCreateComment={() => Promise.resolve({ state: "empty" })}
+                   onEditComment={() => Promise.resolve({ state: "empty" })}
                  />
                )}
                {i.type_ === "communities" && (
    }
  
    get comments() {
-     const { searchResponse, resolveObjectResponse, siteRes } = this.state;
-     const comments = searchResponse?.comments ?? [];
-     if (resolveObjectResponse?.comment) {
-       comments.unshift(resolveObjectResponse?.comment);
+     const {
+       searchRes: searchResponse,
+       resolveObjectRes: resolveObjectResponse,
+       siteRes,
+     } = this.state;
+     const comments =
+       searchResponse.state === "success" ? searchResponse.data.comments : [];
+     if (
+       resolveObjectResponse.state === "success" &&
+       resolveObjectResponse.data.comment
+     ) {
+       comments.unshift(resolveObjectResponse.data.comment);
      }
  
      return (
          enableDownvotes={enableDownvotes(siteRes)}
          allLanguages={siteRes.all_languages}
          siteLanguages={siteRes.discussion_languages}
+         // All of these are unused, since its viewonly
+         finished={new Map()}
+         onSaveComment={() => {}}
+         onBlockPerson={() => {}}
+         onDeleteComment={() => {}}
+         onRemoveComment={() => {}}
+         onCommentVote={() => {}}
+         onCommentReport={() => {}}
+         onDistinguishComment={() => {}}
+         onAddModToCommunity={() => {}}
+         onAddAdmin={() => {}}
+         onTransferCommunity={() => {}}
+         onPurgeComment={() => {}}
+         onPurgePerson={() => {}}
+         onCommentReplyRead={() => {}}
+         onPersonMentionRead={() => {}}
+         onBanPersonFromCommunity={() => {}}
+         onBanPerson={() => {}}
+         onCreateComment={() => Promise.resolve({ state: "empty" })}
+         onEditComment={() => Promise.resolve({ state: "empty" })}
        />
      );
    }
  
    get posts() {
-     const { searchResponse, resolveObjectResponse, siteRes } = this.state;
-     const posts = searchResponse?.posts ?? [];
-     if (resolveObjectResponse?.post) {
-       posts.unshift(resolveObjectResponse.post);
+     const {
+       searchRes: searchResponse,
+       resolveObjectRes: resolveObjectResponse,
+       siteRes,
+     } = this.state;
+     const posts =
+       searchResponse.state === "success" ? searchResponse.data.posts : [];
+     if (
+       resolveObjectResponse.state === "success" &&
+       resolveObjectResponse.data.post
+     ) {
+       posts.unshift(resolveObjectResponse.data.post);
      }
  
      return (
                  allLanguages={siteRes.all_languages}
                  siteLanguages={siteRes.discussion_languages}
                  viewOnly
+                 // All of these are unused, since its view only
+                 onPostEdit={() => {}}
+                 onPostVote={() => {}}
+                 onPostReport={() => {}}
+                 onBlockPerson={() => {}}
+                 onLockPost={() => {}}
+                 onDeletePost={() => {}}
+                 onRemovePost={() => {}}
+                 onSavePost={() => {}}
+                 onFeaturePost={() => {}}
+                 onPurgePerson={() => {}}
+                 onPurgePost={() => {}}
+                 onBanPersonFromCommunity={() => {}}
+                 onBanPerson={() => {}}
+                 onAddModToCommunity={() => {}}
+                 onAddAdmin={() => {}}
+                 onTransferCommunity={() => {}}
                />
              </div>
            </div>
    }
  
    get communities() {
-     const { searchResponse, resolveObjectResponse } = this.state;
-     const communities = searchResponse?.communities ?? [];
-     if (resolveObjectResponse?.community) {
-       communities.unshift(resolveObjectResponse.community);
+     const {
+       searchRes: searchResponse,
+       resolveObjectRes: resolveObjectResponse,
+     } = this.state;
+     const communities =
+       searchResponse.state === "success" ? searchResponse.data.communities : [];
+     if (
+       resolveObjectResponse.state === "success" &&
+       resolveObjectResponse.data.community
+     ) {
+       communities.unshift(resolveObjectResponse.data.community);
      }
  
      return (
    }
  
    get users() {
-     const { searchResponse, resolveObjectResponse } = this.state;
-     const users = searchResponse?.users ?? [];
-     if (resolveObjectResponse?.person) {
-       users.unshift(resolveObjectResponse.person);
+     const {
+       searchRes: searchResponse,
+       resolveObjectRes: resolveObjectResponse,
+     } = this.state;
+     const users =
+       searchResponse.state === "success" ? searchResponse.data.users : [];
+     if (
+       resolveObjectResponse.state === "success" &&
+       resolveObjectResponse.data.person
+     ) {
+       users.unshift(resolveObjectResponse.data.person);
      }
  
      return (
    }
  
    get resultsCount(): number {
-     const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
-     const searchCount = r
-       ? r.posts.length +
-         r.comments.length +
-         r.communities.length +
-         r.users.length
-       : 0;
-     const resObjCount = resolveRes
-       ? resolveRes.post ||
-         resolveRes.person ||
-         resolveRes.community ||
-         resolveRes.comment
-         ? 1
-         : 0
-       : 0;
+     const { searchRes: r, resolveObjectRes: resolveRes } = this.state;
+     const searchCount =
+       r.state === "success"
+         ? r.data.posts.length +
+           r.data.comments.length +
+           r.data.communities.length +
+           r.data.users.length
+         : 0;
+     const resObjCount =
+       resolveRes.state === "success"
+         ? resolveRes.data.post ||
+           resolveRes.data.person ||
+           resolveRes.data.community ||
+           resolveRes.data.comment
+           ? 1
+           : 0
+         : 0;
  
      return resObjCount + searchCount;
    }
  
-   search() {
-     const auth = myAuth(false);
+   async search() {
+     const auth = myAuth();
      const { searchText: q } = this.state;
      const { communityId, creatorId, type, sort, listingType, page } =
        getSearchQueryParams();
  
-     if (q && q !== "") {
-       const form: SearchForm = {
-         q,
-         community_id: communityId ?? undefined,
-         creator_id: creatorId ?? undefined,
-         type_: type,
-         sort,
-         listing_type: listingType,
-         page,
-         limit: fetchLimit,
-         auth,
-       };
-       if (auth) {
-         const resolveObjectForm: ResolveObject = {
+     if (q) {
+       this.setState({ searchRes: { state: "loading" } });
+       this.setState({
+         searchRes: await HttpService.client.search({
            q,
+           community_id: communityId ?? undefined,
+           creator_id: creatorId ?? undefined,
+           type_: type,
+           sort,
+           listing_type: listingType,
+           page,
+           limit: fetchLimit,
            auth,
-         };
-         WebSocketService.Instance.send(
-           wsClient.resolveObject(resolveObjectForm)
-         );
-       }
-       this.setState({
-         searchResponse: undefined,
-         resolveObjectResponse: undefined,
-         searchLoading: true,
+         }),
        });
+       window.scrollTo(0, 0);
+       restoreScrollPosition(this.context);
  
-       WebSocketService.Instance.send(wsClient.search(form));
+       if (auth) {
+         this.setState({ resolveObjectRes: { state: "loading" } });
+         this.setState({
+           resolveObjectRes: await HttpService.client.resolveObject({
+             q,
+             auth,
+           }),
+         });
+       }
      }
    }
  
    handleCreatorSearch = debounce(async (text: string) => {
      const { creatorId } = getSearchQueryParams();
      const { creatorSearchOptions } = this.state;
-     this.setState({
-       searchCreatorLoading: true,
-     });
      const newOptions: Choice[] = [];
  
+     this.setState({ searchCreatorLoading: true });
      const selectedChoice = creatorSearchOptions.find(
        choice => getIdFromString(choice.value) === creatorId
      );
      }
  
      if (text.length > 0) {
-       newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
+       newOptions.push(...(await fetchUsers(text)).map(personToChoice));
      }
  
      this.setState({
      }
  
      if (text.length > 0) {
-       newOptions.push(
-         ...(await fetchCommunities(text)).communities.map(communityToChoice)
-       );
+       newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
      }
  
      this.setState({
      i.setState({ searchText: event.target.value });
    }
  
-   updateUrl({
+   async updateUrl({
      q,
      type,
      listingType,
  
      this.props.history.push(`/search${getQueryString(queryParams)}`);
  
-     this.search();
-   }
-   parseMessage(msg: any) {
-     console.log(msg);
-     const op = wsUserOp(msg);
-     if (msg.error) {
-       if (msg.error === "couldnt_find_object") {
-         this.setState({
-           resolveObjectResponse: {},
-         });
-         this.checkFinishedLoading();
-       } else {
-         toast(i18n.t(msg.error), "danger");
-       }
-     } else {
-       switch (op) {
-         case UserOperation.Search: {
-           const searchResponse = wsJsonToRes<SearchResponse>(msg);
-           this.setState({ searchResponse });
-           window.scrollTo(0, 0);
-           this.checkFinishedLoading();
-           restoreScrollPosition(this.context);
-           break;
-         }
-         case UserOperation.CreateCommentLike: {
-           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
-           createCommentLikeRes(
-             comment_view,
-             this.state.searchResponse?.comments
-           );
-           break;
-         }
-         case UserOperation.CreatePostLike: {
-           const { post_view } = wsJsonToRes<PostResponse>(msg);
-           createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
-           break;
-         }
-         case UserOperation.ListCommunities: {
-           const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
-           this.setState({ communities });
-           break;
-         }
-         case UserOperation.ResolveObject: {
-           const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
-           this.setState({ resolveObjectResponse });
-           this.checkFinishedLoading();
-           break;
-         }
-       }
-     }
-   }
-   checkFinishedLoading() {
-     if (this.state.searchResponse || this.state.resolveObjectResponse) {
-       this.setState({ searchLoading: false });
-     }
+     await this.search();
    }
  }
diff --combined src/shared/interfaces.ts
index dc4490cbdc7701f61eda5b3dd525e9143a362977,3b64f60533dc246b6fd6fe74f5dfb2387ef29fed..dbba70406b1314037691fa5b6add9b023138b76d
@@@ -1,19 -1,20 +1,22 @@@
- import { CommentView, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
+ import { CommentView, GetSiteResponse } from "lemmy-js-client";
  import type { ParsedQs } from "qs";
+ import { RequestState, WrappedLemmyHttp } from "./services/HttpService";
  import { ErrorPageData } from "./utils";
  
  /**
   * This contains serialized data, it needs to be deserialized before use.
   */
- export interface IsoData<T extends object = any> {
 -export interface IsoData {
++export interface IsoData<T extends Record<string, RequestState<any>> = any> {
    path: string;
 -  routeData: RequestState<any>[];
 +  routeData: T;
    site_res: GetSiteResponse;
    errorPageData?: ErrorPageData;
  }
  
- export type IsoDataOptionalSite<T extends object = any> = Partial<IsoData<T>> &
 -export type IsoDataOptionalSite = Partial<IsoData> &
 -  Pick<IsoData, Exclude<keyof IsoData, "site_res">>;
++export type IsoDataOptionalSite<
++  T extends Record<string, RequestState<any>> = any
++> = Partial<IsoData<T>> &
 +  Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
  
  export interface ILemmyConfig {
    wsHost?: string;
@@@ -28,7 -29,7 +31,7 @@@ declare global 
  
  export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
    auth?: string;
-   client: LemmyHttp;
+   client: WrappedLemmyHttp;
    path: string;
    query: T;
    site: GetSiteResponse;
@@@ -69,6 -70,11 +72,11 @@@ export enum PurgeType 
    Comment,
  }
  
+ export enum VoteType {
+   Upvote,
+   Downvote,
+ }
  export interface CommentNodeI {
    comment_view: CommentView;
    children: Array<CommentNodeI>;
diff --combined src/shared/routes.ts
index 81771d03372720ba5735367a4d9f70346c1e3e66,4973bec794dbfb26a34b7d20e2cd848dcab52fd7..68ac6a995f7303a22fb79286432a68bcdbaceedc
@@@ -22,14 -22,14 +22,15 @@@ import { Post } from "./components/post
  import { CreatePrivateMessage } from "./components/private_message/create-private-message";
  import { Search } from "./components/search";
  import { InitialFetchRequest } from "./interfaces";
- import { WithPromiseKeys } from "./utils";
+ import { RequestState } from "./services/HttpService";
  
- interface IRoutePropsWithFetch<T extends object> extends IRouteProps {
 -interface IRoutePropsWithFetch extends IRouteProps {
++interface IRoutePropsWithFetch<T extends Record<string, RequestState<any>>>
++  extends IRouteProps {
    // TODO Make sure this one is good.
-   fetchInitialData?(req: InitialFetchRequest): WithPromiseKeys<T>;
 -  fetchInitialData?(req: InitialFetchRequest): Promise<RequestState<any>>[];
++  fetchInitialData?(req: InitialFetchRequest): T;
  }
  
 -export const routes: IRoutePropsWithFetch[] = [
 +export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
    {
      path: `/`,
      component: Home,
index 0000000000000000000000000000000000000000,b0e476e2e55dca8c3432b7e396427c8b7f1d2ef4..cdcf11d7632a0089c9757c51c5d80abfa0a22f2a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,96 +1,96 @@@
 -type FailedRequestState = {
+ import { LemmyHttp } from "lemmy-js-client";
+ import { getHttpBase } from "../../shared/env";
+ import { i18n } from "../../shared/i18next";
+ import { toast } from "../../shared/utils";
+ type EmptyRequestState = {
+   state: "empty";
+ };
+ type LoadingRequestState = {
+   state: "loading";
+ };
 -              state: "success",
++export type FailedRequestState = {
+   state: "failed";
+   msg: string;
+ };
+ type SuccessRequestState<T> = {
+   state: "success";
+   data: T;
+ };
+ /**
+  * Shows the state of an API request.
+  *
+  * Can be empty, loading, failed, or success
+  */
+ export type RequestState<T> =
+   | EmptyRequestState
+   | LoadingRequestState
+   | FailedRequestState
+   | SuccessRequestState<T>;
+ export type WrappedLemmyHttp = {
+   [K in keyof LemmyHttp]: LemmyHttp[K] extends (...args: any[]) => any
+     ? ReturnType<LemmyHttp[K]> extends Promise<infer U>
+       ? (...args: Parameters<LemmyHttp[K]>) => Promise<RequestState<U>>
+       : (
+           ...args: Parameters<LemmyHttp[K]>
+         ) => Promise<RequestState<LemmyHttp[K]>>
+     : LemmyHttp[K];
+ };
+ class WrappedLemmyHttpClient {
+   #client: LemmyHttp;
+   constructor(client: LemmyHttp) {
+     this.#client = client;
+     for (const key of Object.getOwnPropertyNames(
+       Object.getPrototypeOf(this.#client)
+     )) {
+       if (key !== "constructor") {
+         WrappedLemmyHttpClient.prototype[key] = async (...args) => {
+           try {
+             const res = await this.#client[key](...args);
+             return {
+               data: res,
++              state: !(res === undefined || res === null) ? "success" : "empty",
+             };
+           } catch (error) {
+             console.error(`API error: ${error}`);
+             toast(i18n.t(error), "danger");
+             return {
+               state: "failed",
+               msg: error,
+             };
+           }
+         };
+       }
+     }
+   }
+ }
+ export function wrapClient(client: LemmyHttp) {
+   return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary
+ }
+ export class HttpService {
+   static #_instance: HttpService;
+   #client: WrappedLemmyHttp;
+   private constructor() {
+     this.#client = wrapClient(new LemmyHttp(getHttpBase()));
+   }
+   static get #Instance() {
+     return this.#_instance ?? (this.#_instance = new this());
+   }
+   public static get client() {
+     return this.#Instance.#client;
+   }
+ }
diff --combined src/shared/utils.ts
index fe83977db598e8921cddd7126cd1e92a2db34ef7,46e8601be08e5ff4895d6b9335f24fe765cb7168..83cc6f1adf645b4389d70b7f5367ebcb646af9f9
@@@ -3,7 -3,9 +3,9 @@@ import emojiShortName from "emoji-short
  import {
    BlockCommunityResponse,
    BlockPersonResponse,
+   CommentAggregates,
    Comment as CommentI,
+   CommentReplyView,
    CommentReportView,
    CommentSortType,
    CommentView,
@@@ -14,9 -16,9 +16,9 @@@
    GetSiteResponse,
    Language,
    LemmyHttp,
-   LemmyWebsocket,
    MyUserInfo,
    Person,
+   PersonMentionView,
    PersonView,
    PostReportView,
    PostView,
@@@ -24,8 -26,8 +26,8 @@@
    PrivateMessageView,
    RegistrationApplicationView,
    Search,
+   SearchType,
    SortType,
-   UploadImageResponse,
  } from "lemmy-js-client";
  import { default as MarkdownIt } from "markdown-it";
  import markdown_it_container from "markdown-it-container";
@@@ -37,22 -39,18 +39,19 @@@ import markdown_it_sup from "markdown-i
  import Renderer from "markdown-it/lib/renderer";
  import Token from "markdown-it/lib/token";
  import moment from "moment";
- import { Subscription } from "rxjs";
- import { delay, retryWhen, take } from "rxjs/operators";
  import tippy from "tippy.js";
  import Toastify from "toastify-js";
  import { getHttpBase } from "./env";
  import { i18n, languages } from "./i18next";
- import { CommentNodeI, DataType, IsoData } from "./interfaces";
- import { UserService, WebSocketService } from "./services";
+ import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
+ import { HttpService, UserService } from "./services";
++import { RequestState } from "./services/HttpService";
  
  let Tribute: any;
  if (isBrowser()) {
    Tribute = require("tributejs");
  }
  
- export const wsClient = new LemmyWebsocket();
  export const favIconUrl = "/static/assets/icons/favicon.svg";
  export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
  // TODO
@@@ -63,7 -61,7 +62,7 @@@ export const donateLemmyUrl = `${joinLe
  export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
  export const helpGuideUrl = `${joinLemmyUrl}/docs/en/users/01-getting-started.html`; // TODO find a way to redirect to the non-en folder
  export const markdownHelpUrl = `${joinLemmyUrl}/docs/en/users/02-media.html`;
- export const sortingHelpUrl = `${helpGuideUrl}/docs/en/users/03-votes-and-ranking.html`;
+ export const sortingHelpUrl = `${joinLemmyUrl}/docs/en/users/03-votes-and-ranking.html`;
  export const archiveTodayUrl = "https://archive.today";
  export const ghostArchiveUrl = "https://ghostarchive.org";
  export const webArchiveUrl = "https://web.archive.org";
@@@ -110,7 -108,7 +109,7 @@@ export interface ErrorPageData 
    adminMatrixIds?: string[];
  }
  
let customEmojis: EmojiMartCategory[] = [];
const customEmojis: EmojiMartCategory[] = [];
  export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
    string,
    CustomEmojiView
@@@ -192,11 -190,11 +191,11 @@@ export function hotRankPost(post_view: 
  
  export function hotRank(score: number, timeStr: string): number {
    // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
-   let date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
-   let now: Date = new Date();
-   let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
+   const date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
+   const now: Date = new Date();
+   const hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
  
-   let rank =
+   const rank =
      (10000 * Math.log10(Math.max(1, 3 + Number(score)))) /
      Math.pow(hoursElapsed + 2, 1.8);
  
@@@ -243,7 -241,7 +242,7 @@@ export function canMod
        .concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
  
    if (myUserInfo) {
-     let myIndex = adminsThenMods.findIndex(
+     const myIndex = adminsThenMods.findIndex(
        id => id == myUserInfo.local_user_view.person.id
      );
      if (myIndex == -1) {
@@@ -294,7 -292,7 +293,7 @@@ export function amCommunityCreator
    mods?: CommunityModeratorView[],
    myUserInfo = UserService.Instance.myUserInfo
  ): boolean {
-   let myId = myUserInfo?.local_user_view.person.id;
+   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;
  }
@@@ -304,7 -302,7 +303,7 @@@ export function amSiteCreator
    admins?: PersonView[],
    myUserInfo = UserService.Instance.myUserInfo
  ): boolean {
-   let myId = myUserInfo?.local_user_view.person.id;
+   const myId = myUserInfo?.local_user_view.person.id;
    return myId == admins?.at(0)?.person.id && myId != creator_id;
  }
  
@@@ -331,12 -329,12 +330,12 @@@ export function validURL(str: string) 
  }
  
  export function communityRSSUrl(actorId: string, sort: string): string {
-   let url = new URL(actorId);
+   const url = new URL(actorId);
    return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
  }
  
  export function validEmail(email: string) {
-   let re =
+   const re =
      /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
    return re.test(String(email).toLowerCase());
  }
@@@ -346,8 -344,8 +345,8 @@@ export function capitalizeFirstLetter(s
  }
  
  export async function getSiteMetadata(url: string) {
-   let form: GetSiteMetadata = { url };
-   let client = new LemmyHttp(getHttpBase());
+   const form: GetSiteMetadata = { url };
+   const client = new LemmyHttp(getHttpBase());
    return client.getSiteMetadata(form);
  }
  
@@@ -404,8 -402,8 +403,8 @@@ export function getLanguages
    override?: string,
    myUserInfo = UserService.Instance.myUserInfo
  ): string[] {
-   let myLang = myUserInfo?.local_user_view.local_user.interface_language;
-   let lang = override || myLang || "browser";
+   const myLang = myUserInfo?.local_user_view.local_user.interface_language;
+   const lang = override || myLang || "browser";
  
    if (lang == "browser" && isBrowser()) {
      return getBrowserLanguages();
  
  function getBrowserLanguages(): string[] {
    // Intersect lemmy's langs, with the browser langs
-   let langs = languages ? languages.map(l => l.code) : ["en"];
+   const langs = languages ? languages.map(l => l.code) : ["en"];
  
    // NOTE, mobile browsers seem to be missing this list, so append en
-   let allowedLangs = navigator.languages
+   const allowedLangs = navigator.languages
      .concat("en")
      .filter(v => langs.includes(v));
    return allowedLangs;
@@@ -441,11 -439,11 +440,11 @@@ export async function setTheme(theme: s
      theme = "darkly";
    }
  
-   let themeList = await fetchThemeList();
+   const themeList = await fetchThemeList();
  
    // Unload all the other themes
    for (var i = 0; i < themeList.length; i++) {
-     let styleSheet = document.getElementById(themeList[i]);
+     const styleSheet = document.getElementById(themeList[i]);
      if (styleSheet) {
        styleSheet.setAttribute("disabled", "disabled");
      }
    document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
  
    // Load the theme dynamically
-   let cssLoc = `/css/themes/${theme}.css`;
+   const cssLoc = `/css/themes/${theme}.css`;
  
    loadCss(theme, cssLoc);
    document.getElementById(theme)?.removeAttribute("disabled");
@@@ -559,86 -557,6 +558,6 @@@ export function pictrsDeleteToast(filen
    }
  }
  
- interface NotifyInfo {
-   name: string;
-   icon?: string;
-   link: string;
-   body?: string;
- }
- export function messageToastify(info: NotifyInfo, router: any) {
-   if (isBrowser()) {
-     let htmlBody = info.body ? md.render(info.body) : "";
-     let backgroundColor = `var(--light)`;
-     let toast = Toastify({
-       text: `${htmlBody}<br />${info.name}`,
-       avatar: info.icon,
-       backgroundColor: backgroundColor,
-       className: "text-dark",
-       close: true,
-       gravity: "top",
-       position: "right",
-       duration: 5000,
-       escapeMarkup: false,
-       onClick: () => {
-         if (toast) {
-           toast.hideToast();
-           router.history.push(info.link);
-         }
-       },
-     });
-     toast.showToast();
-   }
- }
- export function notifyPost(post_view: PostView, router: any) {
-   let info: NotifyInfo = {
-     name: post_view.community.name,
-     icon: post_view.community.icon,
-     link: `/post/${post_view.post.id}`,
-     body: post_view.post.name,
-   };
-   notify(info, router);
- }
- export function notifyComment(comment_view: CommentView, router: any) {
-   let info: NotifyInfo = {
-     name: comment_view.creator.name,
-     icon: comment_view.creator.avatar,
-     link: `/comment/${comment_view.comment.id}`,
-     body: comment_view.comment.content,
-   };
-   notify(info, router);
- }
- export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
-   let info: NotifyInfo = {
-     name: pmv.creator.name,
-     icon: pmv.creator.avatar,
-     link: `/inbox`,
-     body: pmv.private_message.content,
-   };
-   notify(info, router);
- }
- function notify(info: NotifyInfo, router: any) {
-   messageToastify(info, router);
-   if (Notification.permission !== "granted") Notification.requestPermission();
-   else {
-     var notification = new Notification(info.name, {
-       ...{ body: info.body },
-       ...(info.icon && { icon: info.icon }),
-     });
-     notification.onclick = (ev: Event): any => {
-       ev.preventDefault();
-       router.history.push(info.link);
-     };
-   }
- }
  export function setupTribute() {
    return new Tribute({
      noMatchTemplate: function () {
        {
          trigger: ":",
          menuItemTemplate: (item: any) => {
-           let shortName = `:${item.original.key}:`;
+           const shortName = `:${item.original.key}:`;
            return `${item.original.val} ${shortName}`;
          },
          selectTemplate: (item: any) => {
-           let customEmoji = customEmojisLookup.get(
+           const customEmoji = customEmojisLookup.get(
              item.original.key
            )?.custom_emoji;
            if (customEmoji == undefined) return `${item.original.val}`;
        {
          trigger: "@",
          selectTemplate: (item: any) => {
-           let it: PersonTribute = item.original;
+           const it: PersonTribute = item.original;
            return `[${it.key}](${it.view.person.actor_id})`;
          },
          values: debounce(async (text: string, cb: any) => {
        {
          trigger: "!",
          selectTemplate: (item: any) => {
-           let it: CommunityTribute = item.original;
+           const it: CommunityTribute = item.original;
            return `[${it.key}](${it.view.community.actor_id})`;
          },
          values: debounce(async (text: string, cb: any) => {
  }
  
  function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
-   let groupedEmojis = groupBy(custom_emoji_views, x => x.custom_emoji.category);
+   const groupedEmojis = groupBy(
+     custom_emoji_views,
+     x => x.custom_emoji.category
+   );
    for (const [category, emojis] of Object.entries(groupedEmojis)) {
      customEmojis.push({
        id: category,
@@@ -739,7 -660,7 +661,7 @@@ export function updateEmojiDataModel(cu
      keywords: custom_emoji_view.keywords.map(x => x.keyword),
      skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
    };
-   let categoryIndex = customEmojis.findIndex(
+   const categoryIndex = customEmojis.findIndex(
      x => x.id == custom_emoji_view.custom_emoji.category
    );
    if (categoryIndex == -1) {
        emojis: [emoji],
      });
    } else {
-     let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
+     const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
        x => x.id == custom_emoji_view.custom_emoji.shortcode
      );
      if (emojiIndex == -1) {
  
  export function removeFromEmojiDataModel(id: number) {
    let view: CustomEmojiView | undefined;
-   for (let item of customEmojisLookup.values()) {
+   for (const item of customEmojisLookup.values()) {
      if (item.custom_emoji.id === id) {
        view = item;
        break;
@@@ -872,15 -793,12 +794,12 @@@ interface PersonTribute 
  }
  
  async function personSearch(text: string): Promise<PersonTribute[]> {
-   let users = (await fetchUsers(text)).users;
-   let persons: PersonTribute[] = users.map(pv => {
-     let tribute: PersonTribute = {
-       key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
-       view: pv,
-     };
-     return tribute;
-   });
-   return persons;
+   const usersResponse = await fetchUsers(text);
+   return usersResponse.map(pv => ({
+     key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
+     view: pv,
+   }));
  }
  
  interface CommunityTribute {
  }
  
  async function communitySearch(text: string): Promise<CommunityTribute[]> {
-   let comms = (await fetchCommunities(text)).communities;
-   let communities: CommunityTribute[] = comms.map(cv => {
-     let tribute: CommunityTribute = {
-       key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
-       view: cv,
-     };
-     return tribute;
-   });
-   return communities;
+   const communitiesResponse = await fetchCommunities(text);
+   return communitiesResponse.map(cv => ({
+     key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
+     view: cv,
+   }));
  }
  
  export function getRecipientIdFromProps(props: any): number {
  }
  
  export function getIdFromProps(props: any): number | undefined {
-   let id = props.match.params.post_id;
+   const id = props.match.params.post_id;
    return id ? Number(id) : undefined;
  }
  
  export function getCommentIdFromProps(props: any): number | undefined {
-   let id = props.match.params.comment_id;
+   const id = props.match.params.comment_id;
    return id ? Number(id) : undefined;
  }
  
- export function editCommentRes(data: CommentView, comments?: CommentView[]) {
-   let found = comments?.find(c => c.comment.id == data.comment.id);
-   if (found) {
-     found.comment.content = data.comment.content;
-     found.comment.distinguished = data.comment.distinguished;
-     found.comment.updated = data.comment.updated;
-     found.comment.removed = data.comment.removed;
-     found.comment.deleted = data.comment.deleted;
-     found.counts.upvotes = data.counts.upvotes;
-     found.counts.downvotes = data.counts.downvotes;
-     found.counts.score = data.counts.score;
-   }
+ type ImmutableListKey =
+   | "comment"
+   | "comment_reply"
+   | "person_mention"
+   | "community"
+   | "private_message"
+   | "post"
+   | "post_report"
+   | "comment_report"
+   | "private_message_report"
+   | "registration_application";
+ function editListImmutable<
+   T extends { [key in F]: { id: number } },
+   F extends ImmutableListKey
+ >(fieldName: F, data: T, list: T[]): T[] {
+   return [
+     ...list.map(c => (c[fieldName].id === data[fieldName].id ? data : c)),
+   ];
+ }
+ export function editComment(
+   data: CommentView,
+   comments: CommentView[]
+ ): CommentView[] {
+   return editListImmutable("comment", data, comments);
  }
  
- export function saveCommentRes(data: CommentView, comments?: CommentView[]) {
-   let found = comments?.find(c => c.comment.id == data.comment.id);
-   if (found) {
-     found.saved = data.saved;
-   }
+ export function editCommentReply(
+   data: CommentReplyView,
+   replies: CommentReplyView[]
+ ): CommentReplyView[] {
+   return editListImmutable("comment_reply", data, replies);
+ }
+ interface WithComment {
+   comment: CommentI;
+   counts: CommentAggregates;
+   my_vote?: number;
+   saved: boolean;
+ }
+ export function editMention(
+   data: PersonMentionView,
+   comments: PersonMentionView[]
+ ): PersonMentionView[] {
+   return editListImmutable("person_mention", data, comments);
+ }
+ export function editCommunity(
+   data: CommunityView,
+   communities: CommunityView[]
+ ): CommunityView[] {
+   return editListImmutable("community", data, communities);
+ }
+ export function editPrivateMessage(
+   data: PrivateMessageView,
+   messages: PrivateMessageView[]
+ ): PrivateMessageView[] {
+   return editListImmutable("private_message", data, messages);
+ }
+ export function editPost(data: PostView, posts: PostView[]): PostView[] {
+   return editListImmutable("post", data, posts);
+ }
+ export function editPostReport(
+   data: PostReportView,
+   reports: PostReportView[]
+ ) {
+   return editListImmutable("post_report", data, reports);
+ }
+ export function editCommentReport(
+   data: CommentReportView,
+   reports: CommentReportView[]
+ ): CommentReportView[] {
+   return editListImmutable("comment_report", data, reports);
+ }
+ export function editPrivateMessageReport(
+   data: PrivateMessageReportView,
+   reports: PrivateMessageReportView[]
+ ): PrivateMessageReportView[] {
+   return editListImmutable("private_message_report", data, reports);
+ }
+ export function editRegistrationApplication(
+   data: RegistrationApplicationView,
+   apps: RegistrationApplicationView[]
+ ): RegistrationApplicationView[] {
+   return editListImmutable("registration_application", data, apps);
+ }
+ export function editWith<D extends WithComment, L extends WithComment>(
+   { comment, counts, saved, my_vote }: D,
+   list: L[]
+ ) {
+   return [
+     ...list.map(c =>
+       c.comment.id === comment.id
+         ? { ...c, comment, counts, saved, my_vote }
+         : c
+     ),
+   ];
  }
  
  export function updatePersonBlock(
    data: BlockPersonResponse,
    myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
  ) {
-   let mui = myUserInfo;
-   if (mui) {
+   if (myUserInfo) {
      if (data.blocked) {
-       mui.person_blocks.push({
-         person: mui.local_user_view.person,
+       myUserInfo.person_blocks.push({
+         person: myUserInfo.local_user_view.person,
          target: data.person_view.person,
        });
        toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
      } else {
-       mui.person_blocks = mui.person_blocks.filter(
-         i => i.target.id != data.person_view.person.id
+       myUserInfo.person_blocks = myUserInfo.person_blocks.filter(
+         i => i.target.id !== data.person_view.person.id
        );
        toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
      }
@@@ -962,128 -963,25 +964,25 @@@ export function updateCommunityBlock
    data: BlockCommunityResponse,
    myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
  ) {
-   let mui = myUserInfo;
-   if (mui) {
+   if (myUserInfo) {
      if (data.blocked) {
-       mui.community_blocks.push({
-         person: mui.local_user_view.person,
+       myUserInfo.community_blocks.push({
+         person: myUserInfo.local_user_view.person,
          community: data.community_view.community,
        });
        toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
      } else {
-       mui.community_blocks = mui.community_blocks.filter(
-         i => i.community.id != data.community_view.community.id
+       myUserInfo.community_blocks = myUserInfo.community_blocks.filter(
+         i => i.community.id !== data.community_view.community.id
        );
        toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
      }
    }
  }
  
- export function createCommentLikeRes(
-   data: CommentView,
-   comments?: CommentView[]
- ) {
-   let found = comments?.find(c => c.comment.id === data.comment.id);
-   if (found) {
-     found.counts.score = data.counts.score;
-     found.counts.upvotes = data.counts.upvotes;
-     found.counts.downvotes = data.counts.downvotes;
-     if (data.my_vote !== null) {
-       found.my_vote = data.my_vote;
-     }
-   }
- }
- export function createPostLikeFindRes(data: PostView, posts?: PostView[]) {
-   let found = posts?.find(p => p.post.id == data.post.id);
-   if (found) {
-     createPostLikeRes(data, found);
-   }
- }
- export function createPostLikeRes(data: PostView, post_view?: PostView) {
-   if (post_view) {
-     post_view.counts.score = data.counts.score;
-     post_view.counts.upvotes = data.counts.upvotes;
-     post_view.counts.downvotes = data.counts.downvotes;
-     if (data.my_vote !== null) {
-       post_view.my_vote = data.my_vote;
-     }
-   }
- }
- export function editPostFindRes(data: PostView, posts?: PostView[]) {
-   let found = posts?.find(p => p.post.id == data.post.id);
-   if (found) {
-     editPostRes(data, found);
-   }
- }
- export function editPostRes(data: PostView, post: PostView) {
-   if (post) {
-     post.post.url = data.post.url;
-     post.post.name = data.post.name;
-     post.post.nsfw = data.post.nsfw;
-     post.post.deleted = data.post.deleted;
-     post.post.removed = data.post.removed;
-     post.post.featured_community = data.post.featured_community;
-     post.post.featured_local = data.post.featured_local;
-     post.post.body = data.post.body;
-     post.post.locked = data.post.locked;
-     post.saved = data.saved;
-   }
- }
- // TODO possible to make these generic?
- export function updatePostReportRes(
-   data: PostReportView,
-   reports?: PostReportView[]
- ) {
-   let found = reports?.find(p => p.post_report.id == data.post_report.id);
-   if (found) {
-     found.post_report = data.post_report;
-   }
- }
- export function updateCommentReportRes(
-   data: CommentReportView,
-   reports?: CommentReportView[]
- ) {
-   let found = reports?.find(c => c.comment_report.id == data.comment_report.id);
-   if (found) {
-     found.comment_report = data.comment_report;
-   }
- }
- export function updatePrivateMessageReportRes(
-   data: PrivateMessageReportView,
-   reports?: PrivateMessageReportView[]
- ) {
-   let found = reports?.find(
-     c => c.private_message_report.id == data.private_message_report.id
-   );
-   if (found) {
-     found.private_message_report = data.private_message_report;
-   }
- }
- export function updateRegistrationApplicationRes(
-   data: RegistrationApplicationView,
-   applications?: RegistrationApplicationView[]
- ) {
-   let found = applications?.find(
-     ra => ra.registration_application.id == data.registration_application.id
-   );
-   if (found) {
-     found.registration_application = data.registration_application;
-     found.admin = data.admin;
-     found.creator_local_user = data.creator_local_user;
-   }
- }
  export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
-   let nodes: CommentNodeI[] = [];
-   for (let comment of comments) {
+   const nodes: CommentNodeI[] = [];
+   for (const comment of comments) {
      nodes.push({ comment_view: comment, children: [], depth: 0 });
    }
    return nodes;
@@@ -1111,15 -1009,15 +1010,15 @@@ export function buildCommentsTree
    comments: CommentView[],
    parentComment: boolean
  ): CommentNodeI[] {
-   let map = new Map<number, CommentNodeI>();
-   let depthOffset = !parentComment
+   const map = new Map<number, CommentNodeI>();
+   const depthOffset = !parentComment
      ? 0
      : getDepthFromComment(comments[0].comment) ?? 0;
  
-   for (let comment_view of comments) {
-     let depthI = getDepthFromComment(comment_view.comment) ?? 0;
-     let depth = depthI ? depthI - depthOffset : 0;
-     let node: CommentNodeI = {
+   for (const comment_view of comments) {
+     const depthI = getDepthFromComment(comment_view.comment) ?? 0;
+     const depth = depthI ? depthI - depthOffset : 0;
+     const node: CommentNodeI = {
        comment_view,
        children: [],
        depth,
      map.set(comment_view.comment.id, { ...node });
    }
  
-   let tree: CommentNodeI[] = [];
+   const tree: CommentNodeI[] = [];
  
    // if its a parent comment fetch, then push the first comment to the top node.
    if (parentComment) {
-     let cNode = map.get(comments[0].comment.id);
+     const cNode = map.get(comments[0].comment.id);
      if (cNode) {
        tree.push(cNode);
      }
    }
  
-   for (let comment_view of comments) {
-     let child = map.get(comment_view.comment.id);
+   for (const comment_view of comments) {
+     const child = map.get(comment_view.comment.id);
      if (child) {
-       let parent_id = getCommentParentId(comment_view.comment);
+       const parent_id = getCommentParentId(comment_view.comment);
        if (parent_id) {
-         let parent = map.get(parent_id);
+         const parent = map.get(parent_id);
          // Necessary because blocked comment might not exist
          if (parent) {
            parent.children.push(child);
  }
  
  export function getCommentParentId(comment?: CommentI): number | undefined {
-   let split = comment?.path.split(".");
+   const split = comment?.path.split(".");
    // remove the 0
    split?.shift();
  
  }
  
  export function getDepthFromComment(comment?: CommentI): number | undefined {
-   let len = comment?.path.split(".").length;
+   const len = comment?.path.split(".").length;
    return len ? len - 2 : undefined;
  }
  
+ // TODO make immutable
  export function insertCommentIntoTree(
    tree: CommentNodeI[],
    cv: CommentView,
    parentComment: boolean
  ) {
    // Building a fake node to be used for later
-   let node: CommentNodeI = {
+   const node: CommentNodeI = {
      comment_view: cv,
      children: [],
      depth: 0,
    };
  
-   let parentId = getCommentParentId(cv.comment);
+   const parentId = getCommentParentId(cv.comment);
    if (parentId) {
-     let parent_comment = searchCommentTree(tree, parentId);
+     const parent_comment = searchCommentTree(tree, parentId);
      if (parent_comment) {
        node.depth = parent_comment.depth + 1;
        parent_comment.children.unshift(node);
@@@ -1201,13 -1100,13 +1101,13 @@@ export function searchCommentTree
    tree: CommentNodeI[],
    id: number
  ): CommentNodeI | undefined {
-   for (let node of tree) {
+   for (const node of tree) {
      if (node.comment_view.comment.id === id) {
        return node;
      }
  
      for (const child of node.children) {
-       let res = searchCommentTree([child], id);
+       const res = searchCommentTree([child], id);
  
        if (res) {
          return res;
@@@ -1232,7 -1131,7 +1132,7 @@@ function hsl(num: number) 
  }
  
  export function hostname(url: string): string {
-   let cUrl = new URL(url);
+   const cUrl = new URL(url);
    return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
  }
  
@@@ -1262,27 -1161,13 +1162,15 @@@ export function isBrowser() 
    return typeof window !== "undefined";
  }
  
- export function setIsoData<T extends object>(context: any): IsoData<T> {
 -export function setIsoData(context: any): IsoData {
++export function setIsoData<T extends Record<string, RequestState<any>>>(
++  context: any
++): IsoData<T> {
    // If its the browser, you need to deserialize the data from the window
    if (isBrowser()) {
      return window.isoData;
    } else return context.router.staticContext;
  }
  
- export function wsSubscribe(parseMessage: any): Subscription | undefined {
-   if (isBrowser()) {
-     return WebSocketService.Instance.subject
-       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-       .subscribe(
-         msg => parseMessage(msg),
-         err => console.error(err),
-         () => console.log("complete")
-       );
-   } else {
-     return undefined;
-   }
- }
  moment.updateLocale("en", {
    relativeTime: {
      future: "in %s",
  });
  
  export function saveScrollPosition(context: any) {
-   let path: string = context.router.route.location.pathname;
-   let y = window.scrollY;
+   const path: string = context.router.route.location.pathname;
+   const y = window.scrollY;
    sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
  }
  
  export function restoreScrollPosition(context: any) {
-   let path: string = context.router.route.location.pathname;
-   let y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
+   const path: string = context.router.route.location.pathname;
+   const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
    window.scrollTo(0, y);
  }
  
@@@ -1346,32 -1231,30 +1234,30 @@@ export function personToChoice(pvs: Per
    };
  }
  
export async function fetchCommunities(q: string) {
-   let form: Search = {
function fetchSearchResults(q: string, type_: SearchType) {
+   const form: Search = {
      q,
-     type_: "Communities",
+     type_,
      sort: "TopAll",
      listing_type: "All",
      page: 1,
      limit: fetchLimit,
-     auth: myAuth(false),
+     auth: myAuth(),
    };
-   let client = new LemmyHttp(getHttpBase());
-   return client.search(form);
+   return HttpService.client.search(form);
+ }
+ export async function fetchCommunities(q: string) {
+   const res = await fetchSearchResults(q, "Communities");
+   return res.state === "success" ? res.data.communities : [];
  }
  
  export async function fetchUsers(q: string) {
-   let form: Search = {
-     q,
-     type_: "Users",
-     sort: "TopAll",
-     listing_type: "All",
-     page: 1,
-     limit: fetchLimit,
-     auth: myAuth(false),
-   };
-   let client = new LemmyHttp(getHttpBase());
-   return client.search(form);
+   const res = await fetchSearchResults(q, "Users");
+   return res.state === "success" ? res.data.users : [];
  }
  
  export function communitySelectName(cv: CommunityView): string {
@@@ -1391,7 -1274,7 +1277,7 @@@ export function initializeSite(site?: G
    UserService.Instance.myUserInfo = site?.my_user;
    i18n.changeLanguage(getLanguages()[0]);
    if (site) {
-     setupEmojiDataModel(site.custom_emojis);
+     setupEmojiDataModel(site.custom_emojis ?? []);
    }
    setupMarkdown();
  }
@@@ -1408,7 -1291,7 +1294,7 @@@ export function numToSI(value: number)
  }
  
  export function isBanned(ps: Person): boolean {
-   let expires = ps.ban_expires;
+   const expires = ps.ban_expires;
    // Add Z to convert from UTC date
    // TODO this check probably isn't necessary anymore
    if (expires) {
    }
  }
  
- export function myAuth(throwErr = true): string | undefined {
-   return UserService.Instance.auth(throwErr);
+ export function myAuth(): string | undefined {
+   return UserService.Instance.auth();
+ }
+ export function myAuthRequired(): string {
+   return UserService.Instance.auth(true) ?? "";
  }
  
  export function enableDownvotes(siteRes: GetSiteResponse): boolean {
@@@ -1478,8 -1365,8 +1368,8 @@@ export function nsfwCheck
    pv: PostView,
    myUserInfo = UserService.Instance.myUserInfo
  ): boolean {
-   let nsfw = pv.post.nsfw || pv.community.nsfw;
-   let myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
+   const nsfw = pv.post.nsfw || pv.community.nsfw;
+   const myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
    return !nsfw || (nsfw && myShowNsfw);
  }
  
@@@ -1503,10 -1390,10 +1393,10 @@@ export function selectableLanguages
    showSite?: boolean,
    myUserInfo = UserService.Instance.myUserInfo
  ): Language[] {
-   let allLangIds = allLanguages.map(l => l.id);
+   const allLangIds = allLanguages.map(l => l.id);
    let myLangs = myUserInfo?.discussion_languages ?? allLangIds;
    myLangs = myLangs.length == 0 ? allLangIds : myLangs;
-   let siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
+   const siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
  
    if (showAll) {
      return allLanguages;
    }
  }
  
- export function uploadImage(image: File): Promise<UploadImageResponse> {
-   const client = new LemmyHttp(getHttpBase());
-   return client.uploadImage({ image });
- }
  interface EmojiMartCategory {
    id: string;
    name: string;
@@@ -1557,10 -1438,6 +1441,6 @@@ export type QueryParams<T extends Recor
    [key in keyof T]?: string;
  };
  
- export type WithPromiseKeys<T extends object> = {
-   [K in keyof T]: Promise<T[K]>;
- };
  export function getQueryParams<T extends Record<string, any>>(processors: {
    [K in keyof T]: (param: string) => T[K];
  }): T {
@@@ -1591,7 -1468,7 +1471,7 @@@ export function getQueryString<T extend
  }
  
  export function isAuthPath(pathname: string) {
-   return /create_.*|inbox|settings|setup|admin|reports|registration_applications/g.test(
+   return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
      pathname
    );
  }
@@@ -1605,3 -1482,11 +1485,15 @@@ export function share(shareData: ShareD
      navigator.share(shareData);
    }
  }
+ export function newVote(voteType: VoteType, myVote?: number): number {
+   if (voteType == VoteType.Upvote) {
+     return myVote == 1 ? 0 : 1;
+   } else {
+     return myVote == -1 ? 0 : -1;
+   }
+ }
++
++export type RouteDataResponse<T extends Record<string, any>> = {
++  [K in keyof T]: RequestState<Exclude<T[K], undefined>>;
++};