]> Untitled Git - lemmy.git/commitdiff
Loading indicators.
authorDessalines <tyhou13@gmx.com>
Mon, 8 Apr 2019 21:46:09 +0000 (14:46 -0700)
committerDessalines <tyhou13@gmx.com>
Mon, 8 Apr 2019 21:46:09 +0000 (14:46 -0700)
- Fixes #10
- Adding SVG symbols component

ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/symbols.tsx [new file with mode: 0644]
ui/src/index.tsx
ui/src/main.css

index b81b11cd0b1ec3b9547326644873386d5a739ae4..cf42238e187ff56d50901962e5b6a0cc0ba14adc 100644 (file)
@@ -10,24 +10,26 @@ declare const Sortable: any;
 
 interface CommunitiesState {
   communities: Array<Community>;
+  loading: boolean;
 }
 
 export class Communities extends Component<any, CommunitiesState> {
   private subscription: Subscription;
   private emptyState: CommunitiesState = {
-    communities: []
+    communities: [],
+    loading: true
   }
 
   constructor(props: any, context: any) {
     super(props, context);
     this.state = this.emptyState;
     this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        (msg) => this.parseMessage(msg),
+    .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+    .subscribe(
+      (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log('complete')
-      );
+    );
     WebSocketService.Instance.listCommunities();
 
   }
@@ -44,40 +46,45 @@ export class Communities extends Component<any, CommunitiesState> {
   render() {
     return (
       <div class="container-fluid">
-        <h4>Communities</h4>
-        <div class="table-responsive">
-          <table id="community_table" class="table table-sm table-hover">
-            <thead class="pointer">
-              <tr>
-                <th>Name</th>
-                <th>Title</th>
-                <th>Category</th>
-                <th class="text-right">Subscribers</th>
-                <th class="text-right">Posts</th>
-                <th class="text-right">Comments</th>
-                <th></th>
-              </tr>
-            </thead>
-            <tbody>
-              {this.state.communities.map(community =>
+        {this.state.loading ? 
+        <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <div>
+          <h4>Communities</h4>
+          <div class="table-responsive">
+            <table id="community_table" class="table table-sm table-hover">
+              <thead class="pointer">
                 <tr>
-                  <td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
-                  <td>{community.title}</td>
-                  <td>{community.category_name}</td>
-                  <td class="text-right">{community.number_of_subscribers}</td>
-                  <td class="text-right">{community.number_of_posts}</td>
-                  <td class="text-right">{community.number_of_comments}</td>
-                  <td class="text-right">
-                    {community.subscribed 
-                      ? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
-                      : <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
-                    }
-                  </td>
+                  <th>Name</th>
+                  <th>Title</th>
+                  <th>Category</th>
+                  <th class="text-right">Subscribers</th>
+                  <th class="text-right">Posts</th>
+                  <th class="text-right">Comments</th>
+                  <th></th>
                 </tr>
-              )}
-            </tbody>
-          </table>
+              </thead>
+              <tbody>
+                {this.state.communities.map(community =>
+                  <tr>
+                    <td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
+                    <td>{community.title}</td>
+                    <td>{community.category_name}</td>
+                    <td class="text-right">{community.number_of_subscribers}</td>
+                    <td class="text-right">{community.number_of_posts}</td>
+                    <td class="text-right">{community.number_of_comments}</td>
+                    <td class="text-right">
+                      {community.subscribed ? 
+                      <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> : 
+                      <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
+                      }
+                    </td>
+                  </tr>
+                )}
+              </tbody>
+            </table>
+          </div>
         </div>
+        }
       </div>
     );
   }
@@ -109,6 +116,7 @@ export class Communities extends Component<any, CommunitiesState> {
       let res: ListCommunitiesResponse = msg;
       this.state.communities = res.communities;
       this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers);
+      this.state.loading = false;
       this.setState(this.state);
     } else if (op == UserOperation.FollowCommunity) {
       let res: CommunityResponse = msg;
index 6250ef9ca4509920b458f837c74a267274a420eb..b5b222c64ee8f572f2a964bcc4d8b1bf3e879c62 100644 (file)
@@ -4,6 +4,7 @@ import { retryWhen, delay, take } from 'rxjs/operators';
 import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
 import { WebSocketService } from '../services';
 import { msgOp } from '../utils';
+import * as autosize from 'autosize';
 
 import { Community } from '../interfaces';
 
@@ -17,6 +18,7 @@ interface CommunityFormProps {
 interface CommunityFormState {
   communityForm: CommunityFormI;
   categories: Array<Category>;
+  loading: boolean;
 }
 
 export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> {
@@ -28,7 +30,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
       title: null,
       category_id: null
     },
-    categories: []
+    categories: [],
+    loading: false
   }
 
   constructor(props: any, context: any) {
@@ -58,6 +61,10 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     WebSocketService.Instance.listCategories();
   }
 
+  componentDidMount() {
+    autosize(document.querySelectorAll('textarea'));
+  }
+
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
@@ -81,7 +88,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
         <div class="form-group row">
           <label class="col-12 col-form-label">Sidebar</label>
           <div class="col-12">
-            <textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={6} />
+            <textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} />
           </div>
         </div>
         <div class="form-group row">
@@ -96,8 +103,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
         </div>
         <div class="form-group row">
           <div class="col-12">
-            <button type="submit" class="btn btn-secondary mr-2">{this.props.community ? 'Save' : 'Create'}</button>
-            {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
+            <button type="submit" class="btn btn-secondary mr-2">
+              {this.state.loading ? 
+              <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 
+              this.props.community ? 'Save' : 'Create'}</button>
+              {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
           </div>
         </div>
       </form>
@@ -106,11 +116,13 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
 
   handleCreateCommunitySubmit(i: CommunityForm, event: any) {
     event.preventDefault();
+    i.state.loading = true;
     if (i.props.community) {
       WebSocketService.Instance.editCommunity(i.state.communityForm);
     } else {
       WebSocketService.Instance.createCommunity(i.state.communityForm);
     }
+    i.setState(i.state);
   }
 
   handleCommunityNameChange(i: CommunityForm, event: any) {
@@ -142,6 +154,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     console.log(msg);
     if (msg.error) {
       alert(msg.error);
+      this.state.loading = false;
       return;
     } else if (op == UserOperation.ListCategories){
       let res: ListCategoriesResponse = msg;
@@ -150,9 +163,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
       this.setState(this.state);
     } else if (op == UserOperation.CreateCommunity) {
       let res: CommunityResponse = msg;
+      this.state.loading = false;
       this.props.onCreate(res.community.id);
     } else if (op == UserOperation.EditCommunity) {
       let res: CommunityResponse = msg;
+      this.state.loading = false;
       this.props.onEdit(res.community);
     }
   }
index ed693c55c639cbac42cc912e0cb5d4e9e7fa5cf5..1ca9a8af72705fae98979f19e12b2d505ccaff07 100644 (file)
@@ -11,6 +11,7 @@ interface State {
   community: CommunityI;
   communityId: number;
   moderators: Array<CommunityUser>;
+  loading: boolean;
 }
 
 export class Community extends Component<any, State> {
@@ -31,7 +32,8 @@ export class Community extends Component<any, State> {
       published: null
     },
     moderators: [],
-    communityId: Number(this.props.match.params.id)
+    communityId: Number(this.props.match.params.id),
+    loading: true
   }
 
   constructor(props: any, context: any) {
@@ -40,12 +42,12 @@ export class Community extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        (msg) => this.parseMessage(msg),
+    .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+    .subscribe(
+      (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log('complete')
-      );
+    );
 
     WebSocketService.Instance.getCommunity(this.state.communityId);
   }
@@ -57,6 +59,8 @@ export class Community extends Component<any, State> {
   render() {
     return (
       <div class="container">
+        {this.state.loading ? 
+        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
         <div class="row">
           <div class="col-12 col-lg-9">
             <h4>/f/{this.state.community.name}</h4>
@@ -66,6 +70,7 @@ export class Community extends Component<any, State> {
             <Sidebar community={this.state.community} moderators={this.state.moderators} />
           </div>
         </div>
+        }
       </div>
     )
   }
@@ -81,6 +86,7 @@ export class Community extends Component<any, State> {
       let res: GetCommunityResponse = msg;
       this.state.community = res.community;
       this.state.moderators = res.moderators;
+      this.state.loading = false;
       this.setState(this.state);
     } else if (op == UserOperation.EditCommunity) {
       let res: CommunityResponse = msg;
index 0cb026167efbb6a3563592eeba488e21f834de94..2443369e0289276c504996b89e813f734f615fdf 100644 (file)
@@ -8,6 +8,8 @@ import { msgOp } from '../utils';
 interface State {
   loginForm: LoginForm;
   registerForm: RegisterForm;
+  loginLoading: boolean;
+  registerLoading: boolean;
 }
 
 let emptyState: State = {
@@ -19,7 +21,9 @@ let emptyState: State = {
     username: undefined,
     password: undefined,
     password_verify: undefined
-  }
+  },
+  loginLoading: false,
+  registerLoading: false
 }
 
 export class Login extends Component<any, State> {
@@ -31,12 +35,12 @@ export class Login extends Component<any, State> {
     this.state = emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        (msg) => this.parseMessage(msg),
+    .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+    .subscribe(
+      (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log("complete")
-      );
+    );
   }
 
   componentWillUnmount() {
@@ -77,7 +81,8 @@ export class Login extends Component<any, State> {
           </div>
           <div class="form-group row">
             <div class="col-sm-10">
-              <button type="submit" class="btn btn-secondary">Login</button>
+              <button type="submit" class="btn btn-secondary">{this.state.loginLoading ? 
+              <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Login'}</button>
             </div>
           </div>
         </form>
@@ -115,7 +120,9 @@ export class Login extends Component<any, State> {
         </div>
         <div class="form-group row">
           <div class="col-sm-10">
-            <button type="submit" class="btn btn-secondary">Sign Up</button>
+            <button type="submit" class="btn btn-secondary">{this.state.registerLoading ? 
+            <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
+
           </div>
         </div>
       </form>
@@ -124,6 +131,8 @@ export class Login extends Component<any, State> {
 
   handleLoginSubmit(i: Login, event: any) {
     event.preventDefault();
+    i.state.loginLoading = true;
+    i.setState(i.state);
     WebSocketService.Instance.login(i.state.loginForm);
   }
 
@@ -138,6 +147,8 @@ export class Login extends Component<any, State> {
   }
 
   handleRegisterSubmit(i: Login, event: any) {
+    i.state.registerLoading = true;
+    i.setState(i.state);
     event.preventDefault();
     WebSocketService.Instance.register(i.state.registerForm);
   }
@@ -166,9 +177,14 @@ export class Login extends Component<any, State> {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
       alert(msg.error);
+      this.state.loginLoading = false;
+      this.state.registerLoading = false;
+      this.setState(this.state);
       return;
     } else {
       if (op == UserOperation.Register || op == UserOperation.Login) {
+        this.state.loginLoading = false;
+        this.state.registerLoading = false;
         let res: LoginResponse = msg;
         UserService.Instance.login(res);
         this.props.history.push('/');
index aa3c3a8cd925b7e38f51f18c134c1061b0818d28..54f185e287a91469294d678eb0c8e7d11e578482 100644 (file)
@@ -9,13 +9,15 @@ import { msgOp } from '../utils';
 
 interface State {
   subscribedCommunities: Array<CommunityUser>;
+  loading: boolean;
 }
 
 export class Main extends Component<any, State> {
 
   private subscription: Subscription;
   private emptyState: State = {
-    subscribedCommunities: []
+    subscribedCommunities: [],
+    loading: true
   }
 
   constructor(props: any, context: any) {
@@ -24,12 +26,12 @@ export class Main extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        (msg) => this.parseMessage(msg),
+    .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+    .subscribe(
+      (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log('complete')
-      );
+    );
 
     if (UserService.Instance.loggedIn) {
       WebSocketService.Instance.getFollowedCommunities();
@@ -51,13 +53,18 @@ export class Main extends Component<any, State> {
             <h4>A Landing message</h4>
             {UserService.Instance.loggedIn &&
               <div>
-                <hr />
-                <h4>Subscribed forums</h4>
-                <ul class="list-unstyled"> 
-                  {this.state.subscribedCommunities.map(community =>
-                    <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
-                  )}
-                </ul>
+                {this.state.loading ? 
+                <h4 class="mt-3"><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+                <div>
+                  <hr />
+                  <h4>Subscribed forums</h4>
+                  <ul class="list-unstyled"> 
+                    {this.state.subscribedCommunities.map(community =>
+                      <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+                    )}
+                  </ul>
+                </div>
+                }
               </div>
             }
           </div>
@@ -76,6 +83,7 @@ export class Main extends Component<any, State> {
     } else if (op == UserOperation.GetFollowedCommunities) {
       let res: GetFollowedCommunitiesResponse = msg;
       this.state.subscribedCommunities = res.communities;
+      this.state.loading = false;
       this.setState(this.state);
     }
   }
index 1f23cae0536b4fb81563141fc614f2764813925a..9845a1b1aa0f572834549543f736ed27782f0c40 100644 (file)
@@ -4,6 +4,7 @@ import { retryWhen, delay, take } from 'rxjs/operators';
 import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse } from '../interfaces';
 import { WebSocketService } from '../services';
 import { msgOp } from '../utils';
+import * as autosize from 'autosize';
 
 interface PostFormProps {
   post?: Post; // If a post is given, that means this is an edit
@@ -15,6 +16,7 @@ interface PostFormProps {
 interface PostFormState {
   postForm: PostFormI;
   communities: Array<Community>;
+  loading: boolean;
 }
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
@@ -26,7 +28,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       auth: null,
       community_id: null
     },
-    communities: []
+    communities: [],
+    loading: false
   }
 
   constructor(props: any, context: any) {
@@ -56,6 +59,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     WebSocketService.Instance.listCommunities();
   }
 
+  componentDidMount() {
+    autosize(document.querySelectorAll('textarea'));
+  }
+
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
@@ -73,13 +80,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
           <div class="form-group row">
             <label class="col-sm-2 col-form-label">Title</label>
             <div class="col-sm-10">
-              <textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={3} />
+              <textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} />
             </div>
           </div>
           <div class="form-group row">
             <label class="col-sm-2 col-form-label">Body</label>
             <div class="col-sm-10">
-              <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={6} />
+              <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
             </div>
           </div>
           <div class="form-group row">
@@ -94,7 +101,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
           </div>
           <div class="form-group row">
             <div class="col-sm-10">
-              <button type="submit" class="btn btn-secondary mr-2">{this.props.post ? 'Save' : 'Create'}</button>
+              <button type="submit" class="btn btn-secondary mr-2">
+              {this.state.loading ? 
+              <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 
+              this.props.post ? 'Save' : 'Create'}</button>
               {this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
             </div>
           </div>
@@ -110,6 +120,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     } else {
       WebSocketService.Instance.createPost(i.state.postForm);
     }
+    i.state.loading = true;
+    i.setState(i.state);
   }
 
   handlePostUrlChange(i: PostForm, event: any) {
@@ -139,6 +151,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   parseMessage(msg: any) {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
+      this.state.loading = false;
       return;
     } else if (op == UserOperation.ListCommunities) {
       let res: ListCommunitiesResponse = msg;
@@ -150,9 +163,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       }
       this.setState(this.state);
     } else if (op == UserOperation.CreatePost) {
+      this.state.loading = false;
       let res: PostResponse = msg;
       this.props.onCreate(res.post.id);
     } else if (op == UserOperation.EditPost) {
+      this.state.loading = false;
       let res: PostResponse = msg;
       this.props.onEdit(res.post);
     }
index bb36cabccfb0ec0b93e469e7a426670070c9c4a4..ce7833b2bb96ec87e93d19655d11c7ba278b4646 100644 (file)
@@ -18,6 +18,7 @@ interface PostListingsState {
   posts: Array<Post>;
   sortType: SortType;
   type_: ListingType;
+  loading: boolean;
 }
 
 export class PostListings extends Component<PostListingsProps, PostListingsState> {
@@ -44,7 +45,8 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
     ? ListingType.Community 
     : UserService.Instance.loggedIn
     ? ListingType.Subscribed 
-    : ListingType.All
+    : ListingType.All,
+    loading: true
   }
 
   constructor(props: any, context: any) {
@@ -77,11 +79,16 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
   render() {
     return (
       <div>
-        <div>{this.selects()}</div>
-        {this.state.posts.length > 0 
-          ? this.state.posts.map(post => 
-            <PostListing post={post} showCommunity={!this.props.communityId}/>) 
-          : <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div>
+        {this.state.loading ? 
+        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <div>
+          <div>{this.selects()}</div>
+          {this.state.posts.length > 0 
+            ? this.state.posts.map(post => 
+              <PostListing post={post} showCommunity={!this.props.communityId}/>) 
+                : <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div>
+          }
+        </div>
         }
       </div>
     )
@@ -103,11 +110,11 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
         </select>
         {!this.props.communityId && 
           UserService.Instance.loggedIn &&
-          <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
-          <option disabled>Type</option>
-          <option value={ListingType.All}>All</option>
-          <option value={ListingType.Subscribed}>Subscribed</option>
-        </select>
+            <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
+              <option disabled>Type</option>
+              <option value={ListingType.All}>All</option>
+              <option value={ListingType.Subscribed}>Subscribed</option>
+            </select>
 
         }
       </div>
@@ -149,6 +156,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
     } else if (op == UserOperation.GetPosts) {
       let res: GetPostsResponse = msg;
       this.state.posts = res.posts;
+      this.state.loading = false;
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
       let res: CreatePostLikeResponse = msg;
index e830856125d95e25d43a5f8203754169a9ba7bff..f36893f69744281504b7bf97ff8259b7aca4fcee 100644 (file)
@@ -19,6 +19,7 @@ interface PostState {
   moderators: Array<CommunityUser>;
   scrolled?: boolean;
   scrolled_comment_id?: number;
+  loading: boolean;
 }
 
 export class Post extends Component<any, PostState> {
@@ -30,7 +31,8 @@ export class Post extends Component<any, PostState> {
     commentSort: CommentSortType.Hot,
     community: null,
     moderators: [],
-    scrolled: false
+    scrolled: false, 
+    loading: true
   }
 
   constructor(props: any, context: any) {
@@ -74,8 +76,9 @@ export class Post extends Component<any, PostState> {
   render() {
     return (
       <div class="container">
-        {this.state.post && 
-          <div class="row">
+        {this.state.loading ? 
+        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <div class="row">
             <div class="col-12 col-sm-8 col-lg-7 mb-3">
               <PostListing post={this.state.post} showBody showCommunity editable />
               <div className="mb-2" />
@@ -202,6 +205,7 @@ export class Post extends Component<any, PostState> {
       this.state.comments = res.comments;
       this.state.community = res.community;
       this.state.moderators = res.moderators;
+      this.state.loading = false;
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
       let res: CommentResponse = msg;
diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx
new file mode 100644 (file)
index 0000000..4f757db
--- /dev/null
@@ -0,0 +1,80 @@
+import { Component } from 'inferno';
+
+export class Symbols extends Component<any, any> {
+
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  render() {
+    return (
+      <svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+        <defs>
+          <symbol id="icon-mouse" version="1.1" x="0px" y="0px"
+            viewBox="0 0 512 512">
+            <g>
+              <g>
+                <path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19
+                  c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406
+                  c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718
+                  c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243
+                  c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001
+                  c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189
+                  c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752
+                  L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934
+                  c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519
+                  c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65
+                  c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z
+                  M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403
+                  C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81
+                  c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>
+                </g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+              <g>
+              </g>
+            </symbol>
+            <symbol id="icon-search" viewBox="0 0 32 32">
+              <title>search</title>
+              <path d="M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 7.787-2.87-0.031 0.677 0.293 1.515 0.998 2.299l6.447 7.58c1.104 1.226 2.907 1.33 4.007 0.23s0.997-2.903-0.23-4.007zM12 20c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"></path>
+            </symbol>
+            <symbol id="icon-github" viewBox="0 0 32 32">
+              <title>github</title>
+              <path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
+            </symbol>
+            <symbol id="icon-spinner" viewBox="0 0 32 32">
+              <title>spinner</title>
+              <path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
+            </symbol>
+          </defs>
+        </svg>
+    );
+  }
+}
index 45bae0f7ddbfc754fe1cb516e24fcb900cb187b5..608e487a3ebd4d4cedd96447f18695d19c160eac 100644 (file)
@@ -10,6 +10,7 @@ import { Post } from './components/post';
 import { Community } from './components/community';
 import { Communities } from './components/communities';
 import { User } from './components/user';
+import { Symbols } from './components/symbols';
 
 import './main.css';
 
@@ -42,68 +43,12 @@ class Index extends Component<any, any> {
             <Route path={`/user/:id/:heading`} component={User} />
             <Route path={`/user/:id`} component={User} />
           </Switch>
-          {this.symbols()}
+          <Symbols />
         </div>
       </HashRouter>
     );
   }
 
-  symbols() {
-    return(
-      <symbol id="icon-mouse" version="1.1" x="0px" y="0px"
-        viewBox="0 0 512 512">
-        <g>
-          <g>
-            <path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19
-              c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406
-              c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718
-              c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243
-              c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001
-              c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189
-              c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752
-              L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934
-              c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519
-              c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65
-              c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z
-              M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403
-              C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81
-              c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>
-            </g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-          <g>
-          </g>
-        </symbol>
-    );
-  }
-
 }
 
 render(<Index />, container);
index 1802eaa3d52cff79df16ef67a8f2772f1225f6f7..0b7da979c9686592e50084bf855472acf1095fe7 100644 (file)
@@ -54,5 +54,15 @@ body {
   stroke: currentColor;
   fill: currentColor;
   vertical-align: middle;
-  margin-bottom: 6px;
+  align-self: center;
+}
+
+
+.spin {
+  animation: spins 2s linear infinite;
+}
+
+@keyframes spins {
+  0% { transform: rotate(0deg); }
+  100% {  transform: rotate(359deg); }
 }