]> Untitled Git - lemmy.git/commitdiff
Done merging http-api and private_message
authorDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 00:17:42 +0000 (19:17 -0500)
committerDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 00:17:42 +0000 (19:17 -0500)
24 files changed:
1  2 
server/src/api/comment.rs
server/src/api/mod.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/src/components/comment-form.tsx
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/inbox.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/modlog.tsx
ui/src/components/navbar.tsx
ui/src/components/password_change.tsx
ui/src/components/post-form.tsx
ui/src/components/post.tsx
ui/src/components/private-message-form.tsx
ui/src/components/search.tsx
ui/src/components/setup.tsx
ui/src/components/user.tsx
ui/src/interfaces.ts
ui/src/utils.ts

Simple merge
Simple merge
Simple merge
index 046da6fb2a40e1ec13547c2b798e0506f4bd58d7,e1ddb1caeda96d237f6dfcbee90ad83e22b784aa..8d2db104cd2caaa44fe10f2266e73425d852b4e5
@@@ -168,42 -158,6 +159,40 @@@ pub struct PasswordChange 
    password_verify: String,
  }
  
-   op: String,
 +#[derive(Serialize, Deserialize)]
 +pub struct CreatePrivateMessage {
 +  content: String,
 +  recipient_id: i32,
 +  auth: String,
 +}
 +
 +#[derive(Serialize, Deserialize)]
 +pub struct EditPrivateMessage {
 +  edit_id: i32,
 +  content: Option<String>,
 +  deleted: Option<bool>,
 +  read: Option<bool>,
 +  auth: String,
 +}
 +
 +#[derive(Serialize, Deserialize)]
 +pub struct GetPrivateMessages {
 +  unread_only: bool,
 +  page: Option<i64>,
 +  limit: Option<i64>,
 +  auth: String,
 +}
 +
 +#[derive(Serialize, Deserialize, Clone)]
 +pub struct PrivateMessagesResponse {
-   op: String,
 +  messages: Vec<PrivateMessageView>,
 +}
 +
 +#[derive(Serialize, Deserialize, Clone)]
 +pub struct PrivateMessageResponse {
 +  message: PrivateMessageView,
 +}
 +
  impl Perform<LoginResponse> for Oper<Login> {
    fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
      let data: &Login = &self.data;
@@@ -805,34 -732,7 +773,31 @@@ impl Perform<GetRepliesResponse> for Op
          };
      }
  
-         Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()),
 +    // messages
 +    let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
 +      .page(1)
 +      .limit(999)
 +      .unread_only(true)
 +      .list()?;
 +
 +    for message in &messages {
 +      let private_message_form = PrivateMessageForm {
 +        content: None,
 +        creator_id: message.to_owned().creator_id,
 +        recipient_id: message.to_owned().recipient_id,
 +        deleted: None,
 +        read: Some(true),
 +        updated: None,
 +      };
 +
 +      let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
 +      {
 +        Ok(message) => message,
-     Ok(GetRepliesResponse {
-       op: self.op.to_string(),
-       replies: vec![],
-     })
++        Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
 +      };
 +    }
 +
+     Ok(GetRepliesResponse { replies: vec![] })
    }
  }
  
@@@ -972,150 -868,3 +933,141 @@@ impl Perform<LoginResponse> for Oper<Pa
      })
    }
  }
-       Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
 +
 +impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
 +  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
 +    let data: &CreatePrivateMessage = &self.data;
 +
 +    let claims = match Claims::decode(&data.auth) {
 +      Ok(claims) => claims.claims,
-       return Err(APIError::err(&self.op, "site_ban").into());
++      Err(_e) => return Err(APIError::err("not_logged_in").into()),
 +    };
 +
 +    let user_id = claims.id;
 +
 +    let hostname = &format!("https://{}", Settings::get().hostname);
 +
 +    // Check for a site ban
 +    if UserView::read(&conn, user_id)?.banned {
-         return Err(APIError::err(&self.op, "couldnt_create_private_message").into());
++      return Err(APIError::err("site_ban").into());
 +    }
 +
 +    let content_slurs_removed = remove_slurs(&data.content.to_owned());
 +
 +    let private_message_form = PrivateMessageForm {
 +      content: Some(content_slurs_removed.to_owned()),
 +      creator_id: user_id,
 +      recipient_id: data.recipient_id,
 +      deleted: None,
 +      read: None,
 +      updated: None,
 +    };
 +
 +    let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
 +      Ok(private_message) => private_message,
 +      Err(_e) => {
-     let private_message_view = PrivateMessageView::read(&conn, inserted_private_message.id)?;
++        return Err(APIError::err("couldnt_create_private_message").into());
 +      }
 +    };
 +
 +    // Send notifications to the recipient
 +    let recipient_user = User_::read(&conn, data.recipient_id)?;
 +    if recipient_user.send_notifications_to_email {
 +      if let Some(email) = recipient_user.email {
 +        let subject = &format!(
 +          "{} - Private Message from {}",
 +          Settings::get().hostname,
 +          claims.username
 +        );
 +        let html = &format!(
 +          "<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
 +          claims.username, &content_slurs_removed, hostname
 +        );
 +        match send_email(subject, &email, &recipient_user.name, html) {
 +          Ok(_o) => _o,
 +          Err(e) => eprintln!("{}", e),
 +        };
 +      }
 +    }
 +
-     Ok(PrivateMessageResponse {
-       op: self.op.to_string(),
-       message: private_message_view,
-     })
++    let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
 +
-       Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
++    Ok(PrivateMessageResponse { message })
 +  }
 +}
 +
 +impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
 +  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
 +    let data: &EditPrivateMessage = &self.data;
 +
 +    let claims = match Claims::decode(&data.auth) {
 +      Ok(claims) => claims.claims,
-       return Err(APIError::err(&self.op, "site_ban").into());
++      Err(_e) => return Err(APIError::err("not_logged_in").into()),
 +    };
 +
 +    let user_id = claims.id;
 +
 +    let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
 +
 +    // Check for a site ban
 +    if UserView::read(&conn, user_id)?.banned {
-       return Err(APIError::err(&self.op, "no_private_message_edit_allowed").into());
++      return Err(APIError::err("site_ban").into());
 +    }
 +
 +    // Check to make sure they are the creator (or the recipient marking as read
 +    if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
 +      || orig_private_message.creator_id.eq(&user_id))
 +    {
-         Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()),
++      return Err(APIError::err("no_private_message_edit_allowed").into());
 +    }
 +
 +    let content_slurs_removed = match &data.content {
 +      Some(content) => Some(remove_slurs(content)),
 +      None => None,
 +    };
 +
 +    let private_message_form = PrivateMessageForm {
 +      content: content_slurs_removed,
 +      creator_id: orig_private_message.creator_id,
 +      recipient_id: orig_private_message.recipient_id,
 +      deleted: data.deleted.to_owned(),
 +      read: data.read.to_owned(),
 +      updated: if data.read.is_some() {
 +        orig_private_message.updated
 +      } else {
 +        Some(naive_now())
 +      },
 +    };
 +
 +    let _updated_private_message =
 +      match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
 +        Ok(private_message) => private_message,
-     let private_message_view = PrivateMessageView::read(&conn, data.edit_id)?;
++        Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
 +      };
 +
-     Ok(PrivateMessageResponse {
-       op: self.op.to_string(),
-       message: private_message_view,
-     })
++    let message = PrivateMessageView::read(&conn, data.edit_id)?;
 +
