]> Untitled Git - lemmy.git/blobdiff - ui/src/components/user.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / user.tsx
index 945206c1de67202c3166516cd4541fd01d83f44d..d7b00ec55bdde91d6638d17cc01a9f082b0da8e2 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -13,10 +14,10 @@ import {
   DeleteAccountForm,
   WebSocketJsonResponse,
   GetSiteResponse,
-  Site,
-  UserDetailsView,
   UserDetailsResponse,
-} from '../interfaces';
+  AddAdminResponse,
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
   wsJsonToRes,
@@ -26,9 +27,12 @@ import {
   themes,
   setTheme,
   languages,
-  showAvatars,
   toast,
   setupTippy,
+  getLanguage,
+  mdToHtml,
+  elementUrl,
+  favIconUrl,
 } from '../utils';
 import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
@@ -37,6 +41,9 @@ import { MomentTime } from './moment-time';
 import { i18n } from '../i18next';
 import moment from 'moment';
 import { UserDetails } from './user-details';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { BannerIconHeader } from './banner-icon-header';
 
 interface UserState {
   user: UserView;
@@ -48,13 +55,12 @@ interface UserState {
   sort: SortType;
   page: number;
   loading: boolean;
-  avatarLoading: boolean;
   userSettingsForm: UserSettingsForm;
   userSettingsLoading: boolean;
   deleteAccountLoading: boolean;
   deleteAccountShowConfirm: boolean;
   deleteAccountForm: DeleteAccountForm;
-  site: Site;
+  siteRes: GetSiteResponse;
 }
 
 interface UserProps {
@@ -67,7 +73,7 @@ interface UserProps {
 
 interface UrlParams {
   view?: string;
-  sort?: string;
+  sort?: SortType;
   page?: number;
 }
 
@@ -84,8 +90,6 @@ export class User extends Component<any, UserState> {
       comment_score: null,
       banned: null,
       avatar: null,
-      show_avatars: null,
-      send_notifications_to_email: null,
       actor_id: null,
       local: null,
     },
@@ -93,8 +97,7 @@ export class User extends Component<any, UserState> {
     username: null,
     follows: [],
     moderates: [],
-    loading: false,
-    avatarLoading: false,
+    loading: true,
     view: User.getViewFromProps(this.props.match.view),
     sort: User.getSortTypeFromProps(this.props.match.sort),
     page: User.getPageFromProps(this.props.match.page),
@@ -107,6 +110,8 @@ export class User extends Component<any, UserState> {
       show_avatars: null,
       send_notifications_to_email: null,
       auth: null,
+      bio: null,
+      preferred_username: null,
     },
     userSettingsLoading: null,
     deleteAccountLoading: null,
@@ -114,19 +119,30 @@ export class User extends Component<any, UserState> {
     deleteAccountForm: {
       password: null,
     },
-    site: {
-      id: undefined,
-      name: undefined,
-      creator_id: undefined,
-      published: undefined,
-      creator_name: undefined,
-      number_of_users: undefined,
-      number_of_posts: undefined,
-      number_of_comments: undefined,
-      number_of_communities: undefined,
-      enable_downvotes: undefined,
-      open_registration: undefined,
-      enable_nsfw: undefined,
+    siteRes: {
+      admins: [],
+      banned: [],
+      online: undefined,
+      site: {
+        id: undefined,
+        name: undefined,
+        creator_id: undefined,
+        published: undefined,
+        creator_name: undefined,
+        number_of_users: undefined,
+        number_of_posts: undefined,
+        number_of_comments: undefined,
+        number_of_communities: undefined,
+        enable_downvotes: undefined,
+        open_registration: undefined,
+        enable_nsfw: undefined,
+        icon: undefined,
+        banner: undefined,
+        creator_preferred_username: undefined,
+      },
+      version: undefined,
+      my_user: undefined,
+      federated_instances: undefined,
     },
   };
 
@@ -142,6 +158,15 @@ export class User extends Component<any, UserState> {
       this
     );
     this.handlePageChange = this.handlePageChange.bind(this);
+    this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
+      this
+    );
+
+    this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+    this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+    this.handleBannerUpload = this.handleBannerUpload.bind(this);
+    this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
     this.state.user_id = Number(this.props.match.params.id) || null;
     this.state.username = this.props.match.params.username;
@@ -155,6 +180,7 @@ export class User extends Component<any, UserState> {
       );
 
     WebSocketService.Instance.getSite();
+    setupTippy();
   }
 
   get isCurrentUser() {
@@ -164,17 +190,15 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  static getViewFromProps(view: any): UserDetailsView {
-    return view
-      ? UserDetailsView[capitalizeFirstLetter(view)]
-      : UserDetailsView.Overview;
+  static getViewFromProps(view: string): UserDetailsView {
+    return view ? UserDetailsView[view] : UserDetailsView.Overview;
   }
 
-  static getSortTypeFromProps(sort: any): SortType {
+  static getSortTypeFromProps(sort: string): SortType {
     return sort ? routeSortTypeToEnum(sort) : SortType.New;
   }
 
-  static getPageFromProps(page: any): number {
+  static getPageFromProps(page: number): number {
     return page ? Number(page) : 1;
   }
 
@@ -201,63 +225,79 @@ export class User extends Component<any, UserState> {
       // Couldnt get a refresh working. This does for now.
       location.reload();
     }
-    document.title = `/u/${this.state.username} - ${this.state.site.name}`;
-    setupTippy();
+  }
+
+  get documentTitle(): string {
+    if (this.state.siteRes.site.name) {
+      return `@${this.state.username} - ${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.siteRes.site.icon
+      ? this.state.siteRes.site.icon
+      : favIconUrl;
   }
 
   render() {
     return (
       <div class="container">
-        {this.state.loading ? (
-          <h5>
-            <svg class="icon icon-spinner spin">
-              <use xlinkHref="#icon-spinner"></use>
-            </svg>
-          </h5>
-        ) : (
-          <div class="row">
-            <div class="col-12 col-md-8">
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
+        <div class="row">
+          <div class="col-12 col-md-8">
+            {this.state.loading ? (
               <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.username}</span>
+                <svg class="icon icon-spinner spin">
+                  <use xlinkHref="#icon-spinner"></use>
+                </svg>
               </h5>
-              {this.selects()}
-              <UserDetails
-                user_id={this.state.user_id}
-                username={this.state.username}
-                sort={SortType[this.state.sort]}
-                page={this.state.page}
-                limit={fetchLimit}
-                enableDownvotes={this.state.site.enable_downvotes}
-                enableNsfw={this.state.site.enable_nsfw}
-                view={this.state.view}
-                onPageChange={this.handlePageChange}
-              />
-            </div>
+            ) : (
+              <>
+                {this.userInfo()}
+                <hr />
+              </>
+            )}
+            {!this.state.loading && this.selects()}
+            <UserDetails
+              user_id={this.state.user_id}
+              username={this.state.username}
+              sort={this.state.sort}
+              page={this.state.page}
+              limit={fetchLimit}
+              enableDownvotes={this.state.siteRes.site.enable_downvotes}
+              enableNsfw={this.state.siteRes.site.enable_nsfw}
+              admins={this.state.siteRes.admins}
+              view={this.state.view}
+              onPageChange={this.handlePageChange}
+            />
+          </div>
+
+          {!this.state.loading && (
             <div class="col-12 col-md-4">
-              {this.userInfo()}
               {this.isCurrentUser && this.userSettings()}
               {this.moderates()}
               {this.follows()}
             </div>
-          </div>
-        )}
+          )}
+        </div>
       </div>
     );
   }
 
   viewRadios() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Overview && 'active'}
           `}
         >
@@ -270,7 +310,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('overview')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Comments && 'active'}
           `}
         >
@@ -283,7 +323,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('comments')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Posts && 'active'}
           `}
         >
@@ -296,7 +336,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('posts')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Saved && 'active'}
           `}
         >
@@ -322,9 +362,7 @@ export class User extends Component<any, UserState> {
           hideHot
         />
         <a
-          href={`/feeds/u/${this.state.username}.xml?sort=${
-            SortType[this.state.sort]
-          }`}
+          href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
           target="_blank"
           rel="noopener"
           title="RSS"
@@ -339,23 +377,90 @@ export class User extends Component<any, UserState> {
 
   userInfo() {
     let user = this.state.user;
+
     return (
       <div>
-        <div class="card border-secondary mb-3">
-          <div class="card-body">
-            <h5>
-              <ul class="list-inline mb-0">
-                <li className="list-inline-item">
-                  <UserListing user={user} realLink />
-                </li>
-                {user.banned && (
-                  <li className="list-inline-item badge badge-danger">
-                    {i18n.t('banned')}
-                  </li>
+        <BannerIconHeader
+          banner={this.state.user.banner}
+          icon={this.state.user.avatar}
+        />
+        <div class="mb-3">
+          <div class="">
+            <div class="mb-0 d-flex flex-wrap">
+              <div>
+                {user.preferred_username && (
+                  <h5 class="mb-0">{user.preferred_username}</h5>
                 )}
+                <ul class="list-inline mb-2">
+                  <li className="list-inline-item">
+                    <UserListing
+                      user={user}
+                      realLink
+                      useApubName
+                      muted
+                      hideAvatar
+                    />
+                  </li>
+                  {user.banned && (
+                    <li className="list-inline-item badge badge-danger">
+                      {i18n.t('banned')}
+                    </li>
+                  )}
+                </ul>
+              </div>
+              <div className="flex-grow-1 unselectable pointer mx-2"></div>
+              {this.isCurrentUser ? (
+                <button
+                  class="d-flex align-self-start btn btn-secondary ml-2"
+                  onClick={linkEvent(this, this.handleLogoutClick)}
+                >
+                  {i18n.t('logout')}
+                </button>
+              ) : (
+                <>
+                  <a
+                    className={`d-flex align-self-start btn btn-secondary ml-2 ${
+                      !this.state.user.matrix_user_id && 'invisible'
+                    }`}
+                    target="_blank"
+                    rel="noopener"
+                    href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+                  >
+                    {i18n.t('send_secure_message')}
+                  </a>
+                  <Link
+                    class="d-flex align-self-start btn btn-secondary ml-2"
+                    to={`/create_private_message?recipient_id=${this.state.user.id}`}
+                  >
+                    {i18n.t('send_message')}
+                  </Link>
+                </>
+              )}
+            </div>
+            {user.bio && (
+              <div className="d-flex align-items-center mb-2">
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(user.bio)}
+                />
+              </div>
+            )}
+            <div>
+              <ul class="list-inline mb-2">
+                <li className="list-inline-item badge badge-light">
+                  {i18n.t('number_of_posts', { count: user.number_of_posts })}
+                </li>
+                <li className="list-inline-item badge badge-light">
+                  {i18n.t('number_of_comments', {
+                    count: user.number_of_comments,
+                  })}
+                </li>
               </ul>
-            </h5>
-            <div className="d-flex align-items-center mb-2">
+            </div>
+            <div class="text-muted">
+              {i18n.t('joined')} <MomentTime data={user} showAgo />
+            </div>
+            <div className="d-flex align-items-center text-muted mb-2">
               <svg class="icon">
                 <use xlinkHref="#icon-cake"></use>
               </svg>
@@ -364,71 +469,6 @@ export class User extends Component<any, UserState> {
                 {moment.utc(user.published).local().format('MMM DD, YYYY')}
               </span>
             </div>
-            <div>
-              {i18n.t('joined')} <MomentTime data={user} showAgo />
-            </div>
-            <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>
-                    {i18n.t('number_of_points', { count: user.post_score })}
-                  </td>
-                  */}
-                  <td>
-                    {i18n.t('number_of_posts', { count: user.number_of_posts })}
-                  </td>
-                  {/* 
-                </tr>
-                <tr>
-                  <td>
-                    {i18n.t('number_of_points', { count: user.comment_score })}
-                  </td>
-                  */}
-                  <td>
-                    {i18n.t('number_of_comments', {
-                      count: user.number_of_comments,
-                    })}
-                  </td>
-                </tr>
-              </table>
-            </div>
-            {this.isCurrentUser ? (
-              <button
-                class="btn btn-block btn-secondary mt-3"
-                onClick={linkEvent(this, this.handleLogoutClick)}
-              >
-                {i18n.t('logout')}
-              </button>
-            ) : (
-              <>
-                <a
-                  className={`btn btn-block btn-secondary mt-3 ${
-                    !this.state.user.matrix_user_id && 'disabled'
-                  }`}
-                  target="_blank"
-                  rel="noopener"
-                  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>
       </div>
@@ -438,59 +478,35 @@ export class User extends Component<any, UserState> {
   userSettings() {
     return (
       <div>
-        <div class="card border-secondary mb-3">
+        <div class="card bg-transparent border-secondary mb-3">
           <div class="card-body">
             <h5>{i18n.t('settings')}</h5>
             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
               <div class="form-group">
                 <label>{i18n.t('avatar')}</label>
-                <form class="d-inline">
-                  <label
-                    htmlFor="file-upload"
-                    class="pointer ml-4 text-muted small font-weight-bold"
-                  >
-                    {!this.checkSettingsAvatar ? (
-                      <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>
-                  <input
-                    id="file-upload"
-                    type="file"
-                    accept="image/*,video/*"
-                    name="file"
-                    class="d-none"
-                    disabled={!UserService.Instance.user}
-                    onChange={linkEvent(this, this.handleImageUpload)}
-                  />
-                </form>
+                <ImageUploadForm
+                  uploadTitle={i18n.t('upload_avatar')}
+                  imageSrc={this.state.userSettingsForm.avatar}
+                  onUpload={this.handleAvatarUpload}
+                  onRemove={this.handleAvatarRemove}
+                  rounded
+                />
+              </div>
+              <div class="form-group">
+                <label>{i18n.t('banner')}</label>
+                <ImageUploadForm
+                  uploadTitle={i18n.t('upload_banner')}
+                  imageSrc={this.state.userSettingsForm.banner}
+                  onUpload={this.handleBannerUpload}
+                  onRemove={this.handleBannerRemove}
+                />
               </div>
-              {this.checkSettingsAvatar && (
-                <div class="form-group">
-                  <button
-                    class="btn btn-secondary btn-block"
-                    onClick={linkEvent(this, this.removeAvatar)}
-                  >
-                    {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
-                      'avatar'
-                    )}`}
-                  </button>
-                </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"
+                  class="ml-2 custom-select w-auto"
                 >
                   <option disabled>{i18n.t('language')}</option>
                   <option value="browser">{i18n.t('browser_default')}</option>
@@ -505,7 +521,7 @@ export class User extends Component<any, UserState> {
                 <select
                   value={this.state.userSettingsForm.theme}
                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
-                  class="ml-2 custom-select custom-select-sm w-auto"
+                  class="ml-2 custom-select w-auto"
                 >
                   <option disabled>{i18n.t('theme')}</option>
                   {themes.map(theme => (
@@ -518,7 +534,15 @@ export class User extends Component<any, UserState> {
                   <div class="mr-2">{i18n.t('sort_type')}</div>
                 </label>
                 <ListingTypeSelect
-                  type_={this.state.userSettingsForm.default_listing_type}
+                  type_={
+                    Object.values(ListingType)[
+                      this.state.userSettingsForm.default_listing_type
+                    ]
+                  }
+                  showLocal={
+                    this.state.siteRes.federated_instances &&
+                    this.state.siteRes.federated_instances.length > 0
+                  }
                   onChange={this.handleUserSettingsListingTypeChange}
                 />
               </form>
@@ -527,10 +551,47 @@ export class User extends Component<any, UserState> {
                   <div class="mr-2">{i18n.t('type')}</div>
                 </label>
                 <SortSelect
-                  sort={this.state.userSettingsForm.default_sort_type}
+                  sort={
+                    Object.values(SortType)[
+                      this.state.userSettingsForm.default_sort_type
+                    ]
+                  }
                   onChange={this.handleUserSettingsSortTypeChange}
                 />
               </form>
+              <div class="form-group row">
+                <label class="col-lg-5 col-form-label">
+                  {i18n.t('display_name')}
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="text"
+                    class="form-control"
+                    placeholder={i18n.t('optional')}
+                    value={this.state.userSettingsForm.preferred_username}
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsPreferredUsernameChange
+                    )}
+                    pattern="^(?!@)(.+)$"
+                    minLength={3}
+                    maxLength={20}
+                  />
+                </div>
+              </div>
+              <div class="form-group row">
+                <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+                  {i18n.t('bio')}
+                </label>
+                <div class="col-lg-9">
+                  <MarkdownTextArea
+                    initialContent={this.state.userSettingsForm.bio}
+                    onContentChange={this.handleUserSettingsBioChange}
+                    maxLength={300}
+                    hideNavigationWarnings
+                  />
+                </div>
+              </div>
               <div class="form-group row">
                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
                   {i18n.t('email')}
@@ -552,11 +613,7 @@ export class User extends Component<any, UserState> {
               </div>
               <div class="form-group row">
                 <label class="col-lg-5 col-form-label">
-                  <a
-                    href="https://about.riot.im/"
-                    target="_blank"
-                    rel="noopener"
-                  >
+                  <a href={elementUrl} target="_blank" rel="noopener">
                     {i18n.t('matrix_user_id')}
                   </a>
                 </label>
@@ -634,7 +691,7 @@ export class User extends Component<any, UserState> {
                   />
                 </div>
               </div>
-              {this.state.site.enable_nsfw && (
+              {this.state.siteRes.site.enable_nsfw && (
                 <div class="form-group">
                   <div class="form-check">
                     <input
@@ -676,7 +733,7 @@ export class User extends Component<any, UserState> {
                     class="form-check-input"
                     id="user-send-notifications-to-email"
                     type="checkbox"
-                    disabled={!this.state.user.email}
+                    disabled={!this.state.userSettingsForm.email}
                     checked={
                       this.state.userSettingsForm.send_notifications_to_email
                     }
@@ -766,7 +823,7 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.moderates.length > 0 && (
-          <div class="card border-secondary mb-3">
+          <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('moderates')}</h5>
               <ul class="list-unstyled mb-0">
@@ -789,7 +846,7 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.follows.length > 0 && (
-          <div class="card border-secondary mb-3">
+          <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('subscribed')}</h5>
               <ul class="list-unstyled mb-0">
@@ -810,10 +867,8 @@ export class User extends Component<any, UserState> {
 
   updateUrl(paramUpdates: UrlParams) {
     const page = paramUpdates.page || this.state.page;
-    const viewStr =
-      paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
-    const sortStr =
-      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
+    const sortStr = paramUpdates.sort || this.state.sort;
     this.props.history.push(
       `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
     );
@@ -824,12 +879,12 @@ export class User extends Component<any, UserState> {
   }
 
   handleSortChange(val: SortType) {
-    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ sort: val, page: 1 });
   }
 
   handleViewChange(i: User, event: any) {
     i.updateUrl({
-      view: UserDetailsView[Number(event.target.value)].toLowerCase(),
+      view: UserDetailsView[Number(event.target.value)],
       page: 1,
     });
   }
@@ -858,25 +913,56 @@ export class User extends Component<any, UserState> {
 
   handleUserSettingsLangChange(i: User, event: any) {
     i.state.userSettingsForm.lang = event.target.value;
-    i18n.changeLanguage(i.state.userSettingsForm.lang);
+    i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
     i.setState(i.state);
   }
 
   handleUserSettingsSortTypeChange(val: SortType) {
-    this.state.userSettingsForm.default_sort_type = val;
+    this.state.userSettingsForm.default_sort_type = Object.keys(
+      SortType
+    ).indexOf(val);
     this.setState(this.state);
   }
 
   handleUserSettingsListingTypeChange(val: ListingType) {
-    this.state.userSettingsForm.default_listing_type = val;
+    this.state.userSettingsForm.default_listing_type = Object.keys(
+      ListingType
+    ).indexOf(val);
     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);
+  }
+
+  handleUserSettingsBioChange(val: string) {
+    this.state.userSettingsForm.bio = val;
+    this.setState(this.state);
+  }
+
+  handleAvatarUpload(url: string) {
+    this.state.userSettingsForm.avatar = url;
+    this.setState(this.state);
+  }
+
+  handleAvatarRemove() {
+    this.state.userSettingsForm.avatar = '';
+    this.setState(this.state);
+  }
+
+  handleBannerUpload(url: string) {
+    this.state.userSettingsForm.banner = url;
+    this.setState(this.state);
+  }
+
+  handleBannerRemove() {
+    this.state.userSettingsForm.banner = '';
+    this.setState(this.state);
+  }
+
+  handleUserSettingsPreferredUsernameChange(i: User, event: any) {
+    i.state.userSettingsForm.preferred_username = event.target.value;
     i.setState(i.state);
   }
 
@@ -915,59 +1001,6 @@ export class User extends Component<any, UserState> {
     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');
-      });
-  }
-
-  removeAvatar(i: User, event: any) {
-    event.preventDefault();
-    i.state.userSettingsLoading = true;
-    i.state.userSettingsForm.avatar = '';
-    i.setState(i.state);
-
-    WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
-  }
-
-  get checkSettingsAvatar(): boolean {
-    return (
-      this.state.userSettingsForm.avatar &&
-      this.state.userSettingsForm.avatar != ''
-    );
-  }
-
   handleUserSettingsSubmit(i: User, event: any) {
     event.preventDefault();
     i.state.userSettingsLoading = true;
@@ -1001,6 +1034,7 @@ export class User extends Component<any, UserState> {
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
+    console.log(msg);
     const res = wsJsonToRes(msg);
     if (msg.error) {
       toast(i18n.t(msg.error), 'danger');
@@ -1009,7 +1043,6 @@ export class User extends Component<any, UserState> {
       }
       this.setState({
         deleteAccountLoading: false,
-        avatarLoading: false,
         userSettingsLoading: false,
       });
       return;
@@ -1035,20 +1068,31 @@ export class User extends Component<any, UserState> {
             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.banner = UserService.Instance.user.banner;
+          this.state.userSettingsForm.preferred_username =
+            UserService.Instance.user.preferred_username;
           this.state.userSettingsForm.show_avatars =
             UserService.Instance.user.show_avatars;
-          this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
+          this.state.userSettingsForm.email = UserService.Instance.user.email;
+          this.state.userSettingsForm.bio = UserService.Instance.user.bio;
+          this.state.userSettingsForm.send_notifications_to_email =
+            UserService.Instance.user.send_notifications_to_email;
+          this.state.userSettingsForm.matrix_user_id =
+            UserService.Instance.user.matrix_user_id;
         }
+        this.state.loading = false;
         this.setState(this.state);
       }
     } else if (res.op == UserOperation.SaveUserSettings) {
       const data = res.data as LoginResponse;
       UserService.Instance.login(data);
-      this.setState({
-        userSettingsLoading: false,
-      });
+      this.state.user.bio = this.state.userSettingsForm.bio;
+      this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
+      this.state.user.banner = this.state.userSettingsForm.banner;
+      this.state.user.avatar = this.state.userSettingsForm.avatar;
+      this.state.userSettingsLoading = false;
+      this.setState(this.state);
+
       window.scrollTo(0, 0);
     } else if (res.op == UserOperation.DeleteAccount) {
       this.setState({
@@ -1058,9 +1102,12 @@ export class User extends Component<any, UserState> {
       this.context.router.history.push('/');
     } else if (res.op == UserOperation.GetSite) {
       const data = res.data as GetSiteResponse;
-      this.setState({
-        site: data.site,
-      });
+      this.state.siteRes = data;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.AddAdmin) {
+      const data = res.data as AddAdminResponse;
+      this.state.siteRes.admins = data.admins;
+      this.setState(this.state);
     }
   }
 }