]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/person/inbox.tsx
component classes v2
[lemmy-ui.git] / src / shared / components / person / inbox.tsx
index 05b75ec7afd54accbed34ed092d57467d15a47cd..415c3e3fd6c9ee216ba5b37b9d5b9b266449276c 100644 (file)
@@ -1,50 +1,76 @@
 import { Component, linkEvent } from "inferno";
 import {
-  BlockPersonResponse,
+  AddAdmin,
+  AddModToCommunity,
+  BanFromCommunity,
+  BanFromCommunityResponse,
+  BanPerson,
+  BanPersonResponse,
+  BlockPerson,
+  CommentId,
+  CommentReplyResponse,
+  CommentReplyView,
   CommentReportResponse,
   CommentResponse,
+  CommentSortType,
   CommentView,
-  GetPersonMentions,
+  CreateComment,
+  CreateCommentLike,
+  CreateCommentReport,
+  CreatePrivateMessage,
+  CreatePrivateMessageReport,
+  DeleteComment,
+  DeletePrivateMessage,
+  DistinguishComment,
+  EditComment,
+  EditPrivateMessage,
   GetPersonMentionsResponse,
-  GetPrivateMessages,
-  GetReplies,
   GetRepliesResponse,
+  GetSiteResponse,
+  MarkCommentReplyAsRead,
+  MarkPersonMentionAsRead,
+  MarkPrivateMessageAsRead,
   PersonMentionResponse,
   PersonMentionView,
-  PostReportResponse,
+  PrivateMessageReportResponse,
   PrivateMessageResponse,
-  PrivateMessagesResponse,
   PrivateMessageView,
-  SiteView,
-  SortType,
-  UserOperation,
+  PrivateMessagesResponse,
+  PurgeComment,
+  PurgeItemResponse,
+  PurgePerson,
+  PurgePost,
+  RemoveComment,
+  SaveComment,
+  TransferCommunity,
 } from "lemmy-js-client";
-import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
-import { InitialFetchRequest } from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { CommentViewType, InitialFetchRequest } from "../../interfaces";
+import { UserService } from "../../services";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
 import {
-  authField,
+  RouteDataResponse,
   commentsToFlatNodes,
-  createCommentLikeRes,
-  editCommentRes,
+  editCommentReply,
+  editMention,
+  editPrivateMessage,
+  editWith,
+  enableDownvotes,
   fetchLimit,
-  isBrowser,
-  saveCommentRes,
+  getCommentParentId,
+  myAuth,
+  myAuthRequired,
+  relTags,
   setIsoData,
-  setupTippy,
   toast,
   updatePersonBlock,
-  wsClient,
-  wsJsonToRes,
-  wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentNodes } from "../comment/comment-nodes";
+import { CommentSortSelect } from "../common/comment-sort-select";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
 import { Paginator } from "../common/paginator";