-       Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
++    Ok(PrivateMessageResponse { message })
 +  }
 +}
 +
 +impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
 +  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
 +    let data: &GetPrivateMessages = &self.data;
 +
 +    let claims = match Claims::decode(&data.auth) {
 +      Ok(claims) => claims.claims,
-     Ok(PrivateMessagesResponse {
-       op: self.op.to_string(),
-       messages,
-     })
++      Err(_e) => return Err(APIError::err("not_logged_in").into()),
 +    };
 +
 +    let user_id = claims.id;
 +
 +    let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
 +      .page(data.page)
 +      .limit(data.limit)
 +      .unread_only(data.unread_only)
 +      .list()?;
 +
++    Ok(PrivateMessagesResponse { messages })
 +  }
 +}
index 74f47ad347dafef1f192a5ef058f084e1f99956e,1be3a8e0e899a177d16dadc766ad614a5cc03db6..021bcb41ee9ee8e846e0d6c80080d0b6859afdf3
@@@ -1,1 -1,44 +1,47 @@@
  pub mod server;
+ #[derive(EnumString, ToString, Debug)]
+ pub enum UserOperation {
+   Login,
+   Register,
+   CreateCommunity,
+   CreatePost,
+   ListCommunities,
+   ListCategories,
+   GetPost,
+   GetCommunity,
+   CreateComment,
+   EditComment,
+   SaveComment,
+   CreateCommentLike,
+   GetPosts,
+   CreatePostLike,
+   EditPost,
+   SavePost,
+   EditCommunity,
+   FollowCommunity,
+   GetFollowedCommunities,
+   GetUserDetails,
+   GetReplies,
+   GetUserMentions,
+   EditUserMention,
+   GetModlog,
+   BanFromCommunity,
+   AddModToCommunity,
+   CreateSite,
+   EditSite,
+   GetSite,
+   AddAdmin,
+   BanUser,
+   Search,
+   MarkAllAsRead,
+   SaveUserSettings,
+   TransferCommunity,
+   TransferSite,
+   DeleteAccount,
+   PasswordReset,
+   PasswordChange,
++  CreatePrivateMessage,
++  EditPrivateMessage,
++  GetPrivateMessages,
+ }
index 5efcb7bf41fe6496c44a07ebcf219409d2bb059e,3015a80d32e34ec324093d77c205293d4eb42cca..b1d4f1387ec78b7fe524e2bebf02603ad11f5a61
@@@ -513,55 -495,27 +495,37 @@@ fn parse_json_message(chat: &mut ChatSe
      UserOperation::GetSite => {
        let online: usize = chat.sessions.len();
        let get_site: GetSite = serde_json::from_str(data)?;
-       let mut res = Oper::new(user_operation, get_site).perform(&conn)?;
+       let mut res = Oper::new(get_site).perform(&conn)?;
        res.online = online;
-       Ok(serde_json::to_string(&res)?)
+       to_json_string(&user_operation, &res)
      }
      UserOperation::Search => {
-       let search: Search = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, search).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
      }
      UserOperation::TransferCommunity => {
-       let transfer_community: TransferCommunity = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, transfer_community).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<TransferCommunity, GetCommunityResponse>(user_operation, data, &conn)
      }
      UserOperation::TransferSite => {
-       let transfer_site: TransferSite = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, transfer_site).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<TransferSite, GetSiteResponse>(user_operation, data, &conn)
      }
      UserOperation::DeleteAccount => {
-       let delete_account: DeleteAccount = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, delete_account).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<DeleteAccount, LoginResponse>(user_operation, data, &conn)
      }
      UserOperation::PasswordReset => {
-       let password_reset: PasswordReset = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, password_reset).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<PasswordReset, PasswordResetResponse>(user_operation, data, &conn)
      }
      UserOperation::PasswordChange => {
-       let password_change: PasswordChange = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, password_change).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
+       do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
      }
-       let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, create_private_message).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
 +    UserOperation::CreatePrivateMessage => {
 +      chat.check_rate_limit_message(msg.id)?;
-       let edit_private_message: EditPrivateMessage = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, edit_private_message).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
++      do_user_operation::<CreatePrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
 +    }
 +    UserOperation::EditPrivateMessage => {
-       let messages: GetPrivateMessages = serde_json::from_str(data)?;
-       let res = Oper::new(user_operation, messages).perform(&conn)?;
-       Ok(serde_json::to_string(&res)?)
++      do_user_operation::<EditPrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
 +    }
 +    UserOperation::GetPrivateMessages => {
++      do_user_operation::<GetPrivateMessages, PrivateMessagesResponse>(user_operation, data, &conn)
 +    }
    }
  }
Simple merge
index 129051fbb28bd53393135e3279d78a9d7db38fe7,ebcbc345ecd4671fb19149c18a44a274ddd75b2e..867cfd818ccca8f442992bd3944495e63e7b8dbd
@@@ -10,9 -10,10 +10,10 @@@ import 
    FollowCommunityForm,
    ListCommunitiesForm,
    SortType,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService } from '../services';
- import { msgOp, toast } from '../utils';
 -import { wsJsonToRes } from '../utils';
++import { wsJsonToRes, toast } from '../utils';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
  
@@@ -231,15 -228,15 +232,15 @@@ export class Communities extends Compon
      WebSocketService.Instance.listCommunities(listCommunitiesForm);
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.ListCommunities) {
-       let res: ListCommunitiesResponse = msg;
-       this.state.communities = res.communities;
+     } else if (res.op == UserOperation.ListCommunities) {
+       let data = res.data as ListCommunitiesResponse;
+       this.state.communities = data.communities;
        this.state.communities.sort(
          (a, b) => b.number_of_subscribers - a.number_of_subscribers
        );
index ec58b010cb7ccdc47c259afe8a37c138090548e2,14cd8e4fb85a975a2cf93c4968f1f3ea8caf70ea..4dc7bfcbb23411417128a6f0118ed1aa68ff0851
@@@ -8,9 -8,9 +8,10 @@@ import 
    ListCategoriesResponse,
    CommunityResponse,
    GetSiteResponse,
++  WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService } from '../services';
- import { msgOp, capitalizeFirstLetter, toast } from '../utils';
 -import { wsJsonToRes, capitalizeFirstLetter } from '../utils';
++import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
  import autosize from 'autosize';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
@@@ -239,11 -239,11 +240,11 @@@ export class CommunityForm extends Comp
      i.props.onCancel();
    }
  
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
+   parseMessage(msg: WebSocketJsonResponse) {
+     let res = wsJsonToRes(msg);
      console.log(msg);
-     if (msg.error) {
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state.loading = false;
        this.setState(this.state);
        return;
index dfd4d6b32d9f81ef729260beeec406663ebb0fda,357fe260629525935b7d90827142efec0a3fabcd..9d02dd8661634042df187de99de402fb8e72f6f3
@@@ -254,43 -252,49 +255,43 @@@ export class Community extends Componen
      WebSocketService.Instance.getPosts(getPostsForm);
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.context.router.history.push('/');
        return;
-     } else if (op == UserOperation.GetCommunity) {
-       let res: GetCommunityResponse = msg;
-       this.state.community = res.community;
-       this.state.moderators = res.moderators;
-       this.state.admins = res.admins;
+     } else if (res.op == UserOperation.GetCommunity) {
+       let data = res.data as GetCommunityResponse;
+       this.state.community = data.community;
+       this.state.moderators = data.moderators;
+       this.state.admins = data.admins;
        document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
        this.setState(this.state);
        this.keepFetchingPosts();
-     } else if (op == UserOperation.EditCommunity) {
-       let res: CommunityResponse = msg;
-       this.state.community = res.community;
+     } else if (res.op == UserOperation.EditCommunity) {
+       let data = res.data as CommunityResponse;
+       this.state.community = data.community;
        this.setState(this.state);
-     } else if (op == UserOperation.FollowCommunity) {
-       let res: CommunityResponse = msg;
-       this.state.community.subscribed = res.community.subscribed;
+     } 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 =
-         res.community.number_of_subscribers;
+         data.community.number_of_subscribers;
        this.setState(this.state);
-     } else if (op == UserOperation.GetPosts) {
-       let res: GetPostsResponse = msg;
-       this.state.posts = res.posts;
+     } else if (res.op == UserOperation.GetPosts) {
+       let data = res.data as GetPostsResponse;
 -
 -      // TODO rework this
 -      // This is needed to refresh the view
 -      this.state.posts = undefined;
 -      this.setState(this.state);
 -
+       this.state.posts = data.posts;
        this.state.loading = false;
        this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let res: CreatePostLikeResponse = msg;
-       let found = this.state.posts.find(c => c.id == res.post.id);
-       found.my_vote = res.post.my_vote;
-       found.score = res.post.score;
-       found.upvotes = res.post.upvotes;
-       found.downvotes = res.post.downvotes;
+     } else if (res.op == UserOperation.CreatePostLike) {
+       let data = res.data as CreatePostLikeResponse;
+       let found = this.state.posts.find(c => c.id == data.post.id);
+       found.my_vote = data.post.my_vote;
+       found.score = data.post.score;
+       found.upvotes = data.post.upvotes;
+       found.downvotes = data.post.downvotes;
        this.setState(this.state);
      }
    }
