]> Untitled Git - lemmy.git/blobdiff - ui/src/components/user.tsx
Merge branch 'master' into federation_merge_from_master_2
[lemmy.git] / ui / src / components / user.tsx
index 361ce633a50a2c61af63e33c95e0f3bdd6f7b597..f635a1cd0b1d8eb48582b62f9c06fdb7234a0bb7 100644 (file)
@@ -18,23 +18,34 @@ import {
   BanUserResponse,
   AddAdminResponse,
   DeleteAccountForm,
+  PostResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
-  msgOp,
+  wsJsonToRes,
   fetchLimit,
   routeSortTypeToEnum,
   capitalizeFirstLetter,
   themes,
   setTheme,
+  languages,
+  showAvatars,
+  toast,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  commentsToFlatNodes,
+  setupTippy,
 } from '../utils';
 import { PostListing } from './post-listing';
+import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
 import { ListingTypeSelect } from './listing-type-select';
 import { CommentNodes } from './comment-nodes';
 import { MomentTime } from './moment-time';
 import { i18n } from '../i18next';
-import { T } from 'inferno-i18next';
 
 enum View {
   Overview,
@@ -57,6 +68,7 @@ interface UserState {
   sort: SortType;
   page: number;
   loading: boolean;
+  avatarLoading: boolean;
   userSettingsForm: UserSettingsForm;
   userSettingsLoading: boolean;
   deleteAccountLoading: boolean;
@@ -70,13 +82,17 @@ export class User extends Component<any, UserState> {
     user: {
       id: null,
       name: null,
-      fedi_name: null,
       published: null,
       number_of_posts: null,
       post_score: null,
       number_of_comments: null,
       comment_score: null,
       banned: null,
+      avatar: null,
+      show_avatars: null,
+      send_notifications_to_email: null,
+      actor_id: null,
+      local: null,
     },
     user_id: null,
     username: null,
@@ -86,6 +102,7 @@ export class User extends Component<any, UserState> {
     posts: [],
     admins: [],
     loading: true,
+    avatarLoading: false,
     view: this.getViewFromProps(this.props),
     sort: this.getSortTypeFromProps(this.props),
     page: this.getPageFromProps(this.props),
@@ -94,6 +111,9 @@ export class User extends Component<any, UserState> {
       theme: null,
       default_sort_type: null,
       default_listing_type: null,
+      lang: null,
+      show_avatars: null,
+      send_notifications_to_email: null,
       auth: null,
     },
     userSettingsLoading: null,
@@ -120,14 +140,7 @@ export class User extends Component<any, UserState> {
     this.state.username = this.props.match.params.username;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -178,6 +191,17 @@ export class User extends Component<any, UserState> {
     }
   }
 
+  componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
+    // Necessary if you are on a post and you click another post (same route)
+    if (
+      lastProps.location.pathname.split('/')[2] !==
+      lastProps.history.location.pathname.split('/')[2]
+    ) {
+      // Couldnt get a refresh working. This does for now.
+      location.reload();
+    }
+  }
+
   render() {
     return (
       <div class="container">
@@ -190,7 +214,17 @@ export class User extends Component<any, UserState> {
         ) : (
           <div class="row">
             <div class="col-12 col-md-8">
-              <h5>/u/{this.state.user.name}</h5>
+              <h5>
+                {this.state.user.avatar && showAvatars() && (
+                  <img
+                    height="80"
+                    width="80"
+                    src={this.state.user.avatar}
+                    class="rounded-circle mr-2"
+                  />
+                )}
+                <span>/u/{this.state.user.name}</span>
+              </h5>
               {this.selects()}
               {this.state.view == View.Overview && this.overview()}
               {this.state.view == View.Comments && this.comments()}
@@ -210,37 +244,85 @@ export class User extends Component<any, UserState> {
     );
   }
 
+  viewRadios() {
+    return (
+      <div class="btn-group btn-group-toggle">
+        <label
+          className={`btn btn-sm btn-secondary pointer btn-outline-light
+            ${this.state.view == View.Overview && 'active'}
+          `}
+        >
+          <input
+            type="radio"
+            value={View.Overview}
+            checked={this.state.view == View.Overview}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t('overview')}
+        </label>
+        <label
+          className={`btn btn-sm btn-secondary pointer btn-outline-light
+            ${this.state.view == View.Comments && 'active'}
+          `}
+        >
+          <input
+            type="radio"
+            value={View.Comments}
+            checked={this.state.view == View.Comments}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t('comments')}
+        </label>
+        <label
+          className={`btn btn-sm btn-secondary pointer btn-outline-light
+            ${this.state.view == View.Posts && 'active'}
+          `}
+        >
+          <input
+            type="radio"
+            value={View.Posts}
+            checked={this.state.view == View.Posts}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t('posts')}
+        </label>
+        <label
+          className={`btn btn-sm btn-secondary pointer btn-outline-light
+            ${this.state.view == View.Saved && 'active'}
+          `}
+        >
+          <input
+            type="radio"
+            value={View.Saved}
+            checked={this.state.view == View.Saved}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t('saved')}
+        </label>
+      </div>
+    );
+  }
+
   selects() {
     return (
       <div className="mb-2">
-        <select
-          value={this.state.view}
-          onChange={linkEvent(this, this.handleViewChange)}
-          class="custom-select custom-select-sm w-auto"
+        <span class="mr-3">{this.viewRadios()}</span>
+        <SortSelect
+          sort={this.state.sort}
+          onChange={this.handleSortChange}
+          hideHot
+        />
+        <a
+          href={`/feeds/u/${this.state.username}.xml?sort=${
+            SortType[this.state.sort]
+          }`}
+          target="_blank"
+          title="RSS"
         >
-          <option disabled>
-            <T i18nKey="view">#</T>
-          </option>
-          <option value={View.Overview}>
-            <T i18nKey="overview">#</T>
-          </option>
-          <option value={View.Comments}>
-            <T i18nKey="comments">#</T>
-          </option>
-          <option value={View.Posts}>
-            <T i18nKey="posts">#</T>
-          </option>
-          <option value={View.Saved}>
-            <T i18nKey="saved">#</T>
-          </option>
-        </select>
-        <span class="ml-2">
-          <SortSelect
-            sort={this.state.sort}
-            onChange={this.handleSortChange}
-            hideHot
-          />
-        </span>
+          <svg class="icon mx-2 text-muted small">
+            <use xlinkHref="#icon-rss">#</use>
+          </svg>
+        </a>
       </div>
     );
   }
@@ -273,13 +355,13 @@ export class User extends Component<any, UserState> {
                 post={i.data as Post}
                 admins={this.state.admins}
                 showCommunity
-                viewOnly
               />
             ) : (
               <CommentNodes
                 nodes={[{ comment: i.data as Comment }]}
                 admins={this.state.admins}
                 noIndent
+                showContext
               />
             )}
           </div>
