]> 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 09129d67ca06365be01b92e362d624df48b344c1..f635a1cd0b1d8eb48582b62f9c06fdb7234a0bb7 100644 (file)
@@ -18,7 +18,7 @@ import {
   BanUserResponse,
   AddAdminResponse,
   DeleteAccountForm,
-  CreatePostLikeResponse,
+  PostResponse,
   WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
@@ -32,14 +32,20 @@ import {
   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,
@@ -76,7 +82,6 @@ export class User extends Component<any, UserState> {
     user: {
       id: null,
       name: null,
-      fedi_name: null,
       published: null,
       number_of_posts: null,
       post_score: null,
@@ -86,6 +91,8 @@ export class User extends Component<any, UserState> {
       avatar: null,
       show_avatars: null,
       send_notifications_to_email: null,
+      actor_id: null,
+      local: null,
     },
     user_id: null,
     username: null,
@@ -237,42 +244,80 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  selects() {
+  viewRadios() {
     return (
-      <div className="mb-2">
-        <select
-          value={this.state.view}
-          onChange={linkEvent(this, this.handleViewChange)}
-          class="custom-select custom-select-sm w-auto"
+      <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'}
+          `}
         >
-          <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
+          <input
+            type="radio"
+            value={View.Comments}
+            checked={this.state.view == View.Comments}
+            onChange={linkEvent(this, this.handleViewChange)}
           />
-        </span>
+          {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">
+        <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"
         >
           <svg class="icon mx-2 text-muted small">
             <use xlinkHref="#icon-rss">#</use>
@@ -316,6 +361,7 @@ export class User extends Component<any, UserState> {
                 nodes={[{ comment: i.data as Comment }]}
                 admins={this.state.admins}
                 noIndent
+                showContext
               />
             )}
           </div>
@@ -327,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>
     );
   }
@@ -356,53 +401,50 @@ 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>
@@ -412,13 +454,14 @@ export class User extends Component<any, UserState> {
                 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'}`}
+                  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}`}
                 >
@@ -443,14 +486,10 @@ 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">
-                <label>
-                  <T i18nKey="avatar">#</T>
-                </label>
+                <label>{i18n.t('avatar')}</label>
                 <form class="d-inline">
                   <label
                     htmlFor="file-upload"
@@ -458,7 +497,7 @@ export class User extends Component<any, UserState> {
                   >
                     {!this.state.userSettingsForm.avatar ? (
                       <span class="btn btn-sm btn-secondary">
-                        <T i18nKey="upload_avatar">#</T>
+                        {i18n.t('upload_avatar')}
                       </span>
                     ) : (
                       <img
@@ -481,20 +520,14 @@ export class User extends Component<any, UserState> {
                 </form>
               </div>
               <div class="form-group">
-                <label>
-                  <T i18nKey="language">#</T>
-                </label>
+                <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>
-                    <T i18nKey="language">#</T>
-                  </option>
-                  <option value="browser">
-                    <T i18nKey="browser_default">#</T>
-                  </option>
+                  <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>
@@ -502,17 +535,13 @@ export class User extends Component<any, UserState> {
                 </select>
               </div>
               <div class="form-group">
-                <label>
-                  <T i18nKey="theme">#</T>
-                </label>
+                <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>
-                    <T i18nKey="theme">#</T>
-                  </option>
+                  <option disabled>{i18n.t('theme')}</option>
                   {themes.map(theme => (
                     <option value={theme}>{theme}</option>
                   ))}
@@ -520,9 +549,7 @@ export class User extends Component<any, UserState> {
               </div>
               <form className="form-group">
                 <label>
-                  <T i18nKey="sort_type" class="mr-2">
-                    #
-                  </T>
+                  <div class="mr-2">{i18n.t('sort_type')}</div>
                 </label>
                 <ListingTypeSelect
                   type_={this.state.userSettingsForm.default_listing_type}
@@ -531,9 +558,7 @@ export class User extends Component<any, UserState> {
               </form>
               <form className="form-group">
                 <label>
-                  <T i18nKey="type" class="mr-2">
-                    #
-                  </T>
+                  <div class="mr-2">{i18n.t('type')}</div>
                 </label>
                 <SortSelect
                   sort={this.state.userSettingsForm.default_sort_type}
@@ -541,12 +566,13 @@ export class User extends Component<any, UserState> {
                 />
               </form>
               <div class="form-group row">
-                <label class="col-lg-3 col-form-label">
-                  <T i18nKey="email">#</T>
+                <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}
@@ -579,14 +605,16 @@ export class User extends Component<any, UserState> {
                 </div>
               </div>
               <div class="form-group row">
-                <label class="col-lg-5 col-form-label">
-                  <T i18nKey="new_password">#</T>
+                <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
@@ -595,14 +623,19 @@ export class User extends Component<any, UserState> {
                 </div>
               </div>
               <div class="form-group row">
-                <label class="col-lg-5 col-form-label">
-                  <T i18nKey="verify_password">#</T>
+                <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
@@ -611,14 +644,19 @@ export class User extends Component<any, UserState> {
                 </div>
               </div>
               <div class="form-group row">
-                <label class="col-lg-5 col-form-label">
-                  <T i18nKey="old_password">#</T>
+                <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
@@ -631,6 +669,7 @@ export class User extends Component<any, UserState> {
                   <div class="form-check">
                     <input
                       class="form-check-input"
+                      id="user-show-nsfw"
                       type="checkbox"
                       checked={this.state.userSettingsForm.show_nsfw}
                       onChange={linkEvent(
@@ -638,8 +677,8 @@ 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>
@@ -648,6 +687,7 @@ export class User extends Component<any, UserState> {
                 <div class="form-check">
                   <input
                     class="form-check-input"
+                    id="user-show-avatars"
                     type="checkbox"
                     checked={this.state.userSettingsForm.show_avatars}
                     onChange={linkEvent(
@@ -655,8 +695,8 @@ export class User extends Component<any, UserState> {
                       this.handleUserSettingsShowAvatarsChange
                     )}
                   />
-                  <label class="form-check-label">
-                    <T i18nKey="show_avatars">#</T>
+                  <label class="form-check-label" htmlFor="user-show-avatars">
+                    {i18n.t('show_avatars')}
                   </label>
                 </div>
               </div>
@@ -664,6 +704,7 @@ export class User extends Component<any, UserState> {
                 <div class="form-check">
                   <input
                     class="form-check-input"
+                    id="user-send-notifications-to-email"
                     type="checkbox"
                     disabled={!this.state.user.email}
                     checked={
@@ -674,8 +715,11 @@ export class User extends Component<any, UserState> {
                       this.handleUserSettingsSendNotificationsToEmailChange
                     )}
                   />
-                  <label class="form-check-label">
-                    <T i18nKey="send_notifications_to_email">#</T>
+                  <label
+                    class="form-check-label"
+                    htmlFor="user-send-notifications-to-email"
+                  >
+                    {i18n.t('send_notifications_to_email')}
                   </label>
                 </div>
               </div>
@@ -699,16 +743,17 @@ export class User extends Component<any, UserState> {
                     this.handleDeleteAccountShowConfirmToggle
                   )}
                 >
-                  <T i18nKey="delete_account">#</T>
+                  {i18n.t('delete_account')}
                 </button>
                 {this.state.deleteAccountShowConfirm && (
                   <>
                     <div class="my-2 alert alert-danger" role="alert">
-                      <T i18nKey="delete_account_confirm">#</T>
+                      {i18n.t('delete_account_confirm')}
                     </div>
                     <input
                       type="password"
                       value={this.state.deleteAccountForm.password}
+                      autoComplete="new-password"
                       onInput={linkEvent(
                         this,
                         this.handleDeleteAccountPasswordChange
@@ -735,7 +780,7 @@ export class User extends Component<any, UserState> {
                         this.handleDeleteAccountShowConfirmToggle
                       )}
                     >
-                      <T i18nKey="cancel">#</T>
+                      {i18n.t('cancel')}
                     </button>
                   </>
                 )}
@@ -753,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>
@@ -778,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>
@@ -805,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>
     );
@@ -886,7 +927,7 @@ export class User extends Component<any, UserState> {
 
   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);
   }
 
@@ -952,9 +993,9 @@ export class User extends Component<any, UserState> {
   handleImageUpload(i: User, event: any) {
     event.preventDefault();
     let file = event.target.files[0];
-    const imageUploadUrl = `/pictshare/api/upload.php`;
+    const imageUploadUrl = `/pictrs/image`;
     const formData = new FormData();
-    formData.append('file', file);
+    formData.append('images[]', file);
 
     i.state.avatarLoading = true;
     i.setState(i.state);
@@ -965,14 +1006,19 @@ export class User extends Component<any, UserState> {
     })
       .then(res => res.json())
       .then(res => {
-        let url = `${window.location.origin}/pictshare/${res.url}`;
-        if (res.filetype == 'mp4') {
-          url += '/raw';
+        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');
         }
-        i.state.userSettingsForm.avatar = url;
-        console.log(url);
-        i.state.avatarLoading = false;
-        i.setState(i.state);
       })
       .catch(error => {
         i.state.avatarLoading = false;
@@ -1016,16 +1062,18 @@ export class User extends Component<any, UserState> {
   parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
     let res = wsJsonToRes(msg);
-    if (res.error) {
+    if (msg.error) {
       toast(i18n.t(msg.error), 'danger');
       this.state.deleteAccountLoading = false;
       this.state.avatarLoading = false;
       this.state.userSettingsLoading = false;
-      if (res.error == 'couldnt_find_that_username_or_email') {
+      if (msg.error == 'couldnt_find_that_username_or_email') {
         this.context.router.history.push('/');
       }
       this.setState(this.state);
       return;
+    } else if (msg.reconnect) {
+      this.refetch();
     } else if (res.op == UserOperation.GetUserDetails) {
       let data = res.data as UserDetailsResponse;
       this.state.user = data.user;
@@ -1056,46 +1104,30 @@ export class User extends Component<any, UserState> {
       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
       window.scrollTo(0, 0);
       this.setState(this.state);
+      setupTippy();
     } 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;
-
+      editCommentRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateComment) {
-      // let res: CommentResponse = msg;
-      toast(i18n.t('reply_sent'));
-      // this.state.comments.unshift(res.comment); // TODO do this right
-      // this.setState(this.state);
+      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;
-      let found = this.state.comments.find(c => c.id == data.comment.id);
-      found.saved = data.comment.saved;
+      saveCommentRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      let found: Comment = this.state.comments.find(
-        c => c.id === data.comment.id
-      );
-      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;
+      createCommentLikeRes(data, this.state.comments);
       this.setState(this.state);
     } 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;
+      let data = res.data as PostResponse;
+      createPostLikeFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.BanUser) {
       let data = res.data as BanUserResponse;