index bf0901797ef3d003031662e01f87b9274b4b8c9e,4aa9cebee44d899c1dde9f2662fcfc2b46965cff..5c3ff6d2b325245c86fce3b968a6b9eb28f46380
@@@ -12,15 -12,11 +12,16 @@@ import 
    GetUserMentionsResponse,
    UserMentionResponse,
    CommentResponse,
+   WebSocketJsonResponse,
 +  PrivateMessage as PrivateMessageI,
 +  GetPrivateMessagesForm,
 +  PrivateMessagesResponse,
 +  PrivateMessageResponse,
  } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
- import { msgOp, fetchLimit, isCommentType, toast } from '../utils';
 -import { wsJsonToRes, fetchLimit } from '../utils';
++import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils';
  import { CommentNodes } from './comment-nodes';
 +import { PrivateMessage } from './private-message';
  import { SortSelect } from './sort-select';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
@@@ -320,15 -297,15 +321,15 @@@ export class Inbox extends Component<an
      WebSocketService.Instance.markAllAsRead();
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.GetReplies) {
-       let res: GetRepliesResponse = msg;
-       this.state.replies = res.replies;
+     } else if (res.op == UserOperation.GetReplies) {
+       let data = res.data as GetRepliesResponse;
+       this.state.replies = data.replies;
        this.sendUnreadCount();
        window.scrollTo(0, 0);
        this.setState(this.state);
        this.sendUnreadCount();
        window.scrollTo(0, 0);
        this.setState(this.state);
-     } else if (op == UserOperation.GetPrivateMessages) {
-       let res: PrivateMessagesResponse = msg;
-       this.state.messages = res.messages;
++    } else if (res.op == UserOperation.GetPrivateMessages) {
++      let data = res.data as PrivateMessagesResponse;
++      this.state.messages = data.messages;
 +      this.sendUnreadCount();
 +      window.scrollTo(0, 0);
 +      this.setState(this.state);
-     } else if (op == UserOperation.EditPrivateMessage) {
-       let res: PrivateMessageResponse = msg;
++    } else if (res.op == UserOperation.EditPrivateMessage) {
++      let data = res.data as PrivateMessageResponse;
 +      let found: PrivateMessageI = this.state.messages.find(
-         m => m.id === res.message.id
++        m => m.id === data.message.id
 +      );
-       found.content = res.message.content;
-       found.updated = res.message.updated;
-       found.deleted = res.message.deleted;
++      found.content = data.message.content;
++      found.updated = data.message.updated;
++      found.deleted = data.message.deleted;
 +      // If youre in the unread view, just remove it from the list
-       if (this.state.unreadOrAll == UnreadOrAll.Unread && res.message.read) {
++      if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
 +        this.state.messages = this.state.messages.filter(
-           r => r.id !== res.message.id
++          r => r.id !== data.message.id
 +        );
 +      } else {
-         let found = this.state.messages.find(c => c.id == res.message.id);
-         found.read = res.message.read;
++        let found = this.state.messages.find(c => c.id == data.message.id);
++        found.read = data.message.read;
 +      }
 +      this.sendUnreadCount();
 +      window.scrollTo(0, 0);
 +      this.setState(this.state);
-     } else if (op == UserOperation.MarkAllAsRead) {
+     } else if (res.op == UserOperation.MarkAllAsRead) {
        this.state.replies = [];
        this.state.mentions = [];
 +      this.state.messages = [];
 +      this.sendUnreadCount();
        window.scrollTo(0, 0);
        this.setState(this.state);
-     } else if (op == UserOperation.EditComment) {
-       let res: CommentResponse = msg;
-       let found = this.state.replies.find(c => c.id == res.comment.id);
-       found.content = res.comment.content;
-       found.updated = res.comment.updated;
-       found.removed = res.comment.removed;
-       found.deleted = res.comment.deleted;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       found.score = res.comment.score;
+     } else if (res.op == UserOperation.EditComment) {
+       let data = res.data as CommentResponse;
+       let found = this.state.replies.find(c => c.id == data.comment.id);
+       found.content = data.comment.content;
+       found.updated = data.comment.updated;
+       found.removed = data.comment.removed;
+       found.deleted = data.comment.deleted;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
+       found.score = data.comment.score;
  
        // If youre in the unread view, just remove it from the list
-       if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
+       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
          this.state.replies = this.state.replies.filter(
-           r => r.id !== res.comment.id
+           r => r.id !== data.comment.id
          );
        } else {
-         let found = this.state.replies.find(c => c.id == res.comment.id);
-         found.read = res.comment.read;
+         let found = this.state.replies.find(c => c.id == data.comment.id);
+         found.read = data.comment.read;
        }
        this.sendUnreadCount();
        this.setState(this.state);
        }
        this.sendUnreadCount();
        this.setState(this.state);
-     } else if (op == UserOperation.CreateComment) {
+     } else if (res.op == UserOperation.CreateComment) {
        // let res: CommentResponse = msg;
 -      alert(i18n.t('reply_sent'));
 +      toast(i18n.t('reply_sent'));
        // this.state.replies.unshift(res.comment); // TODO do this right
        // this.setState(this.state);
-     } else if (op == UserOperation.SaveComment) {
-       let res: CommentResponse = msg;
-       let found = this.state.replies.find(c => c.id == res.comment.id);
-       found.saved = res.comment.saved;
+     } else if (res.op == UserOperation.SaveComment) {
+       let data = res.data as CommentResponse;
+       let found = this.state.replies.find(c => c.id == data.comment.id);
+       found.saved = data.comment.saved;
        this.setState(this.state);
-     } else if (op == UserOperation.CreateCommentLike) {
-       let res: CommentResponse = msg;
+     } else if (res.op == UserOperation.CreateCommentLike) {
+       let data = res.data as CommentResponse;
        let found: Comment = this.state.replies.find(
-         c => c.id === res.comment.id
+         c => c.id === data.comment.id
        );
-       found.score = res.comment.score;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+       found.score = data.comment.score;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
+       if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
        this.setState(this.state);
      }
    }
index 0c8350aa0429c8945a78084b490b1b1ebaed9db0,29482f4536969079ee86c2a1e717841ac331d020..ac60ba74bce85f84e4bb16dbb2f384bad726d24e
@@@ -8,9 -8,10 +8,10 @@@ import 
    UserOperation,
    PasswordResetForm,
    GetSiteResponse,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
- import { msgOp, validEmail, toast } from '../utils';
 -import { wsJsonToRes, validEmail } from '../utils';