-import { SortSelect } from "../common/sort-select";
 import { PrivateMessage } from "../private_message/private-message";
 
 enum UnreadOrAll {
@@ -64,140 +90,201 @@ enum ReplyEnum {
   Mention,
   Message,
 }
+
+type InboxData = RouteDataResponse<{
+  repliesRes: GetRepliesResponse;
+  mentionsRes: GetPersonMentionsResponse;
+  messagesRes: PrivateMessagesResponse;
+}>;
+
 type ReplyType = {
   id: number;
   type_: ReplyEnum;
-  view: CommentView | PrivateMessageView | PersonMentionView;
+  view: CommentView | PrivateMessageView | PersonMentionView | CommentReplyView;
   published: string;
 };
 
 interface InboxState {
   unreadOrAll: UnreadOrAll;
   messageType: MessageType;
-  replies: CommentView[];
-  mentions: PersonMentionView[];
-  messages: PrivateMessageView[];
-  combined: ReplyType[];
-  sort: SortType;
+  repliesRes: RequestState<GetRepliesResponse>;
+  mentionsRes: RequestState<GetPersonMentionsResponse>;
+  messagesRes: RequestState<PrivateMessagesResponse>;
+  markAllAsReadRes: RequestState<GetRepliesResponse>;
+  sort: CommentSortType;
   page: number;
-  site_view: SiteView;
-  loading: boolean;
+  siteRes: GetSiteResponse;
+  finished: Map<CommentId, boolean | undefined>;
+  isIsomorphic: boolean;
 }
 
 export class Inbox extends Component<any, InboxState> {
-  private isoData = setIsoData(this.context);
-  private subscription: Subscription;
-  private emptyState: InboxState = {
+  private isoData = setIsoData<InboxData>(this.context);
+  state: InboxState = {
     unreadOrAll: UnreadOrAll.Unread,
     messageType: MessageType.All,
-    replies: [],
-    mentions: [],
-    messages: [],
-    combined: [],
-    sort: SortType.New,
+    sort: "New",
     page: 1,
-    site_view: this.isoData.site_res.site_view,
-    loading: true,
+    siteRes: this.isoData.site_res,
+    repliesRes: { state: "empty" },
+    mentionsRes: { state: "empty" },
+    messagesRes: { state: "empty" },
+    markAllAsReadRes: { state: "empty" },
+    finished: new Map(),
+    isIsomorphic: false,
   };
 
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.state = this.emptyState;
     this.handleSortChange = this.handleSortChange.bind(this);
     this.handlePageChange = this.handlePageChange.bind(this);
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
-      toast(i18n.t("not_logged_in"), "danger");
-      this.context.router.history.push(`/login`);
-    }
+    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.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
+    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) {
-      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.state.combined = this.buildCombined();
-      this.state.loading = false;
-    } else {
-      this.refetch();
+    if (FirstLoadService.isFirstLoad) {
+      const { mentionsRes, messagesRes, repliesRes } = this.isoData.routeData;
+
+      this.state = {
+        ...this.state,
+        repliesRes,
+        mentionsRes,
+        messagesRes,
+        isIsomorphic: true,
+      };
     }
   }
 
-  componentWillUnmount() {
-    if (isBrowser()) {
-      this.subscription.unsubscribe();
+  async componentDidMount() {
+    if (!this.state.isIsomorphic) {
+      await this.refetch();
     }
   }
 
   get documentTitle(): string {
-    return `@${
-      UserService.Instance.myUserInfo.local_user_view.person.name
-    } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
+    const mui = UserService.Instance.myUserInfo;
+    return mui
+      ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
+          this.state.siteRes.site_view.site.name
+        }`
+      : "";
+  }
+
+  get hasUnreads(): boolean {
+    if (this.state.unreadOrAll == UnreadOrAll.Unread) {
+      const { repliesRes, mentionsRes, messagesRes } = this.state;
+      const replyCount =
+        repliesRes.state == "success" ? repliesRes.data.replies.length : 0;
+      const mentionCount =
+        mentionsRes.state == "success" ? mentionsRes.data.mentions.length : 0;
+      const messageCount =
+        messagesRes.state == "success"
+          ? messagesRes.data.private_messages.length
+          : 0;
+
+      return replyCount + mentionCount + messageCount > 0;
+    } else {
+      return false;
+    }
   }
 
   render() {
+    const auth = myAuth();
+    const inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
     return (
-      <div class="container">
-        {this.state.loading ? (
-          <h5>
-            <Spinner large />
-          </h5>
-        ) : (
-          <div class="row">
-            <div class="col-12">
-              <HtmlTags
-                title={this.documentTitle}
-                path={this.context.router.route.match.url}
-              />
-              <h5 class="mb-2">
-                {i18n.t("inbox")}
+      <div className="inbox container-lg">
+        <div className="row">
+          <div className="col-12">
+            <HtmlTags
+              title={this.documentTitle}
+              path={this.context.router.route.match.url}
+            />
+            <h5 className="mb-2">
+              {i18n.t("inbox")}
+              {inboxRss && (
                 <small>
-                  <a
-                    href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
-                    title="RSS"
-                    rel="noopener"
-                  >
-                    <Icon icon="rss" classes="ml-2 text-muted small" />
+                  <a href={inboxRss} title="RSS" rel={relTags}>
+                    <Icon icon="rss" classes="ms-2 text-muted small" />
                   </a>
+                  <link
+                    rel="alternate"
+                    type="application/atom+xml"
+                    href={inboxRss}
+                  />
                 </small>
-              </h5>
-              {this.state.replies.length +
-                this.state.mentions.length +
-                this.state.messages.length >
-                0 &&
-                this.state.unreadOrAll == UnreadOrAll.Unread && (
-                  <button
-                    class="btn btn-secondary mb-2"
-                    onClick={linkEvent(this, this.markAllAsRead)}
-                  >
-                    {i18n.t("mark_all_as_read")}
-                  </button>
+              )}
+            </h5>
+            {this.hasUnreads && (
+              <button
+                className="btn btn-secondary mb-2"
+                onClick={linkEvent(this, this.handleMarkAllAsRead)}
+              >
+                {this.state.markAllAsReadRes.state == "loading" ? (
+                  <Spinner />
+                ) : (
+                  i18n.t("mark_all_as_read")
                 )}
-              {this.selects()}
-              {this.state.messageType == MessageType.All && this.all()}
-              {this.state.messageType == MessageType.Replies && this.replies()}
-              {this.state.messageType == MessageType.Mentions &&
-                this.mentions()}
-              {this.state.messageType == MessageType.Messages &&
-                this.messages()}
-              <Paginator
-                page={this.state.page}
-                onChange={this.handlePageChange}
-              />
-            </div>
+              </button>
+            )}
+            {this.selects()}
+            {this.section}
+            <Paginator
+              page={this.state.page}
+              onChange={this.handlePageChange}
+            />
           </div>
-        )}
+        </div>
       </div>
     );
   }
 
+  get section() {
+    switch (this.state.messageType) {
+      case MessageType.All: {
+        return this.all();
+      }
+      case MessageType.Replies: {
+        return this.replies();
+      }
+      case MessageType.Mentions: {
+        return this.mentions();
+      }
+      case MessageType.Messages: {
+        return this.messages();
+      }
+      default: {
+        return null;
+      }
+    }
+  }
+
   unreadOrAllRadios() {
     return (
-      <div class="btn-group btn-group-toggle flex-wrap mb-2">
+      <div className="btn-group btn-group-toggle flex-wrap mb-2">
         <label
           className={`btn btn-outline-secondary pointer
             ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
@@ -205,6 +292,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={UnreadOrAll.Unread}
             checked={this.state.unreadOrAll == UnreadOrAll.Unread}
             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
@@ -218,6 +306,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={UnreadOrAll.All}
             checked={this.state.unreadOrAll == UnreadOrAll.All}
             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
@@ -230,7 +319,7 @@ export class Inbox extends Component<any, InboxState> {
 
   messageTypeRadios() {
     return (
-      <div class="btn-group btn-group-toggle flex-wrap mb-2">
+      <div className="btn-group btn-group-toggle flex-wrap mb-2">
         <label
           className={`btn btn-outline-secondary pointer
             ${this.state.messageType == MessageType.All && "active"}
@@ -238,6 +327,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={MessageType.All}
             checked={this.state.messageType == MessageType.All}
             onChange={linkEvent(this, this.handleMessageTypeChange)}
@@ -251,6 +341,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={MessageType.Replies}
             checked={this.state.messageType == MessageType.Replies}
             onChange={linkEvent(this, this.handleMessageTypeChange)}
@@ -264,6 +355,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={MessageType.Mentions}
             checked={this.state.messageType == MessageType.Mentions}
             onChange={linkEvent(this, this.handleMessageTypeChange)}
@@ -277,6 +369,7 @@ export class Inbox extends Component<any, InboxState> {
         >
           <input
             type="radio"
+            className="btn-check"
             value={MessageType.Messages}
             checked={this.state.messageType == MessageType.Messages}
             onChange={linkEvent(this, this.handleMessageTypeChange)}
@@ -290,21 +383,19 @@ export class Inbox extends Component<any, InboxState> {
   selects() {
     return (
       <div className="mb-2">
-        <span class="mr-3">{this.unreadOrAllRadios()}</span>
-        <span class="mr-3">{this.messageTypeRadios()}</span>
-        <SortSelect
+        <span className="me-3">{this.unreadOrAllRadios()}</span>
+        <span className="me-3">{this.messageTypeRadios()}</span>
+        <CommentSortSelect
           sort={this.state.sort}
           onChange={this.handleSortChange}
-          hideHot
-          hideMostComments
         />
       </div>
     );
   }
 
-  replyToReplyType(r: CommentView): ReplyType {
+  replyToReplyType(r: CommentReplyView): ReplyType {
     return {
-      id: r.comment.id,
+      id: r.comment_reply.id,
       type_: ReplyEnum.Reply,
       view: r,
       published: r.comment.published,
@@ -330,15 +421,20 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   buildCombined(): ReplyType[] {
-    let replies: ReplyType[] = this.state.replies.map(r =>
-      this.replyToReplyType(r)
-    );
-    let mentions: ReplyType[] = this.state.mentions.map(r =>
-      this.mentionToReplyType(r)
-    );
-    let messages: ReplyType[] = this.state.messages.map(r =>
-      this.messageToReplyType(r)
-    );
+    const replies: ReplyType[] =
+      this.state.repliesRes.state == "success"
+        ? this.state.repliesRes.data.replies.map(this.replyToReplyType)
+        : [];
+    const mentions: ReplyType[] =
+      this.state.mentionsRes.state == "success"
+        ? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType)
+        : [];
+    const messages: ReplyType[] =
+      this.state.messagesRes.state == "success"
+        ? this.state.messagesRes.data.private_messages.map(
+            this.messageToReplyType
+          )
+        : [];
 
     return [...replies, ...mentions, ...messages].sort((a, b) =>
       b.published.localeCompare(a.published)
@@ -351,24 +447,76 @@ export class Inbox extends Component<any, InboxState> {
         return (
           <CommentNodes
             key={i.id}
-            nodes={[{ comment_view: i.view as CommentView }]}
+            nodes={[
+              { comment_view: i.view as CommentView, children: [], depth: 0 },
+            ]}
+            viewType={CommentViewType.Flat}
+            finished={this.state.finished}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
+            allLanguages={this.state.siteRes.all_languages}
+            siteLanguages={this.state.siteRes.discussion_languages}
+            onSaveComment={this.handleSaveComment}
+            onBlockPerson={this.handleBlockPerson}
+            onDeleteComment={this.handleDeleteComment}
+            onRemoveComment={this.handleRemoveComment}
+            onCommentVote={this.handleCommentVote}
+            onCommentReport={this.handleCommentReport}
+            onDistinguishComment={this.handleDistinguishComment}
+            onAddModToCommunity={this.handleAddModToCommunity}
+            onAddAdmin={this.handleAddAdmin}
+            onTransferCommunity={this.handleTransferCommunity}
+            onPurgeComment={this.handlePurgeComment}
+            onPurgePerson={this.handlePurgePerson}
+            onCommentReplyRead={this.handleCommentReplyRead}
+            onPersonMentionRead={this.handlePersonMentionRead}
+            onBanPersonFromCommunity={this.handleBanFromCommunity}
+            onBanPerson={this.handleBanPerson}
+            onCreateComment={this.handleCreateComment}
+            onEditComment={this.handleEditComment}
           />
         );
       case ReplyEnum.Mention:
         return (
           <CommentNodes
             key={i.id}
-            nodes={[{ comment_view: i.view as PersonMentionView }]}
+            nodes={[
+              {
+                comment_view: i.view as PersonMentionView,
+                children: [],
+                depth: 0,
+              },
+            ]}
+            finished={this.state.finished}
+            viewType={CommentViewType.Flat}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
+            allLanguages={this.state.siteRes.all_languages}
+            siteLanguages={this.state.siteRes.discussion_languages}
+            onSaveComment={this.handleSaveComment}
+            onBlockPerson={this.handleBlockPerson}
+            onDeleteComment={this.handleDeleteComment}
+            onRemoveComment={this.handleRemoveComment}
+            onCommentVote={this.handleCommentVote}
+            onCommentReport={this.handleCommentReport}
+            onDistinguishComment={this.handleDistinguishComment}
+            onAddModToCommunity={this.handleAddModToCommunity}
+            onAddAdmin={this.handleAddAdmin}
+            onTransferCommunity={this.handleTransferCommunity}
+            onPurgeComment={this.handlePurgeComment}
+            onPurgePerson={this.handlePurgePerson}
+            onCommentReplyRead={this.handleCommentReplyRead}
+            onPersonMentionRead={this.handlePersonMentionRead}
+            onBanPersonFromCommunity={this.handleBanFromCommunity}
+            onBanPerson={this.handleBanPerson}
+            onCreateComment={this.handleCreateComment}
+            onEditComment={this.handleEditComment}
           />
         );
       case ReplyEnum.Message:
@@ -376,6 +524,11 @@ export class Inbox extends Component<any, InboxState> {
           <PrivateMessage
             key={i.id}
             private_message_view={i.view as PrivateMessageView}
+            onDelete={this.handleDeleteMessage}
+            onMarkRead={this.handleMarkMessageAsRead}
+            onReport={this.handleMessageReport}
+            onCreate={this.handleCreateMessage}
+            onEdit={this.handleEditMessage}
           />
         );
       default:
@@ -384,414 +537,534 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   all() {
-    return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
+    if (
+      this.state.repliesRes.state == "loading" ||
+      this.state.mentionsRes.state == "loading" ||
+      this.state.messagesRes.state == "loading"
+    ) {
+      return (
+        <h5>
+          <Spinner large />
+        </h5>
+      );
+    } else {
+      return (
+        <div>{this.buildCombined().map(r => this.renderReplyType(r))}</div>
+      );
+    }
   }
 
   replies() {
-    return (
-      <div>
-        <CommentNodes
-          nodes={commentsToFlatNodes(this.state.replies)}
-          noIndent
-          markable
-          showCommunity
-          showContext
-          enableDownvotes={this.state.site_view.site.enable_downvotes}
-        />
-      </div>
-    );
+    switch (this.state.repliesRes.state) {
+      case "loading":
+        return (
+          <h5>
+            <Spinner large />
+          </h5>
+        );
+      case "success": {
+        const replies = this.state.repliesRes.data.replies;
+        return (
+          <div>
+            <CommentNodes
+              nodes={commentsToFlatNodes(replies)}
+              viewType={CommentViewType.Flat}
+              finished={this.state.finished}
+              noIndent
+              markable
+              showCommunity
+              showContext
+              enableDownvotes={enableDownvotes(this.state.siteRes)}
+              allLanguages={this.state.siteRes.all_languages}
+              siteLanguages={this.state.siteRes.discussion_languages}
+              onSaveComment={this.handleSaveComment}
+              onBlockPerson={this.handleBlockPerson}
+              onDeleteComment={this.handleDeleteComment}
+              onRemoveComment={this.handleRemoveComment}
+              onCommentVote={this.handleCommentVote}
+              onCommentReport={this.handleCommentReport}
+              onDistinguishComment={this.handleDistinguishComment}
+              onAddModToCommunity={this.handleAddModToCommunity}
+              onAddAdmin={this.handleAddAdmin}
+              onTransferCommunity={this.handleTransferCommunity}
+              onPurgeComment={this.handlePurgeComment}
+              onPurgePerson={this.handlePurgePerson}
+              onCommentReplyRead={this.handleCommentReplyRead}
+              onPersonMentionRead={this.handlePersonMentionRead}
+              onBanPersonFromCommunity={this.handleBanFromCommunity}
+              onBanPerson={this.handleBanPerson}
+              onCreateComment={this.handleCreateComment}
+              onEditComment={this.handleEditComment}
+            />
+          </div>
+        );
+      }
+    }
   }
 
   mentions() {
-    return (
-      <div>
-        {this.state.mentions.map(umv => (
-          <CommentNodes
-            key={umv.person_mention.id}
-            nodes={[{ comment_view: umv }]}
-            noIndent
-            markable
-            showCommunity
-            showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
-          />
-        ))}
-      </div>
-    );
+    switch (this.state.mentionsRes.state) {
+      case "loading":
+        return (
+          <h5>
+            <Spinner large />
+          </h5>
+        );
+      case "success": {
+        const mentions = this.state.mentionsRes.data.mentions;
+        return (
+          <div>
+            {mentions.map(umv => (
+              <CommentNodes
+                key={umv.person_mention.id}
+                nodes={[{ comment_view: umv, children: [], depth: 0 }]}
+                viewType={CommentViewType.Flat}
+                finished={this.state.finished}
+                noIndent
+                markable
+                showCommunity
+                showContext
+                enableDownvotes={enableDownvotes(this.state.siteRes)}
+                allLanguages={this.state.siteRes.all_languages}
+                siteLanguages={this.state.siteRes.discussion_languages}
+                onSaveComment={this.handleSaveComment}
+                onBlockPerson={this.handleBlockPerson}
+                onDeleteComment={this.handleDeleteComment}
+                onRemoveComment={this.handleRemoveComment}
+                onCommentVote={this.handleCommentVote}
+                onCommentReport={this.handleCommentReport}
+                onDistinguishComment={this.handleDistinguishComment}
+                onAddModToCommunity={this.handleAddModToCommunity}
+                onAddAdmin={this.handleAddAdmin}
+                onTransferCommunity={this.handleTransferCommunity}
+                onPurgeComment={this.handlePurgeComment}
+                onPurgePerson={this.handlePurgePerson}
+                onCommentReplyRead={this.handleCommentReplyRead}
+                onPersonMentionRead={this.handlePersonMentionRead}
+                onBanPersonFromCommunity={this.handleBanFromCommunity}
+                onBanPerson={this.handleBanPerson}
+                onCreateComment={this.handleCreateComment}
+                onEditComment={this.handleEditComment}
+              />
+            ))}
+          </div>
+        );
+      }
+    }
   }
 
   messages() {
-    return (
-      <div>
-        {this.state.messages.map(pmv => (
-          <PrivateMessage
-            key={pmv.private_message.id}
-            private_message_view={pmv}
-          />
-        ))}
-      </div>
-    );
+    switch (this.state.messagesRes.state) {
+      case "loading":
+        return (
+          <h5>
+            <Spinner large />
+          </h5>
+        );
+      case "success": {
+        const messages = this.state.messagesRes.data.private_messages;
+        return (
+          <div>
+            {messages.map(pmv => (
+              <PrivateMessage
+                key={pmv.private_message.id}
+                private_message_view={pmv}
+                onDelete={this.handleDeleteMessage}
+                onMarkRead={this.handleMarkMessageAsRead}
+                onReport={this.handleMessageReport}
+                onCreate={this.handleCreateMessage}
+                onEdit={this.handleEditMessage}
+              />
+            ))}
+          </div>
+        );
+      }
+    }
   }
 
-  handlePageChange(page: number) {
+  async handlePageChange(page: number) {
     this.setState({ page });
-    this.refetch();
+    await this.refetch();
   }
 
-  handleUnreadOrAllChange(i: Inbox, event: any) {
-    i.state.unreadOrAll = Number(event.target.value);
-    i.state.page = 1;
-    i.setState(i.state);
-    i.refetch();
+  async handleUnreadOrAllChange(i: Inbox, event: any) {
+    i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
+    await i.refetch();
   }
 
-  handleMessageTypeChange(i: Inbox, event: any) {
-    i.state.messageType = Number(event.target.value);
-    i.state.page = 1;
-    i.setState(i.state);
-    i.refetch();
+  async handleMessageTypeChange(i: Inbox, event: any) {
+    i.setState({ messageType: Number(event.target.value), page: 1 });
+    await i.refetch();
   }
 
-  static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
-    let promises: Promise<any>[] = [];
+  static async fetchInitialData({
+    client,
+    auth,
+  }: InitialFetchRequest): Promise<InboxData> {
+    const sort: CommentSortType = "New";
 
-    // It can be /u/me, or /username/1
-    let repliesForm: GetReplies = {
-      sort: SortType.New,
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
-    promises.push(req.client.getReplies(repliesForm));
-
-    let personMentionsForm: GetPersonMentions = {
-      sort: SortType.New,
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
+    return {
+      mentionsRes: auth
+        ? await client.getPersonMentions({
+            sort,
+            unread_only: true,
+            page: 1,
+            limit: fetchLimit,
+            auth,
+          })
+        : { state: "empty" },
+      messagesRes: auth
+        ? await client.getPrivateMessages({
+            unread_only: true,
+            page: 1,
+            limit: fetchLimit,
+            auth,
+          })
+        : { state: "empty" },
+      repliesRes: auth
+        ? await client.getReplies({
+            sort,
+            unread_only: true,
+            page: 1,
+            limit: fetchLimit,
+            auth,
+          })
+        : { state: "empty" },
     };
-    promises.push(req.client.getPersonMentions(personMentionsForm));
+  }
 
-    let privateMessagesForm: GetPrivateMessages = {
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
-    promises.push(req.client.getPrivateMessages(privateMessagesForm));
+  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 = myAuthRequired();
 
-    return promises;
+    this.setState({ repliesRes: { state: "loading" } });
+    this.setState({
+      repliesRes: await HttpService.client.getReplies({
+        sort,
+        unread_only,
+        page,
+        limit,
+        auth,
+      }),
+    });
+
+    this.setState({ mentionsRes: { state: "loading" } });
+    this.setState({
+      mentionsRes: await HttpService.client.getPersonMentions({
+        sort,
+        unread_only,
+        page,
+        limit,
+        auth,
+      }),
+    });
+
+    this.setState({ messagesRes: { state: "loading" } });
+    this.setState({
+      messagesRes: await HttpService.client.getPrivateMessages({
+        unread_only,
+        page,
+        limit,
+        auth,
+      }),
+    });
   }
 
-  refetch() {
-    let repliesForm: GetReplies = {
-      sort: this.state.sort,
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
-    WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
-
-    let personMentionsForm: GetPersonMentions = {
-      sort: this.state.sort,
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
-    WebSocketService.Instance.send(
-      wsClient.getPersonMentions(personMentionsForm)
-    );
+  async handleSortChange(val: CommentSortType) {
+    this.setState({ sort: val, page: 1 });
+    await this.refetch();
+  }
 
-    let privateMessagesForm: GetPrivateMessages = {
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
-    WebSocketService.Instance.send(
-      wsClient.getPrivateMessages(privateMessagesForm)
-    );
+  async handleMarkAllAsRead(i: Inbox) {
+    i.setState({ markAllAsReadRes: { state: "loading" } });
+
+    i.setState({
+      markAllAsReadRes: await HttpService.client.markAllAsRead({
+        auth: myAuthRequired(),
+      }),
+    });
+
+    if (i.state.markAllAsReadRes.state == "success") {
+      i.setState({
+        repliesRes: { state: "empty" },
+        mentionsRes: { state: "empty" },
+        messagesRes: { state: "empty" },
+      });
+    }
   }
 
-  handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.setState(this.state);
-    this.refetch();
+  async handleAddModToCommunity(form: AddModToCommunity) {
+    // TODO not sure what to do here
+    HttpService.client.addModToCommunity(form);
   }
 
-  markAllAsRead(i: Inbox) {
-    WebSocketService.Instance.send(
-      wsClient.markAllAsRead({
-        auth: authField(),
-      })
-    );
-    i.state.replies = [];
-    i.state.mentions = [];
-    i.state.messages = [];
-    i.sendUnreadCount();
-    window.scrollTo(0, 0);
-    i.setState(i.state);
-  }
-
-  parseMessage(msg: any) {
-    let op = wsUserOp(msg);
-    console.log(msg);
-    if (msg.error) {
-      toast(i18n.t(msg.error), "danger");
-      return;
-    } else if (msg.reconnect) {
-      this.refetch();
-    } else if (op == UserOperation.GetReplies) {
-      let data = wsJsonToRes<GetRepliesResponse>(msg).data;
-      this.state.replies = data.replies;
-      this.state.combined = this.buildCombined();
-      this.state.loading = false;
-      this.sendUnreadCount();
-      window.scrollTo(0, 0);
-      this.setState(this.state);
-      setupTippy();
-    } else if (op == UserOperation.GetPersonMentions) {
-      let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
-      this.state.mentions = data.mentions;
-      this.state.combined = this.buildCombined();
-      this.sendUnreadCount();
-      window.scrollTo(0, 0);
-      this.setState(this.state);
-      setupTippy();
-    } else if (op == UserOperation.GetPrivateMessages) {
-      let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
-      this.state.messages = data.private_messages;
-      this.state.combined = this.buildCombined();
-      this.sendUnreadCount();
-      window.scrollTo(0, 0);
-      this.setState(this.state);
-      setupTippy();
-    } else if (op == UserOperation.EditPrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-      let found: PrivateMessageView = this.state.messages.find(
-        m =>
-          m.private_message.id === data.private_message_view.private_message.id
-      );
-      if (found) {
-        let combinedView = this.state.combined.find(
-          i => i.id == data.private_message_view.private_message.id
-        ).view as PrivateMessageView;
-        found.private_message.content = combinedView.private_message.content =
-          data.private_message_view.private_message.content;
-        found.private_message.updated = combinedView.private_message.updated =
-          data.private_message_view.private_message.updated;
-      }
-      this.setState(this.state);
-    } else if (op == UserOperation.DeletePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-      let found: PrivateMessageView = this.state.messages.find(
-        m =>
-          m.private_message.id === data.private_message_view.private_message.id
-      );
-      if (found) {
-        let combinedView = this.state.combined.find(
-          i => i.id == data.private_message_view.private_message.id
-        ).view as PrivateMessageView;
-        found.private_message.deleted = combinedView.private_message.deleted =
-          data.private_message_view.private_message.deleted;
-        found.private_message.updated = combinedView.private_message.updated =
-          data.private_message_view.private_message.updated;
-      }
-      this.setState(this.state);
-    } else if (op == UserOperation.MarkPrivateMessageAsRead) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-      let found: PrivateMessageView = this.state.messages.find(
-        m =>
-          m.private_message.id === data.private_message_view.private_message.id
-      );
+  async handlePurgePerson(form: PurgePerson) {
+    const purgePersonRes = await HttpService.client.purgePerson(form);
+    this.purgeItem(purgePersonRes);
+  }
 
-      if (found) {
-        let combinedView = this.state.combined.find(
-          i => i.id == data.private_message_view.private_message.id
-        ).view as PrivateMessageView;
-        found.private_message.updated = combinedView.private_message.updated =
-          data.private_message_view.private_message.updated;
-
-        // If youre in the unread view, just remove it from the list
-        if (
-          this.state.unreadOrAll == UnreadOrAll.Unread &&
-          data.private_message_view.private_message.read
-        ) {
-          this.state.messages = this.state.messages.filter(
-            r =>
-              r.private_message.id !==
-              data.private_message_view.private_message.id
-          );
-          this.state.combined = this.state.combined.filter(
-            r => r.id !== data.private_message_view.private_message.id
-          );
-        } else {
-          found.private_message.read = combinedView.private_message.read =
-            data.private_message_view.private_message.read;
-        }
-      }
-      this.sendUnreadCount();
-      this.setState(this.state);
-    } else if (op == UserOperation.MarkAllAsRead) {
-      // Moved to be instant
-    } else if (
-      op == UserOperation.EditComment ||
-      op == UserOperation.DeleteComment ||
-      op == UserOperation.RemoveComment
-    ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      editCommentRes(data.comment_view, this.state.replies);
-      this.setState(this.state);
-    } else if (op == UserOperation.MarkCommentAsRead) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-
-      // If youre in the unread view, just remove it from the list
-      if (
-        this.state.unreadOrAll == UnreadOrAll.Unread &&
-        data.comment_view.comment.read
-      ) {
-        this.state.replies = this.state.replies.filter(
-          r => r.comment.id !== data.comment_view.comment.id
-        );
-        this.state.combined = this.state.combined.filter(
-          r => r.id !== data.comment_view.comment.id
+  async handlePurgeComment(form: PurgeComment) {
+    const purgeCommentRes = await HttpService.client.purgeComment(form);
+    this.purgeItem(purgeCommentRes);
+  }
+
+  async handlePurgePost(form: PurgePost) {
+    const purgeRes = await HttpService.client.purgePost(form);
+    this.purgeItem(purgeRes);
+  }
+
+  async handleBlockPerson(form: BlockPerson) {
+    const blockPersonRes = await HttpService.client.blockPerson(form);
+    if (blockPersonRes.state == "success") {
+      updatePersonBlock(blockPersonRes.data);
+    }
+  }
+
+  async handleCreateComment(form: CreateComment) {
+    const res = await HttpService.client.createComment(form);
+
+    if (res.state === "success") {
+      toast(i18n.t("reply_sent"));
+      this.findAndUpdateComment(res);
+    }
+
+    return res;
+  }
+
+  async handleEditComment(form: EditComment) {
+    const res = await HttpService.client.editComment(form);
+
+    if (res.state === "success") {
+      toast(i18n.t("edit"));
+      this.findAndUpdateComment(res);
+    } else if (res.state === "failed") {
+      toast(res.msg, "danger");
+    }
+
+    return res;
+  }
+
+  async handleDeleteComment(form: DeleteComment) {
+    const res = await HttpService.client.deleteComment(form);
+    if (res.state == "success") {
+      toast(i18n.t("deleted"));
+      this.findAndUpdateComment(res);
+    }
+  }
+
+  async handleRemoveComment(form: RemoveComment) {
+    const res = await HttpService.client.removeComment(form);
+    if (res.state == "success") {
+      toast(i18n.t("remove_comment"));
+      this.findAndUpdateComment(res);
+    }
+  }
+
+  async handleSaveComment(form: SaveComment) {
+    const res = await HttpService.client.saveComment(form);
+    this.findAndUpdateComment(res);
+  }
+
+  async handleCommentVote(form: CreateCommentLike) {
+    const res = await HttpService.client.likeComment(form);
+    this.findAndUpdateComment(res);
+  }
+
+  async handleCommentReport(form: CreateCommentReport) {
+    const reportRes = await HttpService.client.createCommentReport(form);
+    this.reportToast(reportRes);
+  }
+
+  async handleDistinguishComment(form: DistinguishComment) {
+    const res = await HttpService.client.distinguishComment(form);
+    this.findAndUpdateComment(res);
+  }
+
+  async handleAddAdmin(form: AddAdmin) {
+    const addAdminRes = await HttpService.client.addAdmin(form);
+
+    if (addAdminRes.state === "success") {
+      this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+    }
+  }
+
+  async handleTransferCommunity(form: TransferCommunity) {
+    await HttpService.client.transferCommunity(form);
+    toast(i18n.t("transfer_community"));
+  }
+
+  async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+    const res = await HttpService.client.markCommentReplyAsRead(form);
+    this.findAndUpdateCommentReply(res);
+  }
+
+  async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+    const res = await HttpService.client.markPersonMentionAsRead(form);
+    this.findAndUpdateMention(res);
+  }
+
+  async handleBanFromCommunity(form: BanFromCommunity) {
+    const banRes = await HttpService.client.banFromCommunity(form);
+    this.updateBanFromCommunity(banRes);
+  }
+
+  async handleBanPerson(form: BanPerson) {
+    const banRes = await HttpService.client.banPerson(form);
+    this.updateBan(banRes);
+  }
+
+  async handleDeleteMessage(form: DeletePrivateMessage) {
+    const res = await HttpService.client.deletePrivateMessage(form);
+    this.findAndUpdateMessage(res);
+  }
+
+  async handleEditMessage(form: EditPrivateMessage) {
+    const res = await HttpService.client.editPrivateMessage(form);
+    this.findAndUpdateMessage(res);
+  }
+
+  async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
+    const res = await HttpService.client.markPrivateMessageAsRead(form);
+    this.findAndUpdateMessage(res);
+  }
+
+  async handleMessageReport(form: CreatePrivateMessageReport) {
+    const res = await HttpService.client.createPrivateMessageReport(form);
+    this.reportToast(res);
+  }
+
+  async handleCreateMessage(form: CreatePrivateMessage) {
+    const res = await HttpService.client.createPrivateMessage(form);
+    this.setState(s => {
+      if (s.messagesRes.state == "success" && res.state == "success") {
+        s.messagesRes.data.private_messages.unshift(
+          res.data.private_message_view
         );
-      } else {
-        let found = this.state.replies.find(
-          c => c.comment.id == data.comment_view.comment.id
+      }
+
+      return s;
+    });
+  }
+
+  findAndUpdateMessage(res: RequestState<PrivateMessageResponse>) {
+    this.setState(s => {
+      if (s.messagesRes.state === "success" && res.state === "success") {
+        s.messagesRes.data.private_messages = editPrivateMessage(
+          res.data.private_message_view,
+          s.messagesRes.data.private_messages
         );
-        let combinedView = this.state.combined.find(
-          i => i.id == data.comment_view.comment.id
-        ).view as CommentView;
-        found.comment.read = combinedView.comment.read =
-          data.comment_view.comment.read;
       }
-      this.sendUnreadCount();
-      this.setState(this.state);
-      setupTippy();
-    } else if (op == UserOperation.MarkPersonMentionAsRead) {
-      let data = wsJsonToRes<PersonMentionResponse>(msg).data;
-
-      // TODO this might not be correct, it might need to use the comment id
-      let found = this.state.mentions.find(
-        c => c.person_mention.id == data.person_mention_view.person_mention.id
-      );
+      return s;
+    });
+  }
 
-      if (found) {
-        let combinedView = this.state.combined.find(
-          i => i.id == data.person_mention_view.person_mention.id
-        ).view as PersonMentionView;
-        found.comment.content = combinedView.comment.content =
-          data.person_mention_view.comment.content;
-        found.comment.updated = combinedView.comment.updated =
-          data.person_mention_view.comment.updated;
-        found.comment.removed = combinedView.comment.removed =
-          data.person_mention_view.comment.removed;
-        found.comment.deleted = combinedView.comment.deleted =
-          data.person_mention_view.comment.deleted;
-        found.counts.upvotes = combinedView.counts.upvotes =
-          data.person_mention_view.counts.upvotes;
-        found.counts.downvotes = combinedView.counts.downvotes =
-          data.person_mention_view.counts.downvotes;
-        found.counts.score = combinedView.counts.score =
-          data.person_mention_view.counts.score;
-
-        // If youre in the unread view, just remove it from the list
-        if (
-          this.state.unreadOrAll == UnreadOrAll.Unread &&
-          data.person_mention_view.person_mention.read
-        ) {
-          this.state.mentions = this.state.mentions.filter(
-            r =>
-              r.person_mention.id !== data.person_mention_view.person_mention.id
+  updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+    // Maybe not necessary
+    if (banRes.state == "success") {
+      this.setState(s => {
+        if (s.repliesRes.state == "success") {
+          s.repliesRes.data.replies
+            .filter(c => c.creator.id == banRes.data.person_view.person.id)
+            .forEach(
+              c => (c.creator_banned_from_community = banRes.data.banned)
+            );
+        }
+        if (s.mentionsRes.state == "success") {
+          s.mentionsRes.data.mentions
+            .filter(c => c.creator.id == banRes.data.person_view.person.id)
+            .forEach(
+              c => (c.creator_banned_from_community = banRes.data.banned)
+            );
+        }
+        return s;
+      });
+    }
+  }
+
+  updateBan(banRes: RequestState<BanPersonResponse>) {
+    // Maybe not necessary
+    if (banRes.state == "success") {
+      this.setState(s => {
+        if (s.repliesRes.state == "success") {
+          s.repliesRes.data.replies
+            .filter(c => c.creator.id == banRes.data.person_view.person.id)
+            .forEach(c => (c.creator.banned = banRes.data.banned));
+        }
+        if (s.mentionsRes.state == "success") {
+          s.mentionsRes.data.mentions
+            .filter(c => c.creator.id == banRes.data.person_view.person.id)
+            .forEach(c => (c.creator.banned = banRes.data.banned));
+        }
+        return s;
+      });
+    }
+  }
+
+  purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+    if (purgeRes.state == "success") {
+      toast(i18n.t("purge_success"));
+      this.context.router.history.push(`/`);
+    }
+  }
+
+  reportToast(
+    res: RequestState<PrivateMessageReportResponse | CommentReportResponse>
+  ) {
+    if (res.state == "success") {
+      toast(i18n.t("report_created"));
+    }
+  }
+
+  // A weird case, since you have only replies and mentions, not comment responses
+  findAndUpdateComment(res: RequestState<CommentResponse>) {
+    if (res.state == "success") {
+      this.setState(s => {
+        if (s.repliesRes.state == "success") {
+          s.repliesRes.data.replies = editWith(
+            res.data.comment_view,
+            s.repliesRes.data.replies
           );
-          this.state.combined = this.state.combined.filter(
-            r => r.id !== data.person_mention_view.person_mention.id
+        }
+        if (s.mentionsRes.state == "success") {
+          s.mentionsRes.data.mentions = editWith(
+            res.data.comment_view,
+            s.mentionsRes.data.mentions
           );
-        } else {
-          // TODO test to make sure these mentions are getting marked as read
-          found.person_mention.read = combinedView.person_mention.read =
-            data.person_mention_view.person_mention.read;
         }
-      }
-      this.sendUnreadCount();
-      this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-
-      if (
-        data.recipient_ids.includes(
-          UserService.Instance.myUserInfo.local_user_view.local_user.id
-        )
-      ) {
-        this.state.replies.unshift(data.comment_view);
-        this.state.combined.unshift(this.replyToReplyType(data.comment_view));
-        this.setState(this.state);
-      } else if (
-        data.comment_view.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        // TODO this seems wrong, you should be using form_id
-        toast(i18n.t("reply_sent"));
-      }
-    } else if (op == UserOperation.CreatePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-      if (
-        data.private_message_view.recipient.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        this.state.messages.unshift(data.private_message_view);
-        this.state.combined.unshift(
-          this.messageToReplyType(data.private_message_view)
+        // Set finished for the parent
+        s.finished.set(
+          getCommentParentId(res.data.comment_view.comment) ?? 0,
+          true
         );
-        this.setState(this.state);
-      }
-    } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      saveCommentRes(data.comment_view, this.state.replies);
-      this.setState(this.state);
-      setupTippy();
-    } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      createCommentLikeRes(data.comment_view, this.state.replies);
-      this.setState(this.state);
-    } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
-      updatePersonBlock(data);
-    } else if (op == UserOperation.CreatePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
-      if (data) {
-        toast(i18n.t("report_created"));
-      }
-    } else if (op == UserOperation.CreateCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
-      if (data) {
-        toast(i18n.t("report_created"));
-      }
+        return s;
+      });
     }
   }
 
-  sendUnreadCount() {
-    UserService.Instance.unreadInboxCountSub.next(this.unreadCount());
+  findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+    this.setState(s => {
+      if (s.repliesRes.state == "success" && res.state == "success") {
+        s.repliesRes.data.replies = editCommentReply(
+          res.data.comment_reply_view,
+          s.repliesRes.data.replies
+        );
+      }
+      return s;
+    });
   }
 
-  unreadCount(): number {
-    return (
-      this.state.replies.filter(r => !r.comment.read).length +
-      this.state.mentions.filter(r => !r.person_mention.read).length +
-      this.state.messages.filter(
-        r =>
-          UserService.Instance.myUserInfo &&
-          !r.private_message.read &&
-          // TODO also seems very strange and wrong
-          r.creator.id !==
-            UserService.Instance.myUserInfo.local_user_view.person.id
-      ).length
-    );
+  findAndUpdateMention(res: RequestState<PersonMentionResponse>) {
+    this.setState(s => {
+      if (s.mentionsRes.state == "success" && res.state == "success") {
+        s.mentionsRes.data.mentions = editMention(
+          res.data.person_mention_view,
+          s.mentionsRes.data.mentions
+        );
+      }
+      return s;
+    });
   }
 }