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

index 4ab4f76f93a047107957cd31eda8dce8614f5f3f,43024076ebb74db9d7624a89cf354555d355d3f0..98063558cfab62e0fb622d3f2868a08b498ca4b7
@@@ -19,6 -19,7 +19,11 @@@ import 
    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,
@@@ -129,27 -136,30 +140,30 @@@ server.get("/*", async (req, res) => 
      // 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);
        }
      }
  
index 53e0e967e908d2bccefe202db94244680d952e4f,623269439f8642444aad0fcaa6cb8cdb0649d02d..3eb7bd3aa76cde879b0808b95ceeca618ffc4fe2
@@@ -6,17 -5,14 +5,15 @@@ import 
    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,
@@@ -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 {
@@@ -64,40 -46,13 +51,13 @@@ function getListingTypeFromQuery(listin
    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();
      }
    }
  
      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
@@@ -30,16 -58,16 +58,17 @@@ import 
    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,
@@@ -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,
++        };
++      }
      }
    }
  
  
    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 = {
  
      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 {
index 4419cf36c8fba786f484a5c2e3fa6c3ec72b3808,9b7256d03a507e561f5bf2e92cecc0586b216b60..11be72579c4ca410b3a667f878a430e7a6a0d7b8
@@@ -5,21 -8,16 +8,17 @@@ import 
    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,
@@@ -35,23 -32,19 +33,24 @@@ 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: [],
    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();
      }
    }
  
      );
    }
  
++  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 (
        <>
index e85c3e66899ff326b693f2645d1c7e42dbc20ded,8be983042a5ce584988bb282d928656678ddfeb0..cc9dd518dbddac5ae5a9c651f691a1597dc9ec8f
@@@ -61,7 -77,6 +77,7 @@@ import 
    QueryParams,
    relTags,
    restoreScrollPosition,
-   saveCommentRes,
++  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -104,12 -117,6 +118,12 @@@ 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;
  }
@@@ -210,44 -175,12 +182,12 @@@ 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,
      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();
      }
    }
  
  
    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 {
index fd1ed617600f8ee25de625bbb6e2a0c4408f55f3,30cb9dea0c0491a020890c3206d20c14709dd09e..bec472cf1ea9d6aca77ed7bd3f6685f5788cc272
@@@ -3,69 -3,62 +3,68 @@@ 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 {
index cb4b37f4363d49f579fa72c295ebdbf6eea38481,d917f5f35ed675842e43169fa6da129b696335b6..48be10b6245849c0b511bc9ba9137599eb364566
@@@ -39,7 -34,6 +35,7 @@@ import { HttpService, RequestState } fr
  import {
    Choice,
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    amAdmin,
    amMod,
    debounce,
@@@ -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;
@@@ -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)],
+         };
+       }
      }
    }
  
      }
    }
  
--  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
@@@ -7,44 -14,59 +14,57 @@@ import 
    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;
@@@ -98,8 -114,7 +119,7 @@@ interface InboxState 
  }
  
  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,
      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,
    }
  
    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,
index 00a441970a6af6421492f2942b902bada80cc1cd,f80d5b907a2f7f1972dc0b62d1bebb51d06df1fe..5466bc5fcb746b2d388a7642725208e007201857
@@@ -15,27 -30,35 +30,36 @@@ import 
    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,
@@@ -68,13 -90,8 +91,12 @@@ 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;
@@@ -157,10 -156,9 +161,9 @@@ 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,
      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();
      }
    }
  
      }
    }
  
--  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>
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,12 -28,8 +28,12 @@@ enum UnreadOrAll 
    All,
  }
  
- interface RegistrationApplicationsData {
++type RegistrationApplicationsData = RouteDataResponse<{
 +  listRegistrationApplicationsResponse: ListRegistrationApplicationsResponse;
- }
++}>;
 +
  interface RegistrationApplicationsState {
-   listRegistrationApplicationsResponse?: ListRegistrationApplicationsResponse;
+   appsRes: RequestState<ListRegistrationApplicationsResponse>;
    siteRes: GetSiteResponse;
    unreadOrAll: UnreadOrAll;
    page: number;
@@@ -50,9 -40,9 +44,9 @@@ export class RegistrationApplications e
    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,
      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();
      }
    }
  
      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,
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;
@@@ -87,47 -75,45 +82,49 @@@ interface ReportsState 
  }
  
  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" },
+         };
+       }
      }
    }
  
    }
  
    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,22 -1,22 +1,24 @@@
  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,
@@@ -36,10 -32,6 +34,11 @@@ export interface CreatePostProps 
    communityId?: number;
  }
  
- interface CreatePostData {
++type CreatePostData = RouteDataResponse<{
 +  communityResponse?: GetCommunityResponse;
- }
++  initialCommunitiesRes: ListCommunitiesResponse;
++}>;
 +
  function getCreatePostQueryParams() {
    return getQueryParams<CreatePostProps>({
      communityId: getIdFromString,
@@@ -56,8 -54,7 +61,7 @@@ export class CreatePost extends Compone
    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,
      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 = {
@@@ -92,9 -88,9 +96,9 @@@
        this.state = {
          ...this.state,
          loading: false,
 -        initialCommunitiesRes: listCommunitiesRes,
++        initialCommunitiesRes,
+         isIsomorphic: true,
        };
-     } else {
-       this.fetchCommunity();
      }
    }
  
      });
    }
  
-   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
@@@ -53,7 -77,6 +77,7 @@@ import 
    isImage,
    myAuth,
    restoreScrollPosition,
-   saveCommentRes,
++  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -73,11 -93,6 +94,11 @@@ 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;
  }
  
  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,
      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,
      }
    }
  
-   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;
        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() {
index c897c44e6bd8c8927acccd2183339af4ebf0087f,817cfd880d001181958b56edce97c340af245040..5b9eb9819840a20d54850d4d08d123b774484988
@@@ -3,18 -4,13 +4,14 @@@ import 
    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,
@@@ -25,27 -19,23 +20,27 @@@ import { HtmlTags } from "../common/htm
  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>
+         );
+       }
      }
    }
  
index c62c7a98413542d7220ee7c875579927bb5ad332,8097dbde433b7ac7ef3811e47cc44cd6d4ffe76b..9f466730a8c952cd5cf03fcefb3efe44a1f3e1d1
@@@ -32,7 -27,6 +27,7 @@@ import { HttpService, RequestState } fr
  import {
    Choice,
    QueryParams,
-   WithPromiseKeys,
++  RouteDataResponse,
    capitalizeFirstLetter,
    commentsToFlatNodes,
    communityToChoice,
@@@ -81,14 -70,6 +71,14 @@@ 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 {
@@@ -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) {
      };
  
      // 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,
++          };
++        }
 +      }
      }
    }
  
      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" }));
        }
      }
  
            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>
index dc4490cbdc7701f61eda5b3dd525e9143a362977,3b64f60533dc246b6fd6fe74f5dfb2387ef29fed..dbba70406b1314037691fa5b6add9b023138b76d
@@@ -5,15 -6,15 +6,17 @@@ 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;
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;
+   }
+ }
index fe83977db598e8921cddd7126cd1e92a2db34ef7,46e8601be08e5ff4895d6b9335f24fe765cb7168..83cc6f1adf645b4389d70b7f5367ebcb646af9f9
@@@ -43,8 -43,8 +43,9 @@@ 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()) {
@@@ -1262,7 -1161,7 +1162,9 @@@ 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;
@@@ -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>>;
++};