++import { wsJsonToRes, validEmail, toast } from '../utils';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
  
@@@ -292,32 -293,31 +293,32 @@@ export class Login extends Component<an
      WebSocketService.Instance.passwordReset(resetForm);
    }
  
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+   parseMessage(msg: WebSocketJsonResponse) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state = this.emptyState;
        this.setState(this.state);
        return;
      } else {
-       if (op == UserOperation.Login) {
+       if (res.op == UserOperation.Login) {
+         let data = res.data as LoginResponse;
          this.state = this.emptyState;
          this.setState(this.state);
-         let res: LoginResponse = msg;
-         UserService.Instance.login(res);
+         UserService.Instance.login(data);
 +        toast(i18n.t('logged_in'));
          this.props.history.push('/');
-       } else if (op == UserOperation.Register) {
+       } else if (res.op == UserOperation.Register) {
+         let data = res.data as LoginResponse;
          this.state = this.emptyState;
          this.setState(this.state);
-         let res: LoginResponse = msg;
-         UserService.Instance.login(res);
+         UserService.Instance.login(data);
          this.props.history.push('/communities');
-       } else if (op == UserOperation.PasswordReset) {
+       } else if (res.op == UserOperation.PasswordReset) {
 -        alert(i18n.t('reset_password_mail_sent'));
 +        toast(i18n.t('reset_password_mail_sent'));
-       } else if (op == UserOperation.GetSite) {
-         let res: GetSiteResponse = msg;
-         this.state.enable_nsfw = res.site.enable_nsfw;
+       } else if (res.op == UserOperation.GetSite) {
+         let data = res.data as GetSiteResponse;
+         this.state.enable_nsfw = data.site.enable_nsfw;
          this.setState(this.state);
          document.title = `${i18n.t('login')} - ${
            WebSocketService.Instance.site.name
index b244ce66653d83a318d77634e6336516024f9dd6,1ccebc80a22d60f9a9d244ae45aa9b44e1200924..9f16edb5d340983692810adb477f8be307f19c99
@@@ -563,50 -562,56 +563,50 @@@ export class Main extends Component<any
      WebSocketService.Instance.getPosts(getPostsForm);
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.GetFollowedCommunities) {
-       let res: GetFollowedCommunitiesResponse = msg;
-       this.state.subscribedCommunities = res.communities;
+     } else if (res.op == UserOperation.GetFollowedCommunities) {
+       let data = res.data as GetFollowedCommunitiesResponse;
+       this.state.subscribedCommunities = data.communities;
        this.setState(this.state);
-     } else if (op == UserOperation.ListCommunities) {
-       let res: ListCommunitiesResponse = msg;
-       this.state.trendingCommunities = res.communities;
+     } else if (res.op == UserOperation.ListCommunities) {
+       let data = res.data as ListCommunitiesResponse;
+       this.state.trendingCommunities = data.communities;
        this.setState(this.state);
-     } else if (op == UserOperation.GetSite) {
-       let res: GetSiteResponse = msg;
+     } else if (res.op == UserOperation.GetSite) {
+       let data = res.data as GetSiteResponse;
  
        // This means it hasn't been set up yet
-       if (!res.site) {
+       if (!data.site) {
          this.context.router.history.push('/setup');
        }
-       this.state.site.admins = res.admins;
-       this.state.site.site = res.site;
-       this.state.site.banned = res.banned;
-       this.state.site.online = res.online;
+       this.state.site.admins = data.admins;
+       this.state.site.site = data.site;
+       this.state.site.banned = data.banned;
+       this.state.site.online = data.online;
        this.setState(this.state);
        document.title = `${WebSocketService.Instance.site.name}`;
-     } else if (op == UserOperation.EditSite) {
-       let res: SiteResponse = msg;
-       this.state.site.site = res.site;
+     } else if (res.op == UserOperation.EditSite) {
+       let data = res.data as SiteResponse;
+       this.state.site.site = data.site;
        this.state.showEditSite = false;
        this.setState(this.state);
-     } else if (op == UserOperation.GetPosts) {
-       let res: GetPostsResponse = msg;
-       this.state.posts = res.posts;
+     } else if (res.op == UserOperation.GetPosts) {
+       let data = res.data as GetPostsResponse;
 -
 -      // This is needed to refresh the view
 -      // TODO mess with this
 -      this.state.posts = undefined;
 -      this.setState(this.state);
 -
+       this.state.posts = data.posts;
        this.state.loading = false;
        this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let res: CreatePostLikeResponse = msg;
-       let found = this.state.posts.find(c => c.id == res.post.id);
-       found.my_vote = res.post.my_vote;
-       found.score = res.post.score;
-       found.upvotes = res.post.upvotes;
-       found.downvotes = res.post.downvotes;
+     } else if (res.op == UserOperation.CreatePostLike) {
+       let data = res.data as CreatePostLikeResponse;
+       let found = this.state.posts.find(c => c.id == data.post.id);
+       found.my_vote = data.post.my_vote;
+       found.score = data.post.score;
+       found.upvotes = data.post.upvotes;
+       found.downvotes = data.post.downvotes;
        this.setState(this.state);
      }
    }
index 6c35bce97296cfa61bc1cabcd6b970b8c4c56c8c,b2011af5b925e18ae5613a813c787ca28536409d..dd651092887bd166622968e5537d5014dde575a9
@@@ -17,7 -17,7 +17,7 @@@ import 
    ModAdd,
  } from '../interfaces';
  import { WebSocketService } from '../services';
- import { msgOp, addTypeInfo, fetchLimit, toast } from '../utils';
 -import { wsJsonToRes, addTypeInfo, fetchLimit } from '../utils';
++import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
  import { MomentTime } from './moment-time';
  import moment from 'moment';
  import { i18n } from '../i18next';
@@@ -422,17 -422,17 +422,17 @@@ export class Modlog extends Component<a
      WebSocketService.Instance.getModlog(modlogForm);
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.GetModlog) {
-       let res: GetModlogResponse = msg;
+     } else if (res.op == UserOperation.GetModlog) {
+       let data = res.data as GetModlogResponse;
        this.state.loading = false;
        window.scrollTo(0, 0);
-       this.setCombined(res);
+       this.setCombined(data);
      }
    }
  }
index 81124f77e50aaa367b87d420a958882b1e570744,fac5421289795a2edabdfce94c29dd518004f421..849822af16d6ac220e07168641e11be8e33a623d
@@@ -14,10 -12,10 +14,11 @@@ import 
    SortType,
    GetSiteResponse,
    Comment,
 +  PrivateMessage,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import {
-   msgOp,
+   wsJsonToRes,
    pictshareAvatarThumbnail,
    showAvatars,
    fetchLimit,
@@@ -235,26 -218,12 +236,26 @@@ export class Navbar extends Component<a
        this.state.mentions = unreadMentions;
        this.setState(this.state);
        this.sendUnreadCount();
-     } else if (op == UserOperation.GetPrivateMessages) {
-       let res: PrivateMessagesResponse = msg;
-       let unreadMessages = res.messages.filter(r => !r.read);
++    } else if (res.op == UserOperation.GetPrivateMessages) {
++      let data = res.data as PrivateMessagesResponse;
++      let unreadMessages = data.messages.filter(r => !r.read);
 +      if (
 +        unreadMessages.length > 0 &&
 +        this.state.fetchCount > 1 &&
 +        JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
 +      ) {
 +        this.notify(unreadMessages);
 +      }
 +
 +      this.state.messages = unreadMessages;
 +      this.setState(this.state);
 +      this.sendUnreadCount();
-     } else if (op == UserOperation.GetSite) {
-       let res: GetSiteResponse = msg;
+     } else if (res.op == UserOperation.GetSite) {
+       let data = res.data as GetSiteResponse;
  
-       if (res.site) {
-         this.state.siteName = res.site.name;
-         WebSocketService.Instance.site = res.site;
+       if (data.site) {
+         this.state.siteName = data.site.name;
+         WebSocketService.Instance.site = data.site;
          this.setState(this.state);
        }
      }
index 76b4fb010429282b5824c4bf9e1d64ee4244a46c,97f108883be856aaaa942e34eb52792cbfa40ce4..10b6867c9e5a35aab99791321699ade4a46c3bea
@@@ -5,9 -5,10 +5,10 @@@ import 
    UserOperation,
    LoginResponse,
    PasswordChangeForm,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
- import { msgOp, capitalizeFirstLetter, toast } from '../utils';
 -import { wsJsonToRes, capitalizeFirstLetter } from '../utils';
++import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
  
@@@ -133,10 -134,10 +134,10 @@@ export class PasswordChange extends Com
      WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
    }
  
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
+   parseMessage(msg: WebSocketJsonResponse) {
+     let res = wsJsonToRes(msg);
      if (msg.error) {
 -      alert(i18n.t(msg.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state.loading = false;
        this.setState(this.state);
        return;
index 97a44094a501a7ba32ab3301c3173fd97b62a635,454a569fa94d62c209dc2a10d3dbb71d12f63825..440617743e3bd1a904e5eb754e67025c8608b767
@@@ -458,10 -458,10 +459,10 @@@ export class PostForm extends Component
        });
    }
  
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+   parseMessage(msg: WebSocketJsonResponse) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state.loading = false;
        this.setState(this.state);
        return;
index 308fce850c99c8539a05f020ca8508603436330f,1e334b1d22aa96d48241c0e57b41235400150066..931ced2d1d5c0d7399c408eb794ab14f701c9140
@@@ -26,9 -26,10 +26,10 @@@ import 
    SearchResponse,
    GetSiteResponse,
    GetCommunityResponse,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
- import { msgOp, hotRank, toast } from '../utils';
 -import { wsJsonToRes, hotRank } from '../utils';
++import { wsJsonToRes, hotRank, toast } from '../utils';
  import { PostListing } from './post-listing';
  import { PostListings } from './post-listings';
  import { Sidebar } from './sidebar';
@@@ -341,19 -342,19 +342,19 @@@ export class Post extends Component<any
      );
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.GetPost) {
-       let res: GetPostResponse = msg;
-       this.state.post = res.post;
-       this.state.comments = res.comments;
-       this.state.community = res.community;
-       this.state.moderators = res.moderators;
-       this.state.admins = res.admins;
+     } 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.admins = data.admins;
        this.state.loading = false;
        document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
  
        }
  
        this.setState(this.state);
-     } else if (op == UserOperation.CreateComment) {
-       let res: CommentResponse = msg;
-       this.state.comments.unshift(res.comment);
+     } else if (res.op == UserOperation.CreateComment) {
+       let data = res.data as CommentResponse;
+       this.state.comments.unshift(data.comment);
        this.setState(this.state);
-     } else if (op == UserOperation.EditComment) {
-       let res: CommentResponse = msg;
-       let found = this.state.comments.find(c => c.id == res.comment.id);
-       found.content = res.comment.content;
-       found.updated = res.comment.updated;
-       found.removed = res.comment.removed;
-       found.deleted = res.comment.deleted;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       found.score = res.comment.score;
-       found.read = res.comment.read;
+     } else if (res.op == UserOperation.EditComment) {
+       let data = res.data as CommentResponse;
+       let found = this.state.comments.find(c => c.id == data.comment.id);
+       found.content = data.comment.content;
+       found.updated = data.comment.updated;
+       found.removed = data.comment.removed;
+       found.deleted = data.comment.deleted;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
+       found.score = data.comment.score;
+       found.read = data.comment.read;
  
        this.setState(this.state);
-     } else if (op == UserOperation.SaveComment) {
-       let res: CommentResponse = msg;
-       let found = this.state.comments.find(c => c.id == res.comment.id);
-       found.saved = res.comment.saved;
+     } else if (res.op == UserOperation.SaveComment) {
+       let data = res.data as CommentResponse;
+       let found = this.state.comments.find(c => c.id == data.comment.id);
+       found.saved = data.comment.saved;
        this.setState(this.state);
-     } else if (op == UserOperation.CreateCommentLike) {
-       let res: CommentResponse = msg;
+     } else if (res.op == UserOperation.CreateCommentLike) {
+       let data = res.data as CommentResponse;
        let found: Comment = this.state.comments.find(
-         c => c.id === res.comment.id
+         c => c.id === data.comment.id
        );
-       found.score = res.comment.score;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       if (res.comment.my_vote !== null) {
-         found.my_vote = res.comment.my_vote;
+       found.score = data.comment.score;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
 -      if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
++      if (data.comment.my_vote !== null) {
++        found.my_vote = data.comment.my_vote;
 +        found.upvoteLoading = false;
 +        found.downvoteLoading = false;
 +      }
        this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let res: CreatePostLikeResponse = msg;
-       this.state.post.my_vote = res.post.my_vote;
-       this.state.post.score = res.post.score;
-       this.state.post.upvotes = res.post.upvotes;
-       this.state.post.downvotes = res.post.downvotes;
-       this.state.post.upvoteLoading = false;
-       this.state.post.downvoteLoading = false;
+     } else if (res.op == UserOperation.CreatePostLike) {
+       let data = res.data as CreatePostLikeResponse;
+       this.state.post.my_vote = data.post.my_vote;
+       this.state.post.score = data.post.score;
+       this.state.post.upvotes = data.post.upvotes;
+       this.state.post.downvotes = data.post.downvotes;
        this.setState(this.state);
-     } else if (op == UserOperation.EditPost) {
-       let res: PostResponse = msg;
-       this.state.post = res.post;
+     } else if (res.op == UserOperation.EditPost) {
+       let data = res.data as PostResponse;
+       this.state.post = data.post;
        this.setState(this.state);
-     } else if (op == UserOperation.SavePost) {
-       let res: PostResponse = msg;
-       this.state.post = res.post;
+     } else if (res.op == UserOperation.SavePost) {
+       let data = res.data as PostResponse;
+       this.state.post = data.post;
        this.setState(this.state);
-     } else if (op == UserOperation.EditCommunity) {
-       let res: CommunityResponse = msg;
-       this.state.community = res.community;
-       this.state.post.community_id = res.community.id;
-       this.state.post.community_name = res.community.name;
+     } else if (res.op == UserOperation.EditCommunity) {
+       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.setState(this.state);
-     } else if (op == UserOperation.FollowCommunity) {
-       let res: CommunityResponse = msg;
-       this.state.community.subscribed = res.community.subscribed;
+     } 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 =
-         res.community.number_of_subscribers;
+         data.community.number_of_subscribers;
        this.setState(this.state);
-     } else if (op == UserOperation.BanFromCommunity) {
-       let res: BanFromCommunityResponse = msg;
+     } else if (res.op == UserOperation.BanFromCommunity) {
+       let data = res.data as BanFromCommunityResponse;
        this.state.comments
-         .filter(c => c.creator_id == res.user.id)
-         .forEach(c => (c.banned_from_community = res.banned));
-       if (this.state.post.creator_id == res.user.id) {
-         this.state.post.banned_from_community = res.banned;
+         .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;
        }
        this.setState(this.state);
-     } else if (op == UserOperation.AddModToCommunity) {
-       let res: AddModToCommunityResponse = msg;
-       this.state.moderators = res.moderators;
+     } else if (res.op == UserOperation.AddModToCommunity) {
+       let data = res.data as AddModToCommunityResponse;
+       this.state.moderators = data.moderators;
        this.setState(this.state);
-     } else if (op == UserOperation.BanUser) {
-       let res: BanUserResponse = msg;
+     } else if (res.op == UserOperation.BanUser) {
+       let data = res.data as BanUserResponse;
        this.state.comments
-         .filter(c => c.creator_id == res.user.id)
-         .forEach(c => (c.banned = res.banned));
-       if (this.state.post.creator_id == res.user.id) {
-         this.state.post.banned = res.banned;
+         .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;
        }
        this.setState(this.state);
-     } else if (op == UserOperation.AddAdmin) {
-       let res: AddAdminResponse = msg;
-       this.state.admins = res.admins;
+     } else if (res.op == UserOperation.AddAdmin) {
+       let data = res.data as AddAdminResponse;
+       this.state.admins = data.admins;
        this.setState(this.state);
-     } else if (op == UserOperation.Search) {
-       let res: SearchResponse = msg;
-       this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
+     } else if (res.op == UserOperation.Search) {
+       let data = res.data as SearchResponse;
+       this.state.crossPosts = data.posts.filter(
+         p => p.id != this.state.post.id
+       );
        this.setState(this.state);
-     } else if (op == UserOperation.TransferSite) {
-       let res: GetSiteResponse = msg;
+     } else if (res.op == UserOperation.TransferSite) {
+       let data = res.data as GetSiteResponse;
  
-       this.state.admins = res.admins;
+       this.state.admins = data.admins;
        this.setState(this.state);
-     } else if (op == UserOperation.TransferCommunity) {
-       let res: GetCommunityResponse = msg;
-       this.state.community = res.community;
-       this.state.moderators = res.moderators;
-       this.state.admins = res.admins;
+     } else if (res.op == UserOperation.TransferCommunity) {
+       let data = res.data as GetCommunityResponse;
+       this.state.community = data.community;
+       this.state.moderators = data.moderators;
+       this.state.admins = data.admins;
        this.setState(this.state);
      }
    }
index 96bd807d3ef1f970d247067f0cbcfe0fe67e9d6e,0000000000000000000000000000000000000000..5ee1c1fd57bf54e8538eea798bac0d148bbebd76
mode 100644,000000..100644
--- /dev/null
@@@ -1,292 -1,0 +1,293 @@@
-   msgOp,
 +import { Component, linkEvent } from 'inferno';
 +import { Link } from 'inferno-router';
 +import { Subscription } from 'rxjs';
 +import { retryWhen, delay, take } from 'rxjs/operators';
 +import {
 +  PrivateMessageForm as PrivateMessageFormI,
 +  EditPrivateMessageForm,
 +  PrivateMessageFormParams,
 +  PrivateMessage,
 +  PrivateMessageResponse,
 +  UserView,
 +  UserOperation,
 +  UserDetailsResponse,
 +  GetUserDetailsForm,
 +  SortType,
++  WebSocketJsonResponse,
 +} from '../interfaces';
 +import { WebSocketService } from '../services';
 +import {
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
 +  capitalizeFirstLetter,
 +  markdownHelpUrl,
 +  mdToHtml,
 +  showAvatars,
 +  pictshareAvatarThumbnail,
++  wsJsonToRes,
 +  toast,
 +} from '../utils';
 +import autosize from 'autosize';
 +import { i18n } from '../i18next';
 +import { T } from 'inferno-i18next';
 +
 +interface PrivateMessageFormProps {
 +  privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
 +  params?: PrivateMessageFormParams;
 +  onCancel?(): any;
 +  onCreate?(message: PrivateMessage): any;
 +  onEdit?(message: PrivateMessage): any;
 +}
 +
 +interface PrivateMessageFormState {
 +  privateMessageForm: PrivateMessageFormI;
 +  recipient: UserView;
 +  loading: boolean;
 +  previewMode: boolean;
 +  showDisclaimer: boolean;
 +}
 +
 +export class PrivateMessageForm extends Component<
 +  PrivateMessageFormProps,
 +  PrivateMessageFormState
 +> {
 +  private subscription: Subscription;
 +  private emptyState: PrivateMessageFormState = {
 +    privateMessageForm: {
 +      content: null,
 +      recipient_id: null,
 +    },
 +    recipient: null,
 +    loading: false,
 +    previewMode: false,
 +    showDisclaimer: false,
 +  };
 +
 +  constructor(props: any, context: any) {
 +    super(props, context);
 +
 +    this.state = this.emptyState;
 +
 +    if (this.props.privateMessage) {
 +      this.state.privateMessageForm = {
 +        content: this.props.privateMessage.content,
 +        recipient_id: this.props.privateMessage.recipient_id,
 +      };
 +    }
 +
 +    if (this.props.params) {
 +      this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
 +      let form: GetUserDetailsForm = {
 +        user_id: this.state.privateMessageForm.recipient_id,
 +        sort: SortType[SortType.New],
 +        saved_only: false,
 +      };
 +      WebSocketService.Instance.getUserDetails(form);
 +    }
 +
 +    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')
 +      );
 +  }
 +
 +  componentDidMount() {
 +    autosize(document.querySelectorAll('textarea'));
 +  }
 +
 +  componentWillUnmount() {
 +    this.subscription.unsubscribe();
 +  }
 +
 +  render() {
 +    return (
 +      <div>
 +        <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
 +          {!this.props.privateMessage && (
 +            <div class="form-group row">
 +              <label class="col-sm-2 col-form-label">
 +                {capitalizeFirstLetter(i18n.t('to'))}
 +              </label>
 +
 +              {this.state.recipient && (
 +                <div class="col-sm-10 form-control-plaintext">
 +                  <Link
 +                    className="text-info"
 +                    to={`/u/${this.state.recipient.name}`}
 +                  >
 +                    {this.state.recipient.avatar && showAvatars() && (
 +                      <img
 +                        height="32"
 +                        width="32"
 +                        src={pictshareAvatarThumbnail(
 +                          this.state.recipient.avatar
 +                        )}
 +                        class="rounded-circle mr-1"
 +                      />
 +                    )}
 +                    <span>{this.state.recipient.name}</span>
 +                  </Link>
 +                </div>
 +              )}
 +            </div>
 +          )}
 +          <div class="form-group row">
 +            <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
 +            <div class="col-sm-10">
 +              <textarea
 +                value={this.state.privateMessageForm.content}
 +                onInput={linkEvent(this, this.handleContentChange)}
 +                className={`form-control ${this.state.previewMode && 'd-none'}`}
 +                rows={4}
 +                maxLength={10000}
 +              />
 +              {this.state.previewMode && (
 +                <div
 +                  className="md-div"
 +                  dangerouslySetInnerHTML={mdToHtml(
 +                    this.state.privateMessageForm.content
 +                  )}
 +                />
 +              )}
 +
 +              {this.state.privateMessageForm.content && (
 +                <button
 +                  className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
 +                    .previewMode && 'active'}`}
 +                  onClick={linkEvent(this, this.handlePreviewToggle)}
 +                >
 +                  {i18n.t('preview')}
 +                </button>
 +              )}
 +              <ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
 +                <li class="list-inline-item">
 +                  <span
 +                    onClick={linkEvent(this, this.handleShowDisclaimer)}
 +                    class="pointer"
 +                  >
 +                    {i18n.t('disclaimer')}
 +                  </span>
 +                </li>
 +                <li class="list-inline-item">
 +                  <a href={markdownHelpUrl} target="_blank" class="text-muted">
 +                    {i18n.t('formatting_help')}
 +                  </a>
 +                </li>
 +              </ul>
 +            </div>
 +          </div>
 +
 +          {this.state.showDisclaimer && (
 +            <div class="form-group row">
 +              <div class="col-sm-10">
 +                <div class="alert alert-danger" role="alert">
 +                  <T i18nKey="private_message_disclaimer">
 +                    #
 +                    <a
 +                      class="alert-link"
 +                      target="_blank"
 +                      href="https://about.riot.im/"
 +                    >
 +                      #
 +                    </a>
 +                  </T>
 +                </div>
 +              </div>
 +            </div>
 +          )}
 +          <div class="form-group row">
 +            <div class="col-sm-10">
 +              <button type="submit" class="btn btn-secondary mr-2">
 +                {this.state.loading ? (
 +                  <svg class="icon icon-spinner spin">
 +                    <use xlinkHref="#icon-spinner"></use>
 +                  </svg>
 +                ) : this.props.privateMessage ? (
 +                  capitalizeFirstLetter(i18n.t('save'))
 +                ) : (
 +                  capitalizeFirstLetter(i18n.t('send_message'))
 +                )}
 +              </button>
 +              {this.props.privateMessage && (
 +                <button
 +                  type="button"
 +                  class="btn btn-secondary"
 +                  onClick={linkEvent(this, this.handleCancel)}
 +                >
 +                  {i18n.t('cancel')}
 +                </button>
 +              )}
 +            </div>
 +          </div>
 +        </form>
 +      </div>
 +    );
 +  }
 +
 +  handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
 +    event.preventDefault();
 +    if (i.props.privateMessage) {
 +      let editForm: EditPrivateMessageForm = {
 +        edit_id: i.props.privateMessage.id,
 +        content: i.state.privateMessageForm.content,
 +      };
 +      WebSocketService.Instance.editPrivateMessage(editForm);
 +    } else {
 +      WebSocketService.Instance.createPrivateMessage(
 +        i.state.privateMessageForm
 +      );
 +    }
 +    i.state.loading = true;
 +    i.setState(i.state);
 +  }
 +
 +  handleRecipientChange(i: PrivateMessageForm, event: any) {
 +    i.state.recipient = event.target.value;
 +    i.setState(i.state);
 +  }
 +
 +  handleContentChange(i: PrivateMessageForm, event: any) {
 +    i.state.privateMessageForm.content = event.target.value;
 +    i.setState(i.state);
 +  }
 +
 +  handleCancel(i: PrivateMessageForm) {
 +    i.props.onCancel();
 +  }
 +
 +  handlePreviewToggle(i: PrivateMessageForm, event: any) {
 +    event.preventDefault();
 +    i.state.previewMode = !i.state.previewMode;
 +    i.setState(i.state);
 +  }
 +
 +  handleShowDisclaimer(i: PrivateMessageForm) {
 +    i.state.showDisclaimer = !i.state.showDisclaimer;
 +    i.setState(i.state);
 +  }
 +
