From: Dessalines <tyhou13@gmx.com>
Date: Wed, 9 Sep 2020 00:48:17 +0000 (-0500)
Subject: Add post, inbox, and user routes.
X-Git-Url: http://these/git/%7BpictshareAvatarThumbnail%28admin.avatar%29%7D?a=commitdiff_plain;h=95b74ad74c56754d9ebc456beb2c66ddef055705;p=lemmy-ui.git

Add post, inbox, and user routes.
---

diff --git a/src/shared/components/comment-node.tsx b/src/shared/components/comment-node.tsx
index b9edcae..19c1bdc 100644
--- a/src/shared/components/comment-node.tsx
+++ b/src/shared/components/comment-node.tsx
@@ -202,7 +202,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                     }}
                   />
                   <span class="mx-2">•</span>
-                  <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
+                  <Link className="mr-2" to={`/post/${node.comment.post_id}`}>
                     {node.comment.post_name}
                   </Link>
                 </>
@@ -343,7 +343,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           {!this.myComment && (
                             <button class="btn btn-link btn-animate">
                               <Link
-                                class="text-muted"
+                                className="text-muted"
                                 to={`/create_private_message/recipient/${node.comment.creator_id}`}
                                 title={i18n.t('message').toLowerCase()}
                               >
@@ -757,7 +757,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     let node = this.props.node;
     return (
       <Link
-        class="btn btn-link btn-animate text-muted"
+        className="btn btn-link btn-animate text-muted"
         to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
         title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
       >
diff --git a/src/shared/components/inbox.tsx b/src/shared/components/inbox.tsx
index 8292511..e36b3b8 100644
--- a/src/shared/components/inbox.tsx
+++ b/src/shared/components/inbox.tsx
@@ -1,7 +1,6 @@
 import { Component, linkEvent } from 'inferno';
 import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
 import {
   UserOperation,
   Comment,
@@ -17,7 +16,6 @@ import {
   GetPrivateMessagesForm,
   PrivateMessagesResponse,
   PrivateMessageResponse,
-  GetSiteResponse,
   Site,
 } from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
@@ -31,6 +29,11 @@ import {
   createCommentLikeRes,
   commentsToFlatNodes,
   setupTippy,
+  setIsoData,
+  wsSubscribe,
+  lemmyHttp,
+  setAuth,
+  isBrowser,
 } from '../utils';
 import { CommentNodes } from './comment-nodes';
 import { PrivateMessage } from './private-message';
@@ -60,9 +63,11 @@ interface InboxState {
   sort: SortType;
   page: number;
   site: Site;
+  loading: boolean;
 }
 
 export class Inbox extends Component<any, InboxState> {
+  private isoData = setIsoData(this.context);
   private subscription: Subscription;
   private emptyState: InboxState = {
     unreadOrAll: UnreadOrAll.Unread,
@@ -72,20 +77,8 @@ export class Inbox extends Component<any, InboxState> {
     messages: [],
     sort: SortType.New,
     page: 1,
-    site: {
-      id: undefined,
-      name: undefined,
-      creator_id: undefined,
-      published: undefined,
-      creator_name: undefined,
-      number_of_users: undefined,
-      number_of_posts: undefined,
-      number_of_comments: undefined,
-      number_of_communities: undefined,
-      enable_downvotes: undefined,
-      open_registration: undefined,
-      enable_nsfw: undefined,
-    },
+    site: this.isoData.site.site,
+    loading: true,
   };
 
   constructor(props: any, context: any) {
@@ -94,77 +87,88 @@ export class Inbox extends Component<any, InboxState> {
     this.state = this.emptyState;
     this.handleSortChange = this.handleSortChange.bind(this);
 
-    this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        msg => this.parseMessage(msg),
-        err => console.error(err),
-        () => console.log('complete')
-      );
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
 
-    this.refetch();
-    WebSocketService.Instance.getSite();
+    // Only fetch the data if coming from another route
+    if (this.isoData.path == this.context.router.route.match.url) {
+      this.state.replies = this.isoData.routeData[0].replies;
+      this.state.mentions = this.isoData.routeData[1].mentions;
+      this.state.messages = this.isoData.routeData[2].messages;
+      this.sendUnreadCount();
+      this.state.loading = false;
+    } else {
+      this.refetch();
+    }
   }
 
   componentWillUnmount() {
-    this.subscription.unsubscribe();
+    if (isBrowser()) {
+      this.subscription.unsubscribe();
+    }
   }
 
   get documentTitle(): string {
-    if (this.state.site.name) {
-      return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
-        this.state.site.name
-      }`;
-    } else {
-      return 'Lemmy';
-    }
+    return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
+      this.state.site.name
+    }`;
   }
 
   render() {
     return (
       <div class="container">
         <Helmet title={this.documentTitle} />
-        <div class="row">
-          <div class="col-12">
-            <h5 class="mb-1">
-              {i18n.t('inbox')}
-              <small>
-                <a
-                  href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
-                  target="_blank"
-                  title="RSS"
-                  rel="noopener"
-                >
-                  <svg class="icon ml-2 text-muted small">
-                    <use xlinkHref="#icon-rss">#</use>
-                  </svg>
-                </a>
-              </small>
-            </h5>
-            {this.state.replies.length +
-              this.state.mentions.length +
-              this.state.messages.length >
-              0 &&
-              this.state.unreadOrAll == UnreadOrAll.Unread && (
-                <ul class="list-inline mb-1 text-muted small font-weight-bold">
-                  <li className="list-inline-item">
-                    <span
-                      class="pointer"
-                      onClick={linkEvent(this, this.markAllAsRead)}
-                    >
-                      {i18n.t('mark_all_as_read')}
-                    </span>
-                  </li>
-                </ul>
-              )}
-            {this.selects()}
-            {this.state.messageType == MessageType.All && this.all()}
-            {this.state.messageType == MessageType.Replies && this.replies()}
-            {this.state.messageType == MessageType.Mentions && this.mentions()}
-            {this.state.messageType == MessageType.Messages && this.messages()}
-            {this.paginator()}
+        {this.state.loading ? (
+          <h5>
+            <svg class="icon icon-spinner spin">
+              <use xlinkHref="#icon-spinner"></use>
+            </svg>
+          </h5>
+        ) : (
+          <div class="row">
+            <div class="col-12">
+              <h5 class="mb-1">
+                {i18n.t('inbox')}
+                <small>
+                  <a
+                    href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
+                    target="_blank"
+                    title="RSS"
+                    rel="noopener"
+                  >
+                    <svg class="icon ml-2 text-muted small">
+                      <use xlinkHref="#icon-rss">#</use>
+                    </svg>
+                  </a>
+                </small>
+              </h5>
+              {this.state.replies.length +
+                this.state.mentions.length +
+                this.state.messages.length >
+                0 &&
+                this.state.unreadOrAll == UnreadOrAll.Unread && (
+                  <ul class="list-inline mb-1 text-muted small font-weight-bold">
+                    <li className="list-inline-item">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.markAllAsRead)}
+                      >
+                        {i18n.t('mark_all_as_read')}
+                      </span>
+                    </li>
+                  </ul>
+                )}
+              {this.selects()}
+              {this.state.messageType == MessageType.All && this.all()}
+              {this.state.messageType == MessageType.Replies && this.replies()}
+              {this.state.messageType == MessageType.Mentions &&
+                this.mentions()}
+              {this.state.messageType == MessageType.Messages &&
+                this.messages()}
+              {this.paginator()}
+            </div>
           </div>
-        </div>
+        )}
       </div>
     );
   }
@@ -397,6 +401,39 @@ export class Inbox extends Component<any, InboxState> {
     i.refetch();
   }
 
+  static fetchInitialData(auth: string, _path: string): Promise<any>[] {
+    let promises: Promise<any>[] = [];
+
+    // It can be /u/me, or /username/1
+    let repliesForm: GetRepliesForm = {
+      sort: SortType.New,
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+    setAuth(repliesForm, auth);
+    promises.push(lemmyHttp.getReplies(repliesForm));
+
+    let userMentionsForm: GetUserMentionsForm = {
+      sort: SortType.New,
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+    setAuth(userMentionsForm, auth);
+    promises.push(lemmyHttp.getUserMentions(userMentionsForm));
+
+    let privateMessagesForm: GetPrivateMessagesForm = {
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+    setAuth(privateMessagesForm, auth);
+    promises.push(lemmyHttp.getPrivateMessages(privateMessagesForm));
+
+    return promises;
+  }
+
   refetch() {
     let repliesForm: GetRepliesForm = {
       sort: this.state.sort,
@@ -450,6 +487,7 @@ export class Inbox extends Component<any, InboxState> {
     } else if (res.op == UserOperation.GetReplies) {
       let data = res.data as GetRepliesResponse;
       this.state.replies = data.replies;
+      this.state.loading = false;
       this.sendUnreadCount();
       window.scrollTo(0, 0);
       this.setState(this.state);
@@ -581,10 +619,6 @@ export class Inbox extends Component<any, InboxState> {
       let data = res.data as CommentResponse;
       createCommentLikeRes(data, this.state.replies);
       this.setState(this.state);
-    } else if (res.op == UserOperation.GetSite) {
-      let data = res.data as GetSiteResponse;
-      this.state.site = data.site;
-      this.setState(this.state);
     }
   }
 
diff --git a/src/shared/components/post-form.tsx b/src/shared/components/post-form.tsx
index ac6ad3b..f37b5bc 100644
--- a/src/shared/components/post-form.tsx
+++ b/src/shared/components/post-form.tsx
@@ -115,6 +115,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       }
     }
 
+    this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
   }
 
diff --git a/src/shared/components/post-listing.tsx b/src/shared/components/post-listing.tsx
index d82ddc8..93edfb7 100644
--- a/src/shared/components/post-listing.tsx
+++ b/src/shared/components/post-listing.tsx
@@ -17,6 +17,7 @@ import {
   AddAdminForm,
   TransferSiteForm,
   TransferCommunityForm,
+  Community,
 } from 'lemmy-js-client';
 import { BanType } from '../interfaces';
 import { MomentTime } from './moment-time';
@@ -61,6 +62,7 @@ interface PostListingState {
 
 interface PostListingProps {
   post: Post;
+  communities: Community[];
   showCommunity?: boolean;
   showBody?: boolean;
   moderators?: CommunityUser[];
@@ -127,6 +129,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               onCancel={this.handleEditCancel}
               enableNsfw={this.props.enableNsfw}
               enableDownvotes={this.props.enableDownvotes}
+              communities={this.props.communities}
             />
           </div>
         )}
@@ -184,6 +187,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       }
     } else if (post.thumbnail_url) {
       return pictrsImage(post.thumbnail_url, thumbnail);
+    } else {
+      return null;
     }
   }
 
@@ -598,7 +603,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 </li>
                 <li className="list-inline-item">
                   <Link
-                    class="btn btn-link btn-animate text-muted"
+                    className="btn btn-link btn-animate text-muted"
                     to={`/create_post${this.crossPostParams}`}
                     title={i18n.t('cross_post')}
                   >
diff --git a/src/shared/components/post.tsx b/src/shared/components/post.tsx
index d35a77d..1406de7 100644
--- a/src/shared/components/post.tsx
+++ b/src/shared/components/post.tsx
@@ -1,17 +1,13 @@
 import { Component, linkEvent } from 'inferno';
 import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
 import {
   UserOperation,
-  Community,
   Post as PostI,
   GetPostResponse,
   PostResponse,
-  Comment,
   MarkCommentAsReadForm,
   CommentResponse,
-  CommunityUser,
   CommunityResponse,
   CommentNode as CommentNodeI,
   BanFromCommunityResponse,
@@ -26,6 +22,8 @@ import {
   GetSiteResponse,
   GetCommunityResponse,
   WebSocketJsonResponse,
+  ListCategoriesResponse,
+  Category,
 } from 'lemmy-js-client';
 import { CommentSortType, CommentViewType } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
@@ -39,6 +37,13 @@ import {
   commentsToFlatNodes,
   setupTippy,
   favIconUrl,
+  setIsoData,
+  getIdFromProps,
+  getCommentIdFromProps,
+  wsSubscribe,
+  setAuth,
+  lemmyHttp,
+  isBrowser,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { Sidebar } from './sidebar';
@@ -48,56 +53,32 @@ import autosize from 'autosize';
 import { i18n } from '../i18next';
 
 interface PostState {
-  post: PostI;
-  comments: Comment[];
+  postRes: GetPostResponse;
+  postId: number;
+  commentId?: number;
   commentSort: CommentSortType;
   commentViewType: CommentViewType;
-  community: Community;
-  moderators: CommunityUser[];
-  online: number;
   scrolled?: boolean;
-  scrolled_comment_id?: number;
   loading: boolean;
   crossPosts: PostI[];
   siteRes: GetSiteResponse;
+  categories: Category[];
 }
 
 export class Post extends Component<any, PostState> {
   private subscription: Subscription;
+  private isoData = setIsoData(this.context);
   private emptyState: PostState = {
-    post: null,
-    comments: [],
+    postRes: null,
+    postId: getIdFromProps(this.props),
+    commentId: getCommentIdFromProps(this.props),
     commentSort: CommentSortType.Hot,
     commentViewType: CommentViewType.Tree,
-    community: null,
-    moderators: [],
-    online: null,
     scrolled: false,
     loading: true,
     crossPosts: [],
-    siteRes: {
-      admins: [],
-      banned: [],
-      site: {
-        id: undefined,
-        name: undefined,
-        creator_id: undefined,
-        published: undefined,
-        creator_name: undefined,
-        number_of_users: undefined,
-        number_of_posts: undefined,
-        number_of_comments: undefined,
-        number_of_communities: undefined,
-        enable_downvotes: undefined,
-        open_registration: undefined,
-        enable_nsfw: undefined,
-        icon: undefined,
-        banner: undefined,
-      },
-      online: null,
-      version: null,
-      federated_instances: undefined,
-    },
+    siteRes: this.isoData.site,
+    categories: [],
   };
 
   constructor(props: any, context: any) {
@@ -105,24 +86,46 @@ export class Post extends Component<any, PostState> {
 
     this.state = this.emptyState;
 
-    let postId = Number(this.props.match.params.id);
-    if (this.props.match.params.comment_id) {
-      this.state.scrolled_comment_id = this.props.match.params.comment_id;
-    }
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
 
-    this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        msg => this.parseMessage(msg),
-        err => console.error(err),
-        () => console.log('complete')
-      );
+    // Only fetch the data if coming from another route
+    if (this.isoData.path == this.context.router.route.match.url) {
+      this.state.postRes = this.isoData.routeData[0];
+      this.state.categories = this.isoData.routeData[1].categories;
+      this.state.loading = false;
 
+      if (isBrowser() && this.state.commentId) {
+        this.scrollCommentIntoView();
+      }
+    } else {
+      this.fetchPost();
+      WebSocketService.Instance.listCategories();
+    }
+  }
+
+  fetchPost() {
     let form: GetPostForm = {
-      id: postId,
+      id: this.state.postId,
     };
     WebSocketService.Instance.getPost(form);
-    WebSocketService.Instance.getSite();
+  }
+
+  static fetchInitialData(auth: string, path: string): Promise<any>[] {
+    let pathSplit = path.split('/');
+    let promises: Promise<any>[] = [];
+
+    let id = Number(pathSplit[2]);
+
+    let postForm: GetPostForm = {
+      id,
+    };
+    setAuth(postForm, auth);
+
+    promises.push(lemmyHttp.getPost(postForm));
+    promises.push(lemmyHttp.listCategories());
+
+    return promises;
   }
 
   componentWillUnmount() {
@@ -135,17 +138,12 @@ export class Post extends Component<any, PostState> {
 
   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
     if (
-      this.state.scrolled_comment_id &&
+      this.state.commentId &&
       !this.state.scrolled &&
-      lastState.comments.length > 0
+      lastState.postRes &&
+      lastState.postRes.comments.length > 0
     ) {
-      var elmnt = document.getElementById(
-        `comment-${this.state.scrolled_comment_id}`
-      );
-      elmnt.scrollIntoView();
-      elmnt.classList.add('mark');
-      this.state.scrolled = true;
-      this.markScrolledAsRead(this.state.scrolled_comment_id);
+      this.scrollCommentIntoView();
     }
 
     // Necessary if you are on a post and you click another post (same route)
@@ -161,12 +159,20 @@ export class Post extends Component<any, PostState> {
     }
   }
 
+  scrollCommentIntoView() {
+    var elmnt = document.getElementById(`comment-${this.state.commentId}`);
+    elmnt.scrollIntoView();
+    elmnt.classList.add('mark');
+    this.state.scrolled = true;
+    this.markScrolledAsRead(this.state.commentId);
+  }
+
   markScrolledAsRead(commentId: number) {
-    let found = this.state.comments.find(c => c.id == commentId);
-    let parent = this.state.comments.find(c => found.parent_id == c.id);
+    let found = this.state.postRes.comments.find(c => c.id == commentId);
+    let parent = this.state.postRes.comments.find(c => found.parent_id == c.id);
     let parent_user_id = parent
       ? parent.creator_id
-      : this.state.post.creator_id;
+      : this.state.postRes.post.creator_id;
 
     if (
       UserService.Instance.user &&
@@ -185,8 +191,8 @@ export class Post extends Component<any, PostState> {
   }
 
   get documentTitle(): string {
-    if (this.state.post) {
-      return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
+    if (this.state.postRes) {
+      return `${this.state.postRes.post.name} - ${this.state.siteRes.site.name}`;
     } else {
       return 'Lemmy';
     }
@@ -219,20 +225,21 @@ export class Post extends Component<any, PostState> {
           <div class="row">
             <div class="col-12 col-md-8 mb-3">
               <PostListing
-                post={this.state.post}
+                communities={[this.state.postRes.community]}
+                post={this.state.postRes.post}
                 showBody
                 showCommunity
-                moderators={this.state.moderators}
+                moderators={this.state.postRes.moderators}
                 admins={this.state.siteRes.admins}
                 enableDownvotes={this.state.siteRes.site.enable_downvotes}
                 enableNsfw={this.state.siteRes.site.enable_nsfw}
               />
               <div className="mb-2" />
               <CommentForm
-                postId={this.state.post.id}
-                disabled={this.state.post.locked}
+                postId={this.state.postId}
+                disabled={this.state.postRes.post.locked}
               />
-              {this.state.comments.length > 0 && this.sortRadios()}
+              {this.state.postRes.comments.length > 0 && this.sortRadios()}
               {this.state.commentViewType == CommentViewType.Tree &&
                 this.commentsTree()}
               {this.state.commentViewType == CommentViewType.Chat &&
@@ -325,12 +332,12 @@ export class Post extends Component<any, PostState> {
     return (
       <div>
         <CommentNodes
-          nodes={commentsToFlatNodes(this.state.comments)}
+          nodes={commentsToFlatNodes(this.state.postRes.comments)}
           noIndent
-          locked={this.state.post.locked}
-          moderators={this.state.moderators}
+          locked={this.state.postRes.post.locked}
+          moderators={this.state.postRes.moderators}
           admins={this.state.siteRes.admins}
-          postCreatorId={this.state.post.creator_id}
+          postCreatorId={this.state.postRes.post.creator_id}
           showContext
           enableDownvotes={this.state.siteRes.site.enable_downvotes}
           sort={this.state.commentSort}
@@ -343,12 +350,13 @@ export class Post extends Component<any, PostState> {
     return (
       <div class="mb-3">
         <Sidebar
-          community={this.state.community}
-          moderators={this.state.moderators}
+          community={this.state.postRes.community}
+          moderators={this.state.postRes.moderators}
           admins={this.state.siteRes.admins}
-          online={this.state.online}
+          online={this.state.postRes.online}
           enableNsfw={this.state.siteRes.site.enable_nsfw}
           showIcon
+          categories={this.state.categories}
         />
       </div>
     );
@@ -368,7 +376,7 @@ export class Post extends Component<any, PostState> {
 
   buildCommentsTree(): CommentNodeI[] {
     let map = new Map<number, CommentNodeI>();
-    for (let comment of this.state.comments) {
+    for (let comment of this.state.postRes.comments) {
       let node: CommentNodeI = {
         comment: comment,
         children: [],
@@ -376,7 +384,7 @@ export class Post extends Component<any, PostState> {
       map.set(comment.id, { ...node });
     }
     let tree: CommentNodeI[] = [];
-    for (let comment of this.state.comments) {
+    for (let comment of this.state.postRes.comments) {
       let child = map.get(comment.id);
       if (comment.parent_id) {
         let parent_ = map.get(comment.parent_id);
@@ -404,10 +412,10 @@ export class Post extends Component<any, PostState> {
       <div>
         <CommentNodes
           nodes={nodes}
-          locked={this.state.post.locked}
-          moderators={this.state.moderators}
+          locked={this.state.postRes.post.locked}
+          moderators={this.state.postRes.moderators}
           admins={this.state.siteRes.admins}
-          postCreatorId={this.state.post.creator_id}
+          postCreatorId={this.state.postRes.post.creator_id}
           sort={this.state.commentSort}
           enableDownvotes={this.state.siteRes.site.enable_downvotes}
         />
@@ -427,17 +435,13 @@ export class Post extends Component<any, PostState> {
       });
     } else if (res.op == UserOperation.GetPost) {
       let data = res.data as GetPostResponse;
-      this.state.post = data.post;
-      this.state.comments = data.comments;
-      this.state.community = data.community;
-      this.state.moderators = data.moderators;
-      this.state.online = data.online;
+      this.state.postRes = data;
       this.state.loading = false;
 
       // Get cross-posts
-      if (this.state.post.url) {
+      if (this.state.postRes.post.url) {
         let form: SearchForm = {
-          q: this.state.post.url,
+          q: this.state.postRes.post.url,
           type_: SearchType.Url,
           sort: SortType.TopAll,
           page: 1,
@@ -453,7 +457,7 @@ export class Post extends Component<any, PostState> {
 
       // Necessary since it might be a user reply
       if (data.recipient_ids.length == 0) {
-        this.state.comments.unshift(data.comment);
+        this.state.postRes.comments.unshift(data.comment);
         this.setState(this.state);
       }
     } else if (
@@ -462,20 +466,20 @@ export class Post extends Component<any, PostState> {
       res.op == UserOperation.RemoveComment
     ) {
       let data = res.data as CommentResponse;
-      editCommentRes(data, this.state.comments);
+      editCommentRes(data, this.state.postRes.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.SaveComment) {
       let data = res.data as CommentResponse;
-      saveCommentRes(data, this.state.comments);
+      saveCommentRes(data, this.state.postRes.comments);
       this.setState(this.state);
       setupTippy();
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      createCommentLikeRes(data, this.state.comments);
+      createCommentLikeRes(data, this.state.postRes.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      createPostLikeRes(data, this.state.post);
+      createPostLikeRes(data, this.state.postRes.post);
       this.setState(this.state);
     } else if (
       res.op == UserOperation.EditPost ||
@@ -485,12 +489,12 @@ export class Post extends Component<any, PostState> {
       res.op == UserOperation.StickyPost
     ) {
       let data = res.data as PostResponse;
-      this.state.post = data.post;
+      this.state.postRes.post = data.post;
       this.setState(this.state);
       setupTippy();
     } else if (res.op == UserOperation.SavePost) {
       let data = res.data as PostResponse;
-      this.state.post = data.post;
+      this.state.postRes.post = data.post;
       this.setState(this.state);
       setupTippy();
     } else if (
@@ -499,36 +503,36 @@ export class Post extends Component<any, PostState> {
       res.op == UserOperation.RemoveCommunity
     ) {
       let data = res.data as CommunityResponse;
-      this.state.community = data.community;
-      this.state.post.community_id = data.community.id;
-      this.state.post.community_name = data.community.name;
+      this.state.postRes.community = data.community;
+      this.state.postRes.post.community_id = data.community.id;
+      this.state.postRes.post.community_name = data.community.name;
       this.setState(this.state);
     } else if (res.op == UserOperation.FollowCommunity) {
       let data = res.data as CommunityResponse;
-      this.state.community.subscribed = data.community.subscribed;
-      this.state.community.number_of_subscribers =
+      this.state.postRes.community.subscribed = data.community.subscribed;
+      this.state.postRes.community.number_of_subscribers =
         data.community.number_of_subscribers;
       this.setState(this.state);
     } else if (res.op == UserOperation.BanFromCommunity) {
       let data = res.data as BanFromCommunityResponse;
-      this.state.comments
+      this.state.postRes.comments
         .filter(c => c.creator_id == data.user.id)
         .forEach(c => (c.banned_from_community = data.banned));
-      if (this.state.post.creator_id == data.user.id) {
-        this.state.post.banned_from_community = data.banned;
+      if (this.state.postRes.post.creator_id == data.user.id) {
+        this.state.postRes.post.banned_from_community = data.banned;
       }
       this.setState(this.state);
     } else if (res.op == UserOperation.AddModToCommunity) {
       let data = res.data as AddModToCommunityResponse;
-      this.state.moderators = data.moderators;
+      this.state.postRes.moderators = data.moderators;
       this.setState(this.state);
     } else if (res.op == UserOperation.BanUser) {
       let data = res.data as BanUserResponse;
-      this.state.comments
+      this.state.postRes.comments
         .filter(c => c.creator_id == data.user.id)
         .forEach(c => (c.banned = data.banned));
-      if (this.state.post.creator_id == data.user.id) {
-        this.state.post.banned = data.banned;
+      if (this.state.postRes.post.creator_id == data.user.id) {
+        this.state.postRes.post.banned = data.banned;
       }
       this.setState(this.state);
     } else if (res.op == UserOperation.AddAdmin) {
@@ -541,20 +545,21 @@ export class Post extends Component<any, PostState> {
         p => p.id != Number(this.props.match.params.id)
       );
       if (this.state.crossPosts.length) {
-        this.state.post.duplicates = this.state.crossPosts;
+        this.state.postRes.post.duplicates = this.state.crossPosts;
       }
       this.setState(this.state);
-    } else if (
-      res.op == UserOperation.TransferSite ||
-      res.op == UserOperation.GetSite
-    ) {
+    } else if (res.op == UserOperation.TransferSite) {
       let data = res.data as GetSiteResponse;
       this.state.siteRes = data;
       this.setState(this.state);
     } else if (res.op == UserOperation.TransferCommunity) {
       let data = res.data as GetCommunityResponse;
-      this.state.community = data.community;
-      this.state.moderators = data.moderators;
+      this.state.postRes.community = data.community;
+      this.state.postRes.moderators = data.moderators;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.ListCategories) {
+      let data = res.data as ListCategoriesResponse;
+      this.state.categories = data.categories;
       this.setState(this.state);
     }
   }
diff --git a/src/shared/components/user-details.tsx b/src/shared/components/user-details.tsx
index c249689..834000e 100644
--- a/src/shared/components/user-details.tsx
+++ b/src/shared/components/user-details.tsx
@@ -1,38 +1,13 @@
 import { Component, linkEvent } from 'inferno';
-import { WebSocketService, UserService } from '../services';
-import { Subscription } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
 import { i18n } from '../i18next';
-import {
-  UserOperation,
-  Post,
-  Comment,
-  CommunityUser,
-  SortType,
-  UserDetailsResponse,
-  UserView,
-  WebSocketJsonResponse,
-  CommentResponse,
-  BanUserResponse,
-  PostResponse,
-} from 'lemmy-js-client';
+import { Post, Comment, SortType, UserDetailsResponse } from 'lemmy-js-client';
 import { UserDetailsView } from '../interfaces';
-import {
-  wsJsonToRes,
-  toast,
-  commentsToFlatNodes,
-  setupTippy,
-  editCommentRes,
-  saveCommentRes,
-  createCommentLikeRes,
-  createPostLikeFindRes,
-} from '../utils';
+import { commentsToFlatNodes, setupTippy } from '../utils';
 import { PostListing } from './post-listing';
 import { CommentNodes } from './comment-nodes';
 
 interface UserDetailsProps {
-  username?: string;
-  user_id?: number;
+  userRes: UserDetailsResponse;
   page: number;
   limit: number;
   sort: SortType;
@@ -40,67 +15,29 @@ interface UserDetailsProps {
   enableNsfw: boolean;
   view: UserDetailsView;
   onPageChange(page: number): number | any;
-  admins: UserView[];
 }
 
-interface UserDetailsState {
-  follows: CommunityUser[];
-  moderates: CommunityUser[];
-  comments: Comment[];
-  posts: Post[];
-  saved?: Post[];
-}
+interface UserDetailsState {}
 
 export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
-  private subscription: Subscription;
   constructor(props: any, context: any) {
     super(props, context);
-
-    this.state = {
-      follows: [],
-      moderates: [],
-      comments: [],
-      posts: [],
-      saved: [],
-    };
-
-    this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        msg => this.parseMessage(msg),
-        err => console.error(err),
-        () => console.log('complete')
-      );
-  }
-
-  componentWillUnmount() {
-    this.subscription.unsubscribe();
   }
 
+  // TODO needed here?
   componentDidMount() {
-    this.fetchUserData();
     setupTippy();
   }
 
-  componentDidUpdate(lastProps: UserDetailsProps) {
-    for (const key of Object.keys(lastProps)) {
-      if (lastProps[key] !== this.props[key]) {
-        this.fetchUserData();
-        break;
-      }
-    }
-  }
-
-  fetchUserData() {
-    WebSocketService.Instance.getUserDetails({
-      user_id: this.props.user_id,
-      username: this.props.username,
-      sort: this.props.sort,
-      saved_only: this.props.view === UserDetailsView.Saved,
-      page: this.props.page,
-      limit: this.props.limit,
-    });
-  }
+  // TODO wut?
+  // componentDidUpdate(lastProps: UserDetailsProps) {
+  //   for (const key of Object.keys(lastProps)) {
+  //     if (lastProps[key] !== this.props[key]) {
+  //       this.fetchUserData();
+  //       break;
+  //     }
+  //   }
+  // }
 
   render() {
     return (
@@ -114,20 +51,20 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
   viewSelector(view: UserDetailsView) {
     if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
       return this.overview();
-    }
-    if (view === UserDetailsView.Comments) {
+    } else if (view === UserDetailsView.Comments) {
       return this.comments();
-    }
-    if (view === UserDetailsView.Posts) {
+    } else if (view === UserDetailsView.Posts) {
       return this.posts();
+    } else {
+      return null;
     }
   }
 
   overview() {
-    const comments = this.state.comments.map((c: Comment) => {
+    const comments = this.props.userRes.comments.map((c: Comment) => {
       return { type: 'comments', data: c };
     });
-    const posts = this.state.posts.map((p: Post) => {
+    const posts = this.props.userRes.posts.map((p: Post) => {
       return { type: 'posts', data: p };
     });
 
@@ -150,9 +87,10 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
             <div>
               {i.type === 'posts' ? (
                 <PostListing
+                  communities={[]}
                   key={(i.data as Post).id}
                   post={i.data as Post}
-                  admins={this.props.admins}
+                  admins={this.props.userRes.admins}
                   showCommunity
                   enableDownvotes={this.props.enableDownvotes}
                   enableNsfw={this.props.enableNsfw}
@@ -161,7 +99,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
                 <CommentNodes
                   key={(i.data as Comment).id}
                   nodes={[{ comment: i.data as Comment }]}
-                  admins={this.props.admins}
+                  admins={this.props.userRes.admins}
                   noBorder
                   noIndent
                   showCommunity
@@ -181,8 +119,8 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
     return (
       <div>
         <CommentNodes
-          nodes={commentsToFlatNodes(this.state.comments)}
-          admins={this.props.admins}
+          nodes={commentsToFlatNodes(this.props.userRes.comments)}
+          admins={this.props.userRes.admins}
           noIndent
           showCommunity
           showContext
@@ -195,11 +133,12 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
   posts() {
     return (
       <div>
-        {this.state.posts.map(post => (
+        {this.props.userRes.posts.map(post => (
           <>
             <PostListing
+              communities={[]}
               post={post}
-              admins={this.props.admins}
+              admins={this.props.userRes.admins}
               showCommunity
               enableDownvotes={this.props.enableDownvotes}
               enableNsfw={this.props.enableNsfw}
@@ -222,7 +161,8 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
             {i18n.t('prev')}
           </button>
         )}
-        {this.state.comments.length + this.state.posts.length > 0 && (
+        {this.props.userRes.comments.length + this.props.userRes.posts.length >
+          0 && (
           <button
             class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
@@ -241,75 +181,4 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
   prevPage(i: UserDetails) {
     i.props.onPageChange(i.props.page - 1);
   }
-
-  parseMessage(msg: WebSocketJsonResponse) {
-    console.log(msg);
-    const res = wsJsonToRes(msg);
-
-    if (msg.error) {
-      toast(i18n.t(msg.error), 'danger');
-      if (msg.error == 'couldnt_find_that_username_or_email') {
-        this.context.router.history.push('/');
-      }
-      return;
-    } else if (msg.reconnect) {
-      this.fetchUserData();
-    } else if (res.op == UserOperation.GetUserDetails) {
-      const data = res.data as UserDetailsResponse;
-      this.setState({
-        comments: data.comments,
-        follows: data.follows,
-        moderates: data.moderates,
-        posts: data.posts,
-      });
-    } else if (res.op == UserOperation.CreateCommentLike) {
-      const data = res.data as CommentResponse;
-      createCommentLikeRes(data, this.state.comments);
-      this.setState({
-        comments: this.state.comments,
-      });
-    } else if (
-      res.op == UserOperation.EditComment ||
-      res.op == UserOperation.DeleteComment ||
-      res.op == UserOperation.RemoveComment
-    ) {
-      const data = res.data as CommentResponse;
-      editCommentRes(data, this.state.comments);
-      this.setState({
-        comments: this.state.comments,
-      });
-    } else if (res.op == UserOperation.CreateComment) {
-      const data = res.data as CommentResponse;
-      if (
-        UserService.Instance.user &&
-        data.comment.creator_id == UserService.Instance.user.id
-      ) {
-        toast(i18n.t('reply_sent'));
-      }
-    } else if (res.op == UserOperation.SaveComment) {
-      const data = res.data as CommentResponse;
-      saveCommentRes(data, this.state.comments);
-      this.setState({
-        comments: this.state.comments,
-      });
-    } else if (res.op == UserOperation.CreatePostLike) {
-      const data = res.data as PostResponse;
-      createPostLikeFindRes(data, this.state.posts);
-      this.setState({
-        posts: this.state.posts,
-      });
-    } else if (res.op == UserOperation.BanUser) {
-      const data = res.data as BanUserResponse;
-      this.state.comments
-        .filter(c => c.creator_id == data.user.id)
-        .forEach(c => (c.banned = data.banned));
-      this.state.posts
-        .filter(c => c.creator_id == data.user.id)
-        .forEach(c => (c.banned = data.banned));
-      this.setState({
-        posts: this.state.posts,
-        comments: this.state.comments,
-      });
-    }
-  }
 }
diff --git a/src/shared/components/user.tsx b/src/shared/components/user.tsx
index a2d0e9a..84d9cd1 100644
--- a/src/shared/components/user.tsx
+++ b/src/shared/components/user.tsx
@@ -2,13 +2,10 @@ import { Component, linkEvent } from 'inferno';
 import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
 import {
   UserOperation,
-  CommunityUser,
   SortType,
   ListingType,
-  UserView,
   UserSettingsForm,
   LoginResponse,
   DeleteAccountForm,
@@ -16,6 +13,10 @@ import {
   GetSiteResponse,
   UserDetailsResponse,
   AddAdminResponse,
+  GetUserDetailsForm,
+  CommentResponse,
+  PostResponse,
+  BanUserResponse,
 } from 'lemmy-js-client';
 import { UserDetailsView } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
@@ -33,6 +34,16 @@ import {
   mdToHtml,
   elementUrl,
   favIconUrl,
+  setIsoData,
+  getIdFromProps,
+  getUsernameFromProps,
+  wsSubscribe,
+  createCommentLikeRes,
+  editCommentRes,
+  saveCommentRes,
+  createPostLikeFindRes,
+  setAuth,
+  lemmyHttp,
 } from '../utils';
 import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
@@ -46,11 +57,9 @@ import { ImageUploadForm } from './image-upload-form';
 import { BannerIconHeader } from './banner-icon-header';
 
 interface UserState {
-  user: UserView;
-  user_id: number;
-  username: string;
-  follows: CommunityUser[];
-  moderates: CommunityUser[];
+  userRes: UserDetailsResponse;
+  userId: number;
+  userName: string;
   view: UserDetailsView;
   sort: SortType;
   page: number;
@@ -78,25 +87,12 @@ interface UrlParams {
 }
 
 export class User extends Component<any, UserState> {
+  private isoData = setIsoData(this.context);
   private subscription: Subscription;
   private emptyState: UserState = {
-    user: {
-      id: null,
-      name: null,
-      published: null,
-      number_of_posts: null,
-      post_score: null,
-      number_of_comments: null,
-      comment_score: null,
-      banned: null,
-      avatar: null,
-      actor_id: null,
-      local: null,
-    },
-    user_id: null,
-    username: null,
-    follows: [],
-    moderates: [],
+    userRes: undefined,
+    userId: getIdFromProps(this.props),
+    userName: getUsernameFromProps(this.props),
     loading: true,
     view: User.getViewFromProps(this.props.match.view),
     sort: User.getSortTypeFromProps(this.props.match.sort),
@@ -119,31 +115,7 @@ export class User extends Component<any, UserState> {
     deleteAccountForm: {
       password: null,
     },
-    siteRes: {
-      admins: [],
-      banned: [],
-      online: undefined,
-      site: {
-        id: undefined,
-        name: undefined,
-        creator_id: undefined,
-        published: undefined,
-        creator_name: undefined,
-        number_of_users: undefined,
-        number_of_posts: undefined,
-        number_of_comments: undefined,
-        number_of_communities: undefined,
-        enable_downvotes: undefined,
-        open_registration: undefined,
-        enable_nsfw: undefined,
-        icon: undefined,
-        banner: undefined,
-        creator_preferred_username: undefined,
-      },
-      version: undefined,
-      my_user: undefined,
-      federated_instances: undefined,
-    },
+    siteRes: this.isoData.site,
   };
 
   constructor(props: any, context: any) {
@@ -168,25 +140,37 @@ export class User extends Component<any, UserState> {
     this.handleBannerUpload = this.handleBannerUpload.bind(this);
     this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
-    this.state.user_id = Number(this.props.match.params.id) || null;
-    this.state.username = this.props.match.params.username;
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
 
-    this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        msg => this.parseMessage(msg),
-        err => console.error(err),
-        () => console.log('complete')
-      );
+    // Only fetch the data if coming from another route
+    if (this.isoData.path == this.context.router.route.match.url) {
+      this.state.userRes = this.isoData.routeData[0];
+      this.setUserInfo();
+      this.state.loading = false;
+    } else {
+      this.fetchUserData();
+    }
 
-    WebSocketService.Instance.getSite();
     setupTippy();
   }
 
+  fetchUserData() {
+    let form: GetUserDetailsForm = {
+      user_id: this.state.userId,
+      username: this.state.userName,
+      sort: this.state.sort,
+      saved_only: this.state.view === UserDetailsView.Saved,
+      page: this.state.page,
+      limit: fetchLimit,
+    };
+    WebSocketService.Instance.getUserDetails(form);
+  }
+
   get isCurrentUser() {
     return (
       UserService.Instance.user &&
-      UserService.Instance.user.id == this.state.user.id
+      UserService.Instance.user.id == this.state.userRes.user.id
     );
   }
 
@@ -202,6 +186,44 @@ export class User extends Component<any, UserState> {
     return page ? Number(page) : 1;
   }
 
+  static fetchInitialData(auth: string, path: string): Promise<any>[] {
+    let pathSplit = path.split('/');
+    let promises: Promise<any>[] = [];
+
+    // It can be /u/me, or /username/1
+    let idOrName = pathSplit[2];
+    let user_id: number;
+    let username: string;
+    if (isNaN(Number(idOrName))) {
+      username = idOrName;
+    } else {
+      user_id = Number(idOrName);
+    }
+
+    let view = this.getViewFromProps(pathSplit[4]);
+    let sort = this.getSortTypeFromProps(pathSplit[6]);
+    let page = this.getPageFromProps(Number(pathSplit[8]));
+
+    let form: GetUserDetailsForm = {
+      sort,
+      saved_only: view === UserDetailsView.Saved,
+      page,
+      limit: fetchLimit,
+    };
+    this.setIdOrName(form, user_id, username);
+    setAuth(form, auth);
+    promises.push(lemmyHttp.getUserDetails(form));
+    return promises;
+  }
+
+  static setIdOrName(obj: any, id: number, name_: string) {
+    if (id) {
+      obj.user_id = id;
+    } else {
+      obj.username = name_;
+    }
+  }
+
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
@@ -229,7 +251,7 @@ export class User extends Component<any, UserState> {
 
   get documentTitle(): string {
     if (this.state.siteRes.site.name) {
-      return `@${this.state.username} - ${this.state.siteRes.site.name}`;
+      return `@${this.state.userName} - ${this.state.siteRes.site.name}`;
     } else {
       return 'Lemmy';
     }
@@ -252,43 +274,41 @@ export class User extends Component<any, UserState> {
             href={this.favIcon}
           />
         </Helmet>
-        <div class="row">
-          <div class="col-12 col-md-8">
-            {this.state.loading ? (
-              <h5>
-                <svg class="icon icon-spinner spin">
-                  <use xlinkHref="#icon-spinner"></use>
-                </svg>
-              </h5>
-            ) : (
+        {this.state.loading ? (
+          <h5>
+            <svg class="icon icon-spinner spin">
+              <use xlinkHref="#icon-spinner"></use>
+            </svg>
+          </h5>
+        ) : (
+          <div class="row">
+            <div class="col-12 col-md-8">
               <>
                 {this.userInfo()}
                 <hr />
               </>
+              {!this.state.loading && this.selects()}
+              <UserDetails
+                userRes={this.state.userRes}
+                sort={this.state.sort}
+                page={this.state.page}
+                limit={fetchLimit}
+                enableDownvotes={this.state.siteRes.site.enable_downvotes}
+                enableNsfw={this.state.siteRes.site.enable_nsfw}
+                view={this.state.view}
+                onPageChange={this.handlePageChange}
+              />
+            </div>
+
+            {!this.state.loading && (
+              <div class="col-12 col-md-4">
+                {this.isCurrentUser && this.userSettings()}
+                {this.moderates()}
+                {this.follows()}
+              </div>
             )}
-            {!this.state.loading && this.selects()}
-            <UserDetails
-              user_id={this.state.user_id}
-              username={this.state.username}
-              sort={this.state.sort}
-              page={this.state.page}
-              limit={fetchLimit}
-              enableDownvotes={this.state.siteRes.site.enable_downvotes}
-              enableNsfw={this.state.siteRes.site.enable_nsfw}
-              admins={this.state.siteRes.admins}
-              view={this.state.view}
-              onPageChange={this.handlePageChange}
-            />
           </div>
-
-          {!this.state.loading && (
-            <div class="col-12 col-md-4">
-              {this.isCurrentUser && this.userSettings()}
-              {this.moderates()}
-              {this.follows()}
-            </div>
-          )}
-        </div>
+        )}
       </div>
     );
   }
@@ -362,7 +382,7 @@ export class User extends Component<any, UserState> {
           hideHot
         />
         <a
-          href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
+          href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
           target="_blank"
           rel="noopener"
           title="RSS"
@@ -376,14 +396,11 @@ export class User extends Component<any, UserState> {
   }
 
   userInfo() {
-    let user = this.state.user;
+    let user = this.state.userRes.user;
 
     return (
       <div>
-        <BannerIconHeader
-          banner={this.state.user.banner}
-          icon={this.state.user.avatar}
-        />
+        <BannerIconHeader banner={user.banner} icon={user.avatar} />
         <div class="mb-3">
           <div class="">
             <div class="mb-0 d-flex flex-wrap">
@@ -420,17 +437,17 @@ export class User extends Component<any, UserState> {
                 <>
                   <a
                     className={`d-flex align-self-start btn btn-secondary ml-2 ${
-                      !this.state.user.matrix_user_id && 'invisible'
+                      !user.matrix_user_id && 'invisible'
                     }`}
                     target="_blank"
                     rel="noopener"
-                    href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+                    href={`https://matrix.to/#/${user.matrix_user_id}`}
                   >
                     {i18n.t('send_secure_message')}
                   </a>
                   <Link
                     class="d-flex align-self-start btn btn-secondary ml-2"
-                    to={`/create_private_message/recipient/${this.state.user.id}`}
+                    to={`/create_private_message/recipient/${user.id}`}
                   >
                     {i18n.t('send_message')}
                   </Link>
@@ -818,12 +835,12 @@ export class User extends Component<any, UserState> {
   moderates() {
     return (
       <div>
-        {this.state.moderates.length > 0 && (
+        {this.state.userRes.moderates.length > 0 && (
           <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('moderates')}</h5>
               <ul class="list-unstyled mb-0">
-                {this.state.moderates.map(community => (
+                {this.state.userRes.moderates.map(community => (
                   <li>
                     <Link to={`/c/${community.community_name}`}>
                       {community.community_name}
@@ -841,12 +858,12 @@ export class User extends Component<any, UserState> {
   follows() {
     return (
       <div>
-        {this.state.follows.length > 0 && (
+        {this.state.userRes.follows.length > 0 && (
           <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('subscribed')}</h5>
               <ul class="list-unstyled mb-0">
-                {this.state.follows.map(community => (
+                {this.state.userRes.follows.map(community => (
                   <li>
                     <Link to={`/c/${community.community_name}`}>
                       {community.community_name}
@@ -866,7 +883,7 @@ export class User extends Component<any, UserState> {
     const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
     const sortStr = paramUpdates.sort || this.state.sort;
     this.props.history.push(
-      `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
+      `/u/${this.state.userName}/view/${viewStr}/sort/${sortStr}/page/${page}`
     );
   }
 
@@ -966,7 +983,7 @@ export class User extends Component<any, UserState> {
     i.state.userSettingsForm.matrix_user_id = event.target.value;
     if (
       i.state.userSettingsForm.matrix_user_id == '' &&
-      !i.state.user.matrix_user_id
+      !i.state.userRes.user.matrix_user_id
     ) {
       i.state.userSettingsForm.matrix_user_id = undefined;
     }
@@ -1029,6 +1046,33 @@ export class User extends Component<any, UserState> {
     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
   }
 
+  setUserInfo() {
+    if (this.isCurrentUser) {
+      this.state.userSettingsForm.show_nsfw =
+        UserService.Instance.user.show_nsfw;
+      this.state.userSettingsForm.theme = UserService.Instance.user.theme
+        ? UserService.Instance.user.theme
+        : 'darkly';
+      this.state.userSettingsForm.default_sort_type =
+        UserService.Instance.user.default_sort_type;
+      this.state.userSettingsForm.default_listing_type =
+        UserService.Instance.user.default_listing_type;
+      this.state.userSettingsForm.lang = UserService.Instance.user.lang;
+      this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+      this.state.userSettingsForm.banner = UserService.Instance.user.banner;
+      this.state.userSettingsForm.preferred_username =
+        UserService.Instance.user.preferred_username;
+      this.state.userSettingsForm.show_avatars =
+        UserService.Instance.user.show_avatars;
+      this.state.userSettingsForm.email = UserService.Instance.user.email;
+      this.state.userSettingsForm.bio = UserService.Instance.user.bio;
+      this.state.userSettingsForm.send_notifications_to_email =
+        UserService.Instance.user.send_notifications_to_email;
+      this.state.userSettingsForm.matrix_user_id =
+        UserService.Instance.user.matrix_user_id;
+    }
+  }
+
   parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
     const res = wsJsonToRes(msg);
@@ -1042,50 +1086,24 @@ export class User extends Component<any, UserState> {
         userSettingsLoading: false,
       });
       return;
+    } else if (msg.reconnect) {
+      this.fetchUserData();
     } else if (res.op == UserOperation.GetUserDetails) {
       // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
       // and set the parent state if it is not set or differs
+      // TODO this might need to get abstracted
       const data = res.data as UserDetailsResponse;
-
-      if (this.state.user.id !== data.user.id) {
-        this.state.user = data.user;
-        this.state.follows = data.follows;
-        this.state.moderates = data.moderates;
-
-        if (this.isCurrentUser) {
-          this.state.userSettingsForm.show_nsfw =
-            UserService.Instance.user.show_nsfw;
-          this.state.userSettingsForm.theme = UserService.Instance.user.theme
-            ? UserService.Instance.user.theme
-            : 'darkly';
-          this.state.userSettingsForm.default_sort_type =
-            UserService.Instance.user.default_sort_type;
-          this.state.userSettingsForm.default_listing_type =
-            UserService.Instance.user.default_listing_type;
-          this.state.userSettingsForm.lang = UserService.Instance.user.lang;
-          this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
-          this.state.userSettingsForm.banner = UserService.Instance.user.banner;
-          this.state.userSettingsForm.preferred_username =
-            UserService.Instance.user.preferred_username;
-          this.state.userSettingsForm.show_avatars =
-            UserService.Instance.user.show_avatars;
-          this.state.userSettingsForm.email = UserService.Instance.user.email;
-          this.state.userSettingsForm.bio = UserService.Instance.user.bio;
-          this.state.userSettingsForm.send_notifications_to_email =
-            UserService.Instance.user.send_notifications_to_email;
-          this.state.userSettingsForm.matrix_user_id =
-            UserService.Instance.user.matrix_user_id;
-        }
-        this.state.loading = false;
-        this.setState(this.state);
-      }
+      this.state.userRes = data;
+      this.setUserInfo();
+      this.state.loading = false;
+      this.setState(this.state);
     } else if (res.op == UserOperation.SaveUserSettings) {
       const data = res.data as LoginResponse;
       UserService.Instance.login(data);
-      this.state.user.bio = this.state.userSettingsForm.bio;
-      this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
-      this.state.user.banner = this.state.userSettingsForm.banner;
-      this.state.user.avatar = this.state.userSettingsForm.avatar;
+      this.state.userRes.user.bio = this.state.userSettingsForm.bio;
+      this.state.userRes.user.preferred_username = this.state.userSettingsForm.preferred_username;
+      this.state.userRes.user.banner = this.state.userSettingsForm.banner;
+      this.state.userRes.user.avatar = this.state.userSettingsForm.avatar;
       this.state.userSettingsLoading = false;
       this.setState(this.state);
 
@@ -1096,14 +1114,47 @@ export class User extends Component<any, UserState> {
         deleteAccountShowConfirm: false,
       });
       this.context.router.history.push('/');
-    } else if (res.op == UserOperation.GetSite) {
-      const data = res.data as GetSiteResponse;
-      this.state.siteRes = data;
-      this.setState(this.state);
     } else if (res.op == UserOperation.AddAdmin) {
       const data = res.data as AddAdminResponse;
       this.state.siteRes.admins = data.admins;
       this.setState(this.state);
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      const data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.userRes.comments);
+      this.setState(this.state);
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
+      const data = res.data as CommentResponse;
+      editCommentRes(data, this.state.userRes.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateComment) {
+      const data = res.data as CommentResponse;
+      if (
+        UserService.Instance.user &&
+        data.comment.creator_id == UserService.Instance.user.id
+      ) {
+        toast(i18n.t('reply_sent'));
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      const data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.userRes.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreatePostLike) {
+      const data = res.data as PostResponse;
+      createPostLikeFindRes(data, this.state.userRes.posts);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.BanUser) {
+      const data = res.data as BanUserResponse;
+      this.state.userRes.comments
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
+      this.state.userRes.posts
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
+      this.setState(this.state);
     }
   }
 }
diff --git a/src/shared/routes.ts b/src/shared/routes.ts
index 6580db4..14d5495 100644
--- a/src/shared/routes.ts
+++ b/src/shared/routes.ts
@@ -64,8 +64,13 @@ export const routes: IRoutePropsWithFetch[] = [
   {
     path: `/post/:id/comment/:comment_id`,
     component: Post,
+    fetchInitialData: (auth, path) => Post.fetchInitialData(auth, path),
+  },
+  {
+    path: `/post/:id`,
+    component: Post,
+    fetchInitialData: (auth, path) => Post.fetchInitialData(auth, path),
   },
-  { path: `/post/:id`, component: Post },
   {
     path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
     component: Community,
@@ -84,10 +89,23 @@ export const routes: IRoutePropsWithFetch[] = [
   {
     path: `/u/:username/view/:view/sort/:sort/page/:page`,
     component: User,
+    fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
+  },
+  {
+    path: `/user/:id`,
+    component: User,
+    fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
+  },
+  {
+    path: `/u/:username`,
+    component: User,
+    fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
+  },
+  {
+    path: `/inbox`,
+    component: Inbox,
+    fetchInitialData: (auth, path) => Inbox.fetchInitialData(auth, path),
   },
-  { path: `/user/:id`, component: User },
-  { path: `/u/:username`, component: User },
-  { path: `/inbox`, component: Inbox },
   {
     path: `/modlog/community/:community_id`,
     component: Modlog,
diff --git a/src/shared/services/UserService.ts b/src/shared/services/UserService.ts
index dce3309..785ce3f 100644
--- a/src/shared/services/UserService.ts
+++ b/src/shared/services/UserService.ts
@@ -31,7 +31,9 @@ export class UserService {
 
   public login(res: LoginResponse) {
     this.setClaims(res.jwt);
-    IsomorphicCookie.save('jwt', res.jwt, { expires: 365 });
+    let expires = new Date();
+    expires.setDate(expires.getDate() + 365);
+    IsomorphicCookie.save('jwt', res.jwt, { expires });
     console.log('jwt cookie set');
   }
 
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 5ec209d..e48edf0 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -845,6 +845,18 @@ export function getRecipientIdFromProps(props: any): number {
     : 1;
 }
 
+export function getIdFromProps(props: any): number {
+  return Number(props.match.params.id);
+}
+
+export function getCommentIdFromProps(props: any): number {
+  return Number(props.match.params.comment_id);
+}
+
+export function getUsernameFromProps(props: any): string {
+  return props.match.params.username;
+}
+
 export function editCommentRes(data: CommentResponse, comments: Comment[]) {
   let found = comments.find(c => c.id == data.comment.id);
   if (found) {