@@ -291,13 +373,12 @@ export class User extends Component<any, UserState> {
   comments() {
     return (
       <div>
-        {this.state.comments.map(comment => (
-          <CommentNodes
-            nodes={[{ comment: comment }]}
-            admins={this.state.admins}
-            noIndent
-          />
-        ))}
+        <CommentNodes
+          nodes={commentsToFlatNodes(this.state.comments)}
+          admins={this.state.admins}
+          noIndent
+          showContext
+        />
       </div>
     );
   }
@@ -306,12 +387,7 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.posts.map(post => (
-          <PostListing
-            post={post}
-            admins={this.state.admins}
-            showCommunity
-            viewOnly
-          />
+          <PostListing post={post} admins={this.state.admins} showCommunity />
         ))}
       </div>
     );
@@ -325,64 +401,79 @@ export class User extends Component<any, UserState> {
           <div class="card-body">
             <h5>
               <ul class="list-inline mb-0">
-                <li className="list-inline-item">{user.name}</li>
+                <li className="list-inline-item">
+                  <UserListing user={user} realLink />
+                </li>
                 {user.banned && (
                   <li className="list-inline-item badge badge-danger">
-                    <T i18nKey="banned">#</T>
+                    {i18n.t('banned')}
                   </li>
                 )}
               </ul>
             </h5>
             <div>
-              {i18n.t('joined')} <MomentTime data={user} />
+              {i18n.t('joined')} <MomentTime data={user} showAgo />
             </div>
-            <div class="table-responsive">
+            <div class="table-responsive mt-1">
               <table class="table table-bordered table-sm mt-2 mb-0">
+                {/*
                 <tr>
+                  <td class="text-center" colSpan={2}>
+                    {i18n.t('number_of_points', {
+                      count: user.post_score + user.comment_score,
+                    })}
+                  </td>
+                </tr>
+                */}
+                <tr>
+                  {/* 
                   <td>
-                    <T
-                      i18nKey="number_of_points"
-                      interpolation={{ count: user.post_score }}
-                    >
-                      #
-                    </T>
+                    {i18n.t('number_of_points', { count: user.post_score })}
                   </td>
+                  */}
                   <td>
-                    <T
-                      i18nKey="number_of_posts"
-                      interpolation={{ count: user.number_of_posts }}
-                    >
-                      #
-                    </T>
+                    {i18n.t('number_of_posts', { count: user.number_of_posts })}
                   </td>
+                  {/* 
                 </tr>
                 <tr>
                   <td>
-                    <T
-                      i18nKey="number_of_points"
-                      interpolation={{ count: user.comment_score }}
-                    >
-                      #
-                    </T>
+                    {i18n.t('number_of_points', { count: user.comment_score })}
                   </td>
+                  */}
                   <td>
-                    <T
-                      i18nKey="number_of_comments"
-                      interpolation={{ count: user.number_of_comments }}
-                    >
-                      #
-                    </T>
+                    {i18n.t('number_of_comments', {
+                      count: user.number_of_comments,
+                    })}
                   </td>
                 </tr>
               </table>
             </div>
-            {this.isCurrentUser && (
+            {this.isCurrentUser ? (
               <button
                 class="btn btn-block btn-secondary mt-3"
                 onClick={linkEvent(this, this.handleLogoutClick)}
               >
-                <T i18nKey="logout">#</T>
+                {i18n.t('logout')}
               </button>
+            ) : (
+              <>
+                <a
+                  className={`btn btn-block btn-secondary mt-3 ${
+                    !this.state.user.matrix_user_id && 'disabled'
+                  }`}
+                  target="_blank"
+                  href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+                >
+                  {i18n.t('send_secure_message')}
+                </a>
+                <Link
+                  class="btn btn-block btn-secondary mt-3"
+                  to={`/create_private_message?recipient_id=${this.state.user.id}`}
+                >
+                  {i18n.t('send_message')}
+                </Link>
+              </>
             )}
           </div>
         </div>
@@ -395,63 +486,190 @@ export class User extends Component<any, UserState> {
       <div>
         <div class="card border-secondary mb-3">
           <div class="card-body">
-            <h5>
-              <T i18nKey="settings">#</T>
-            </h5>
+            <h5>{i18n.t('settings')}</h5>
             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
               <div class="form-group">
-                <div class="col-12">
-                  <label>
-                    <T i18nKey="theme">#</T>
+                <label>{i18n.t('avatar')}</label>
+                <form class="d-inline">
+                  <label
+                    htmlFor="file-upload"
+                    class="pointer ml-4 text-muted small font-weight-bold"
+                  >
+                    {!this.state.userSettingsForm.avatar ? (
+                      <span class="btn btn-sm btn-secondary">
+                        {i18n.t('upload_avatar')}
+                      </span>
+                    ) : (
+                      <img
+                        height="80"
+                        width="80"
+                        src={this.state.userSettingsForm.avatar}
+                        class="rounded-circle"
+                      />
+                    )}
                   </label>
-                  <select
-                    value={this.state.userSettingsForm.theme}
-                    onChange={linkEvent(
+                  <input
+                    id="file-upload"
+                    type="file"
+                    accept="image/*,video/*"
+                    name="file"
+                    class="d-none"
+                    disabled={!UserService.Instance.user}
+                    onChange={linkEvent(this, this.handleImageUpload)}
+                  />
+                </form>
+              </div>
+              <div class="form-group">
+                <label>{i18n.t('language')}</label>
+                <select
+                  value={this.state.userSettingsForm.lang}
+                  onChange={linkEvent(this, this.handleUserSettingsLangChange)}
+                  class="ml-2 custom-select custom-select-sm w-auto"
+                >
+                  <option disabled>{i18n.t('language')}</option>
+                  <option value="browser">{i18n.t('browser_default')}</option>
+                  <option disabled>──</option>
+                  {languages.map(lang => (
+                    <option value={lang.code}>{lang.name}</option>
+                  ))}
+                </select>
+              </div>
+              <div class="form-group">
+                <label>{i18n.t('theme')}</label>
+                <select
+                  value={this.state.userSettingsForm.theme}
+                  onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
+                  class="ml-2 custom-select custom-select-sm w-auto"
+                >
+                  <option disabled>{i18n.t('theme')}</option>
+                  {themes.map(theme => (
+                    <option value={theme}>{theme}</option>
+                  ))}
+                </select>
+              </div>
+              <form className="form-group">
+                <label>
+                  <div class="mr-2">{i18n.t('sort_type')}</div>
+                </label>
+                <ListingTypeSelect
+                  type_={this.state.userSettingsForm.default_listing_type}
+                  onChange={this.handleUserSettingsListingTypeChange}
+                />
+              </form>
+              <form className="form-group">
+                <label>
+                  <div class="mr-2">{i18n.t('type')}</div>
+                </label>
+                <SortSelect
+                  sort={this.state.userSettingsForm.default_sort_type}
+                  onChange={this.handleUserSettingsSortTypeChange}
+                />
+              </form>
+              <div class="form-group row">
+                <label class="col-lg-3 col-form-label" htmlFor="user-email">
+                  {i18n.t('email')}
+                </label>
+                <div class="col-lg-9">
+                  <input
+                    type="email"
+                    id="user-email"
+                    class="form-control"
+                    placeholder={i18n.t('optional')}
+                    value={this.state.userSettingsForm.email}
+                    onInput={linkEvent(
                       this,
-                      this.handleUserSettingsThemeChange
+                      this.handleUserSettingsEmailChange
                     )}
-                    class="ml-2 custom-select custom-select-sm w-auto"
-                  >
-                    <option disabled>
-                      <T i18nKey="theme">#</T>
-                    </option>
-                    {themes.map(theme => (
-                      <option value={theme}>{theme}</option>
-                    ))}
-                  </select>
+                    minLength={3}
+                  />
                 </div>
               </div>
-              <form className="form-group">
-                <div class="col-12">
-                  <label>
-                    <T i18nKey="sort_type" class="mr-2">
-                      #
-                    </T>
-                  </label>
-                  <ListingTypeSelect
-                    type_={this.state.userSettingsForm.default_listing_type}
-                    onChange={this.handleUserSettingsListingTypeChange}
+              <div class="form-group row">
+                <label class="col-lg-5 col-form-label">
+                  <a href="https://about.riot.im/" target="_blank">
+                    {i18n.t('matrix_user_id')}
+                  </a>
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="text"
+                    class="form-control"
+                    placeholder="@user:example.com"
+                    value={this.state.userSettingsForm.matrix_user_id}
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsMatrixUserIdChange
+                    )}
+                    minLength={3}
                   />
                 </div>
-              </form>
-              <form className="form-group">
-                <div class="col-12">
-                  <label>
-                    <T i18nKey="type" class="mr-2">
-                      #
-                    </T>
-                  </label>
-                  <SortSelect
-                    sort={this.state.userSettingsForm.default_sort_type}
-                    onChange={this.handleUserSettingsSortTypeChange}
+              </div>
+              <div class="form-group row">
+                <label class="col-lg-5 col-form-label" htmlFor="user-password">
+                  {i18n.t('new_password')}
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="password"
+                    id="user-password"
+                    class="form-control"
+                    value={this.state.userSettingsForm.new_password}
+                    autoComplete="new-password"
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsNewPasswordChange
+                    )}
                   />
                 </div>
-              </form>
-              <div class="form-group">
-                <div class="col-12">
+              </div>
+              <div class="form-group row">
+                <label
+                  class="col-lg-5 col-form-label"
+                  htmlFor="user-verify-password"
+                >
+                  {i18n.t('verify_password')}
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="password"
+                    id="user-verify-password"
+                    class="form-control"
+                    value={this.state.userSettingsForm.new_password_verify}
+                    autoComplete="new-password"
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsNewPasswordVerifyChange
+                    )}
+                  />
+                </div>
+              </div>
+              <div class="form-group row">
+                <label
+                  class="col-lg-5 col-form-label"
+                  htmlFor="user-old-password"
+                >
+                  {i18n.t('old_password')}
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="password"
+                    id="user-old-password"
+                    class="form-control"
+                    value={this.state.userSettingsForm.old_password}
+                    autoComplete="new-password"
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsOldPasswordChange
+                    )}
+                  />
+                </div>
+              </div>
+              {WebSocketService.Instance.site.enable_nsfw && (
+                <div class="form-group">
                   <div class="form-check">
                     <input
                       class="form-check-input"
+                      id="user-show-nsfw"
                       type="checkbox"
                       checked={this.state.userSettingsForm.show_nsfw}
                       onChange={linkEvent(
@@ -459,80 +677,114 @@ export class User extends Component<any, UserState> {
                         this.handleUserSettingsShowNsfwChange
                       )}
                     />
-                    <label class="form-check-label">
-                      <T i18nKey="show_nsfw">#</T>
+                    <label class="form-check-label" htmlFor="user-show-nsfw">
+                      {i18n.t('show_nsfw')}
                     </label>
                   </div>
                 </div>
-              </div>
+              )}
               <div class="form-group">
-                <div class="col-12">
-                  <button
-                    type="submit"
-                    class="btn btn-block btn-secondary mr-4"
-                  >
-                    {this.state.userSettingsLoading ? (
-                      <svg class="icon icon-spinner spin">
-                        <use xlinkHref="#icon-spinner"></use>
-                      </svg>
-                    ) : (
-                      capitalizeFirstLetter(i18n.t('save'))
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="user-show-avatars"
+                    type="checkbox"
+                    checked={this.state.userSettingsForm.show_avatars}
+                    onChange={linkEvent(
+                      this,
+                      this.handleUserSettingsShowAvatarsChange
                     )}
-                  </button>
+                  />
+                  <label class="form-check-label" htmlFor="user-show-avatars">
+                    {i18n.t('show_avatars')}
+                  </label>
                 </div>
               </div>
-              <hr />
-              <div class="form-group mb-0">
-                <div class="col-12">
-                  <button
-                    class="btn btn-block btn-danger"
-                    onClick={linkEvent(
+              <div class="form-group">
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="user-send-notifications-to-email"
+                    type="checkbox"
+                    disabled={!this.state.user.email}
+                    checked={
+                      this.state.userSettingsForm.send_notifications_to_email
+                    }
+                    onChange={linkEvent(
                       this,
-                      this.handleDeleteAccountShowConfirmToggle
+                      this.handleUserSettingsSendNotificationsToEmailChange
                     )}
+                  />
+                  <label
+                    class="form-check-label"
+                    htmlFor="user-send-notifications-to-email"
                   >
-                    <T i18nKey="delete_account">#</T>
-                  </button>
-                  {this.state.deleteAccountShowConfirm && (
-                    <>
-                      <div class="my-2 alert alert-danger" role="alert">
-                        <T i18nKey="delete_account_confirm">#</T>
-                      </div>
-                      <input
-                        type="password"
-                        value={this.state.deleteAccountForm.password}
-                        onInput={linkEvent(
-                          this,
-                          this.handleDeleteAccountPasswordChange
-                        )}
-                        class="form-control my-2"
-                      />
-                      <button
-                        class="btn btn-danger mr-4"
-                        disabled={!this.state.deleteAccountForm.password}
-                        onClick={linkEvent(this, this.handleDeleteAccount)}
-                      >
-                        {this.state.deleteAccountLoading ? (
-                          <svg class="icon icon-spinner spin">
-                            <use xlinkHref="#icon-spinner"></use>
-                          </svg>
-                        ) : (
-                          capitalizeFirstLetter(i18n.t('delete'))
-                        )}
-                      </button>
-                      <button
-                        class="btn btn-secondary"
-                        onClick={linkEvent(
-                          this,
-                          this.handleDeleteAccountShowConfirmToggle
-                        )}
-                      >
-                        <T i18nKey="cancel">#</T>
-                      </button>
-                    </>
-                  )}
+                    {i18n.t('send_notifications_to_email')}
+                  </label>
                 </div>
               </div>
+              <div class="form-group">
+                <button type="submit" class="btn btn-block btn-secondary mr-4">
+                  {this.state.userSettingsLoading ? (
+                    <svg class="icon icon-spinner spin">
+                      <use xlinkHref="#icon-spinner"></use>
+                    </svg>
+                  ) : (
+                    capitalizeFirstLetter(i18n.t('save'))
+                  )}
+                </button>
+              </div>
+              <hr />
+              <div class="form-group mb-0">
+                <button
+                  class="btn btn-block btn-danger"
+                  onClick={linkEvent(
+                    this,
+                    this.handleDeleteAccountShowConfirmToggle
+                  )}
+                >
+                  {i18n.t('delete_account')}
+                </button>
+                {this.state.deleteAccountShowConfirm && (
+                  <>
+                    <div class="my-2 alert alert-danger" role="alert">
+                      {i18n.t('delete_account_confirm')}
+                    </div>
+                    <input
+                      type="password"
+                      value={this.state.deleteAccountForm.password}
+                      autoComplete="new-password"
+                      onInput={linkEvent(
+                        this,
+                        this.handleDeleteAccountPasswordChange
+                      )}
+                      class="form-control my-2"
+                    />
+                    <button
+                      class="btn btn-danger mr-4"
+                      disabled={!this.state.deleteAccountForm.password}
+                      onClick={linkEvent(this, this.handleDeleteAccount)}
+                    >
+                      {this.state.deleteAccountLoading ? (
+                        <svg class="icon icon-spinner spin">
+                          <use xlinkHref="#icon-spinner"></use>
+                        </svg>
+                      ) : (
+                        capitalizeFirstLetter(i18n.t('delete'))
+                      )}
+                    </button>
+                    <button
+                      class="btn btn-secondary"
+                      onClick={linkEvent(
+                        this,
+                        this.handleDeleteAccountShowConfirmToggle
+                      )}
+                    >
+                      {i18n.t('cancel')}
+                    </button>
+                  </>
+                )}
+              </div>
             </form>
           </div>
         </div>
@@ -546,9 +798,7 @@ export class User extends Component<any, UserState> {
         {this.state.moderates.length > 0 && (
           <div class="card border-secondary mb-3">
             <div class="card-body">
-              <h5>
-                <T i18nKey="moderates">#</T>
-              </h5>
+              <h5>{i18n.t('moderates')}</h5>
               <ul class="list-unstyled mb-0">
                 {this.state.moderates.map(community => (
                   <li>
@@ -571,9 +821,7 @@ export class User extends Component<any, UserState> {
         {this.state.follows.length > 0 && (
           <div class="card border-secondary mb-3">
             <div class="card-body">
-              <h5>
-                <T i18nKey="subscribed">#</T>
-              </h5>
+              <h5>{i18n.t('subscribed')}</h5>
               <ul class="list-unstyled mb-0">
                 {this.state.follows.map(community => (
                   <li>
@@ -598,14 +846,14 @@ export class User extends Component<any, UserState> {
             class="btn btn-sm btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
-            <T i18nKey="prev">#</T>
+            {i18n.t('prev')}
           </button>
         )}
         <button
           class="btn btn-sm btn-secondary"
           onClick={linkEvent(this, this.nextPage)}
         >
-          <T i18nKey="next">#</T>
+          {i18n.t('next')}
         </button>
       </div>
     );
@@ -666,9 +914,26 @@ export class User extends Component<any, UserState> {
     i.setState(i.state);
   }
 
+  handleUserSettingsShowAvatarsChange(i: User, event: any) {
+    i.state.userSettingsForm.show_avatars = event.target.checked;
+    UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
+    i.setState(i.state);
+  }
+
+  handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
+    i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleUserSettingsThemeChange(i: User, event: any) {
     i.state.userSettingsForm.theme = event.target.value;
-    setTheme(event.target.value);
+    setTheme(event.target.value, true);
+    i.setState(i.state);
+  }
+
+  handleUserSettingsLangChange(i: User, event: any) {
+    i.state.userSettingsForm.lang = event.target.value;
+    i18n.changeLanguage(i.state.userSettingsForm.lang);
     i.setState(i.state);
   }
 
@@ -682,6 +947,86 @@ export class User extends Component<any, UserState> {
     this.setState(this.state);
   }
 
+  handleUserSettingsEmailChange(i: User, event: any) {
+    i.state.userSettingsForm.email = event.target.value;
+    if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
+      i.state.userSettingsForm.email = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleUserSettingsMatrixUserIdChange(i: User, event: any) {
+    i.state.userSettingsForm.matrix_user_id = event.target.value;
+    if (
+      i.state.userSettingsForm.matrix_user_id == '' &&
+      !i.state.user.matrix_user_id
+    ) {
+      i.state.userSettingsForm.matrix_user_id = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleUserSettingsNewPasswordChange(i: User, event: any) {
+    i.state.userSettingsForm.new_password = event.target.value;
+    if (i.state.userSettingsForm.new_password == '') {
+      i.state.userSettingsForm.new_password = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
+    i.state.userSettingsForm.new_password_verify = event.target.value;
+    if (i.state.userSettingsForm.new_password_verify == '') {
+      i.state.userSettingsForm.new_password_verify = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleUserSettingsOldPasswordChange(i: User, event: any) {
+    i.state.userSettingsForm.old_password = event.target.value;
+    if (i.state.userSettingsForm.old_password == '') {
+      i.state.userSettingsForm.old_password = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleImageUpload(i: User, event: any) {
+    event.preventDefault();
+    let file = event.target.files[0];
+    const imageUploadUrl = `/pictrs/image`;
+    const formData = new FormData();
+    formData.append('images[]', file);
+
+    i.state.avatarLoading = true;
+    i.setState(i.state);
+
+    fetch(imageUploadUrl, {
+      method: 'POST',
+      body: formData,
+    })
+      .then(res => res.json())
+      .then(res => {
+        console.log('pictrs upload:');
+        console.log(res);
+        if (res.msg == 'ok') {
+          let hash = res.files[0].file;
+          let url = `${window.location.origin}/pictrs/image/${hash}`;
+          i.state.userSettingsForm.avatar = url;
+          i.state.avatarLoading = false;
+          i.setState(i.state);
+        } else {
+          i.state.avatarLoading = false;
+          i.setState(i.state);
+          toast(JSON.stringify(res), 'danger');
+        }
+      })
+      .catch(error => {
+        i.state.avatarLoading = false;
+        i.setState(i.state);
+        toast(error, 'danger');
+      });
+  }
+
   handleUserSettingsSubmit(i: User, event: any) {
     event.preventDefault();
     i.state.userSettingsLoading = true;
@@ -714,22 +1059,29 @@ export class User extends Component<any, UserState> {
     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
+    let res = wsJsonToRes(msg);
     if (msg.error) {
-      alert(i18n.t(msg.error));
+      toast(i18n.t(msg.error), 'danger');
       this.state.deleteAccountLoading = false;
+      this.state.avatarLoading = false;
+      this.state.userSettingsLoading = false;
+      if (msg.error == 'couldnt_find_that_username_or_email') {
+        this.context.router.history.push('/');
+      }
       this.setState(this.state);
       return;
-    } else if (op == UserOperation.GetUserDetails) {
-      let res: UserDetailsResponse = msg;
-      this.state.user = res.user;
-      this.state.comments = res.comments;
-      this.state.follows = res.follows;
-      this.state.moderates = res.moderates;
-      this.state.posts = res.posts;
-      this.state.admins = res.admins;
+    } else if (msg.reconnect) {
+      this.refetch();
+    } else if (res.op == UserOperation.GetUserDetails) {
+      let data = res.data as UserDetailsResponse;
+      this.state.user = data.user;
+      this.state.comments = data.comments;
+      this.state.follows = data.follows;
+      this.state.moderates = data.moderates;
+      this.state.posts = data.posts;
+      this.state.admins = data.admins;
       this.state.loading = false;
       if (this.isCurrentUser) {
         this.state.userSettingsForm.show_nsfw =
@@ -741,63 +1093,62 @@ export class User extends Component<any, UserState> {
           UserService.Instance.user.default_sort_type;
         this.state.userSettingsForm.default_listing_type =
           UserService.Instance.user.default_listing_type;
+        this.state.userSettingsForm.lang = UserService.Instance.user.lang;
+        this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+        this.state.userSettingsForm.email = this.state.user.email;
+        this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
+        this.state.userSettingsForm.show_avatars =
+          UserService.Instance.user.show_avatars;
+        this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
       }
       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;
-
-      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;
-
+      setupTippy();
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
+      editCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateComment) {
+      let data = res.data as CommentResponse;
+      if (
+        UserService.Instance.user &&
+        data.comment.creator_id == UserService.Instance.user.id
+      ) {
+        toast(i18n.t('reply_sent'));
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.comments);
       this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
-      // let res: CommentResponse = msg;
-      alert(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.CreateCommentLike) {
+      let data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.comments);
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let res: CommentResponse = msg;
-      let found: Comment = this.state.comments.find(
-        c => c.id === res.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;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as PostResponse;
+      createPostLikeFindRes(data, this.state.posts);
       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);
-      let res: LoginResponse = msg;
-      UserService.Instance.login(res);
-    } else if (op == UserOperation.DeleteAccount) {
+      UserService.Instance.login(data);
+    } else if (res.op == UserOperation.DeleteAccount) {
       this.state.deleteAccountLoading = false;
       this.state.deleteAccountShowConfirm = false;
       this.setState(this.state);