-     } else if (op == UserOperation.EditPrivateMessage) {
++  parseMessage(msg: WebSocketJsonResponse) {
++    let res = wsJsonToRes(msg);
++    if (res.error) {
 +      toast(i18n.t(msg.error), 'danger');
 +      this.state.loading = false;
 +      this.setState(this.state);
 +      return;
-       let res: PrivateMessageResponse = msg;
-       this.props.onEdit(res.message);
-     } else if (op == UserOperation.GetUserDetails) {
-       let res: UserDetailsResponse = msg;
-       this.state.recipient = res.user;
-       this.state.privateMessageForm.recipient_id = res.user.id;
++    } else if (res.op == UserOperation.EditPrivateMessage) {
++      let data = res.data as PrivateMessageResponse;
 +      this.state.loading = false;
-     } else if (op == UserOperation.CreatePrivateMessage) {
++      this.props.onEdit(data.message);
++    } else if (res.op == UserOperation.GetUserDetails) {
++      let data = res.data as UserDetailsResponse;
++      this.state.recipient = data.user;
++      this.state.privateMessageForm.recipient_id = data.user.id;
 +      this.setState(this.state);
-       let res: PrivateMessageResponse = msg;
-       this.props.onCreate(res.message);
++    } else if (res.op == UserOperation.CreatePrivateMessage) {
++      let data = res.data as PrivateMessageResponse;
 +      this.state.loading = false;
++      this.props.onCreate(data.message);
 +      this.setState(this.state);
 +    }
 +  }
 +}
index d2280cb27272cc3c8999178d1e96e331a6d60dea,ae0f8dbc180ca458690fa645e599f2b40e12a537..18b5d34199a0b8e5dc3fe8a504479451d96fd0b6
@@@ -12,8 -12,7 +12,9 @@@ import 
    SearchForm,
    SearchResponse,
    SearchType,
 +  CreatePostLikeResponse,
 +  CommentResponse,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService } from '../services';
  import {
@@@ -477,45 -461,21 +476,45 @@@ export class Search extends Component<a
      );
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        return;
-     } else if (op == UserOperation.Search) {
-       let res: SearchResponse = msg;
-       this.state.searchResponse = res;
+     } else if (res.op == UserOperation.Search) {
+       let data = res.data as SearchResponse;
+       this.state.searchResponse = data;
        this.state.loading = false;
        document.title = `${i18n.t('search')} - ${this.state.q} - ${
          WebSocketService.Instance.site.name
        }`;
        window.scrollTo(0, 0);
        this.setState(this.state);
-     } else if (op == UserOperation.CreateCommentLike) {
-       let res: CommentResponse = msg;
++    } else if (res.op == UserOperation.CreateCommentLike) {
++      let data = res.data as CommentResponse;
 +      let found: Comment = this.state.searchResponse.comments.find(
-         c => c.id === res.comment.id
++        c => c.id === data.comment.id
 +      );
-       found.score = res.comment.score;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       if (res.comment.my_vote !== null) {
-         found.my_vote = res.comment.my_vote;
++      found.score = data.comment.score;
++      found.upvotes = data.comment.upvotes;
++      found.downvotes = data.comment.downvotes;
++      if (data.comment.my_vote !== null) {
++        found.my_vote = data.comment.my_vote;
 +        found.upvoteLoading = false;
 +        found.downvoteLoading = false;
 +      }
 +      this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let res: CreatePostLikeResponse = msg;
++    } else if (res.op == UserOperation.CreatePostLike) {
++      let data = res.data as CreatePostLikeResponse;
 +      let found = this.state.searchResponse.posts.find(
-         c => c.id == res.post.id
++        c => c.id == data.post.id
 +      );
-       found.my_vote = res.post.my_vote;
-       found.score = res.post.score;
-       found.upvotes = res.post.upvotes;
-       found.downvotes = res.post.downvotes;
++      found.my_vote = data.post.my_vote;
++      found.score = data.post.score;
++      found.upvotes = data.post.upvotes;
++      found.downvotes = data.post.downvotes;
 +      this.setState(this.state);
      }
    }
  }
index d06a9a58a795636a88f43f63cfd16fdd22ae0e95,c4c9dc63a8e91622a8084b39abe07802fad3064a..26475a387ea72144f2685a2cd5f6b700c4505616
@@@ -1,9 -1,14 +1,14 @@@
  import { Component, linkEvent } from 'inferno';
  import { Subscription } from 'rxjs';
  import { retryWhen, delay, take } from 'rxjs/operators';
- import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
+ import {
+   RegisterForm,
+   LoginResponse,
+   UserOperation,
+   WebSocketJsonResponse,
+ } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
- import { msgOp, toast } from '../utils';
 -import { wsJsonToRes } from '../utils';
++import { wsJsonToRes, toast } from '../utils';
  import { SiteForm } from './site-form';
  import { i18n } from '../i18next';
  import { T } from 'inferno-i18next';
@@@ -181,10 -186,10 +186,10 @@@ export class Setup extends Component<an
      i.setState(i.state);
    }
  
-   parseMessage(msg: any) {
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+   parseMessage(msg: WebSocketJsonResponse) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state.userLoading = false;
        this.setState(this.state);
        return;
index 89bc478585f2eeee2ae27053a34e7e82a9b3497b,606d85ab495ce6f4b1c6d52df0760e60fd290a04..09129d67ca06365be01b92e362d624df48b344c1
@@@ -18,7 -18,7 +18,8 @@@ import 
    BanUserResponse,
    AddAdminResponse,
    DeleteAccountForm,
 +  CreatePostLikeResponse,
+   WebSocketJsonResponse,
  } from '../interfaces';
  import { WebSocketService, UserService } from '../services';
  import {
@@@ -1012,11 -969,11 +1013,11 @@@ export class User extends Component<any
      WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
    }
  
-   parseMessage(msg: any) {
+   parseMessage(msg: WebSocketJsonResponse) {
      console.log(msg);
-     let op: UserOperation = msgOp(msg);
-     if (msg.error) {
+     let res = wsJsonToRes(msg);
+     if (res.error) {
 -      alert(i18n.t(res.error));
 +      toast(i18n.t(msg.error), 'danger');
        this.state.deleteAccountLoading = false;
        this.state.avatarLoading = false;
        this.state.userSettingsLoading = false;
        document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
        window.scrollTo(0, 0);
        this.setState(this.state);
-     } else if (op == UserOperation.EditComment) {
-       let res: CommentResponse = msg;
+     } else if (res.op == UserOperation.EditComment) {
+       let data = res.data as CommentResponse;
  
-       let found = this.state.comments.find(c => c.id == res.comment.id);
-       found.content = res.comment.content;
-       found.updated = res.comment.updated;
-       found.removed = res.comment.removed;
-       found.deleted = res.comment.deleted;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       found.score = res.comment.score;
+       let found = this.state.comments.find(c => c.id == data.comment.id);
+       found.content = data.comment.content;
+       found.updated = data.comment.updated;
+       found.removed = data.comment.removed;
+       found.deleted = data.comment.deleted;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
+       found.score = data.comment.score;
  
        this.setState(this.state);
-     } else if (op == UserOperation.CreateComment) {
+     } else if (res.op == UserOperation.CreateComment) {
        // let res: CommentResponse = msg;
 -      alert(i18n.t('reply_sent'));
 +      toast(i18n.t('reply_sent'));
        // this.state.comments.unshift(res.comment); // TODO do this right
        // this.setState(this.state);
-     } else if (op == UserOperation.SaveComment) {
-       let res: CommentResponse = msg;
-       let found = this.state.comments.find(c => c.id == res.comment.id);
-       found.saved = res.comment.saved;
+     } else if (res.op == UserOperation.SaveComment) {
+       let data = res.data as CommentResponse;
+       let found = this.state.comments.find(c => c.id == data.comment.id);
+       found.saved = data.comment.saved;
        this.setState(this.state);
-     } else if (op == UserOperation.CreateCommentLike) {
-       let res: CommentResponse = msg;
+     } else if (res.op == UserOperation.CreateCommentLike) {
+       let data = res.data as CommentResponse;
        let found: Comment = this.state.comments.find(
-         c => c.id === res.comment.id
+         c => c.id === data.comment.id
        );
-       found.score = res.comment.score;
-       found.upvotes = res.comment.upvotes;
-       found.downvotes = res.comment.downvotes;
-       if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+       found.score = data.comment.score;
+       found.upvotes = data.comment.upvotes;
+       found.downvotes = data.comment.downvotes;
+       if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
        this.setState(this.state);
-     } else if (op == UserOperation.CreatePostLike) {
-       let res: CreatePostLikeResponse = msg;
-       let found = this.state.posts.find(c => c.id == res.post.id);
-       found.my_vote = res.post.my_vote;
-       found.score = res.post.score;
-       found.upvotes = res.post.upvotes;
-       found.downvotes = res.post.downvotes;
++    } else if (res.op == UserOperation.CreatePostLike) {
++      let data = res.data as CreatePostLikeResponse;
++      let found = this.state.posts.find(c => c.id == data.post.id);
++      found.my_vote = data.post.my_vote;
++      found.score = data.post.score;
++      found.upvotes = data.post.upvotes;
++      found.downvotes = data.post.downvotes;
 +      this.setState(this.state);
-     } else if (op == UserOperation.BanUser) {
-       let res: BanUserResponse = msg;
+     } else if (res.op == UserOperation.BanUser) {
+       let data = res.data as BanUserResponse;
        this.state.comments
-         .filter(c => c.creator_id == res.user.id)
-         .forEach(c => (c.banned = res.banned));
+         .filter(c => c.creator_id == data.user.id)
+         .forEach(c => (c.banned = data.banned));
        this.state.posts
-         .filter(c => c.creator_id == res.user.id)
-         .forEach(c => (c.banned = res.banned));
+         .filter(c => c.creator_id == data.user.id)
+         .forEach(c => (c.banned = data.banned));
        this.setState(this.state);
-     } else if (op == UserOperation.AddAdmin) {
-       let res: AddAdminResponse = msg;
-       this.state.admins = res.admins;
+     } else if (res.op == UserOperation.AddAdmin) {
+       let data = res.data as AddAdminResponse;
+       this.state.admins = data.admins;
        this.setState(this.state);
-     } else if (op == UserOperation.SaveUserSettings) {
+     } else if (res.op == UserOperation.SaveUserSettings) {
+       let data = res.data as LoginResponse;
        this.state = this.emptyState;
        this.state.userSettingsLoading = false;
        this.setState(this.state);
index fc7d8cb7e52f940701669ff2c039b4eb0f560daa,63c608563ff1f1a77558e7addbd5063d64290b2e..bd954d20a4e1e2fdb2ee98ac5268c2d8fb6e255c
@@@ -750,37 -701,34 +726,69 @@@ export interface PasswordChangeForm 
    password_verify: string;
  }
  
-   op: string;
 +export interface PrivateMessageForm {
 +  content: string;
 +  recipient_id: number;
 +  auth?: string;
 +}
 +
 +export interface PrivateMessageFormParams {
 +  recipient_id: number;
 +}
 +
 +export interface EditPrivateMessageForm {
 +  edit_id: number;
 +  content?: string;
 +  deleted?: boolean;
 +  read?: boolean;
 +  auth?: string;
 +}
 +
 +export interface GetPrivateMessagesForm {
 +  unread_only: boolean;
 +  page?: number;
 +  limit?: number;
 +  auth?: string;
 +}
 +
 +export interface PrivateMessagesResponse {
-   op: string;
 +  messages: Array<PrivateMessage>;
 +}
 +
 +export interface PrivateMessageResponse {
 -  | AddAdminResponse;
 +  message: PrivateMessage;
 +}
++
+ type ResponseType =
+   | SiteResponse
+   | GetFollowedCommunitiesResponse
+   | ListCommunitiesResponse
+   | GetPostsResponse
+   | CreatePostLikeResponse
+   | GetRepliesResponse
+   | GetUserMentionsResponse
+   | ListCategoriesResponse
+   | CommunityResponse
+   | CommentResponse
+   | UserMentionResponse
+   | LoginResponse
+   | GetModlogResponse
+   | SearchResponse
+   | BanFromCommunityResponse
+   | AddModToCommunityResponse
+   | BanUserResponse
++  | AddAdminResponse
++  | PrivateMessageResponse
++  | PrivateMessagesResponse;
+ export interface WebSocketResponse {
+   op: UserOperation;
+   data: ResponseType;
+   error?: string;
+ }
+ export interface WebSocketJsonResponse {
+   op: string;
+   data: ResponseType;
+   error?: string;
+ }
diff --cc ui/src/utils.ts
Simple merge