]> Untitled Git - lemmy-ui.git/blob - src/shared/components/main.tsx
Adding support for join messages. Fixes #8
[lemmy-ui.git] / src / shared / components / main.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import {
5   UserOperation,
6   CommunityUser,
7   GetFollowedCommunitiesResponse,
8   ListCommunitiesForm,
9   ListCommunitiesResponse,
10   Community,
11   SortType,
12   GetSiteResponse,
13   ListingType,
14   SiteResponse,
15   GetPostsResponse,
16   PostResponse,
17   Post,
18   GetPostsForm,
19   Comment,
20   GetCommentsForm,
21   GetCommentsResponse,
22   CommentResponse,
23   AddAdminResponse,
24   BanUserResponse,
25   WebSocketJsonResponse,
26 } from 'lemmy-js-client';
27 import { DataType } from '../interfaces';
28 import { WebSocketService, UserService } from '../services';
29 import { PostListings } from './post-listings';
30 import { CommentNodes } from './comment-nodes';
31 import { SortSelect } from './sort-select';
32 import { ListingTypeSelect } from './listing-type-select';
33 import { DataTypeSelect } from './data-type-select';
34 import { SiteForm } from './site-form';
35 import { UserListing } from './user-listing';
36 import { CommunityLink } from './community-link';
37 import { BannerIconHeader } from './banner-icon-header';
38 import {
39   wsJsonToRes,
40   repoUrl,
41   mdToHtml,
42   fetchLimit,
43   toast,
44   getListingTypeFromProps,
45   getPageFromProps,
46   getSortTypeFromProps,
47   getDataTypeFromProps,
48   editCommentRes,
49   saveCommentRes,
50   createCommentLikeRes,
51   createPostLikeFindRes,
52   editPostFindRes,
53   commentsToFlatNodes,
54   setupTippy,
55   notifyPost,
56   setIsoData,
57   wsSubscribe,
58   isBrowser,
59   setAuth,
60   lemmyHttp,
61 } from '../utils';
62 import { i18n } from '../i18next';
63 import { T } from 'inferno-i18next';
64 import { HtmlTags } from './html-tags';
65
66 interface MainState {
67   subscribedCommunities: CommunityUser[];
68   trendingCommunities: Community[];
69   siteRes: GetSiteResponse;
70   showEditSite: boolean;
71   loading: boolean;
72   posts: Post[];
73   comments: Comment[];
74   listingType: ListingType;
75   dataType: DataType;
76   sort: SortType;
77   page: number;
78 }
79
80 interface MainProps {
81   listingType: ListingType;
82   dataType: DataType;
83   sort: SortType;
84   page: number;
85 }
86
87 interface UrlParams {
88   listingType?: ListingType;
89   dataType?: string;
90   sort?: SortType;
91   page?: number;
92 }
93
94 export class Main extends Component<any, MainState> {
95   private isoData = setIsoData(this.context);
96   private subscription: Subscription;
97   private emptyState: MainState = {
98     subscribedCommunities: [],
99     trendingCommunities: [],
100     siteRes: this.isoData.site,
101     showEditSite: false,
102     loading: true,
103     posts: [],
104     comments: [],
105     listingType: getListingTypeFromProps(this.props),
106     dataType: getDataTypeFromProps(this.props),
107     sort: getSortTypeFromProps(this.props),
108     page: getPageFromProps(this.props),
109   };
110
111   constructor(props: any, context: any) {
112     super(props, context);
113
114     this.state = this.emptyState;
115     this.handleEditCancel = this.handleEditCancel.bind(this);
116     this.handleSortChange = this.handleSortChange.bind(this);
117     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
118     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
119
120     this.parseMessage = this.parseMessage.bind(this);
121     this.subscription = wsSubscribe(this.parseMessage);
122
123     // Only fetch the data if coming from another route
124     if (this.isoData.path == this.context.router.route.match.url) {
125       if (this.state.dataType == DataType.Post) {
126         this.state.posts = this.isoData.routeData[0].posts;
127       } else {
128         this.state.comments = this.isoData.routeData[0].comments;
129       }
130       this.state.trendingCommunities = this.isoData.routeData[1].communities;
131       if (UserService.Instance.user) {
132         this.state.subscribedCommunities = this.isoData.routeData[2].communities;
133       }
134       this.state.loading = false;
135     } else {
136       this.fetchTrendingCommunities();
137       this.fetchData();
138       if (UserService.Instance.user) {
139         WebSocketService.Instance.getFollowedCommunities();
140       }
141     }
142
143     setupTippy();
144   }
145
146   fetchTrendingCommunities() {
147     let listCommunitiesForm: ListCommunitiesForm = {
148       sort: SortType.Hot,
149       limit: 6,
150     };
151     WebSocketService.Instance.listCommunities(listCommunitiesForm);
152   }
153
154   componentDidMount() {
155     WebSocketService.Instance.communityJoin({ community_id: 0 });
156   }
157
158   componentWillUnmount() {
159     if (isBrowser()) {
160       this.subscription.unsubscribe();
161     }
162   }
163
164   static getDerivedStateFromProps(props: any): MainProps {
165     return {
166       listingType: getListingTypeFromProps(props),
167       dataType: getDataTypeFromProps(props),
168       sort: getSortTypeFromProps(props),
169       page: getPageFromProps(props),
170     };
171   }
172
173   static fetchInitialData(auth: string, path: string): Promise<any>[] {
174     let pathSplit = path.split('/');
175     let dataType: DataType = pathSplit[3]
176       ? DataType[pathSplit[3]]
177       : DataType.Post;
178
179     // TODO figure out auth default_listingType, default_sort_type
180     let type_: ListingType = pathSplit[5]
181       ? ListingType[pathSplit[5]]
182       : UserService.Instance.user
183       ? Object.values(ListingType)[
184           UserService.Instance.user.default_listing_type
185         ]
186       : ListingType.All;
187     let sort: SortType = pathSplit[7]
188       ? SortType[pathSplit[7]]
189       : UserService.Instance.user
190       ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
191       : SortType.Active;
192
193     let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
194
195     let promises: Promise<any>[] = [];
196
197     if (dataType == DataType.Post) {
198       let getPostsForm: GetPostsForm = {
199         page,
200         limit: fetchLimit,
201         sort,
202         type_,
203       };
204       setAuth(getPostsForm, auth);
205       promises.push(lemmyHttp.getPosts(getPostsForm));
206     } else {
207       let getCommentsForm: GetCommentsForm = {
208         page,
209         limit: fetchLimit,
210         sort,
211         type_,
212       };
213       setAuth(getCommentsForm, auth);
214       promises.push(lemmyHttp.getComments(getCommentsForm));
215     }
216
217     let trendingCommunitiesForm: ListCommunitiesForm = {
218       sort: SortType.Hot,
219       limit: 6,
220     };
221     promises.push(lemmyHttp.listCommunities(trendingCommunitiesForm));
222
223     if (auth) {
224       promises.push(lemmyHttp.getFollowedCommunities({ auth }));
225     }
226
227     return promises;
228   }
229
230   componentDidUpdate(_: any, lastState: MainState) {
231     if (
232       lastState.listingType !== this.state.listingType ||
233       lastState.dataType !== this.state.dataType ||
234       lastState.sort !== this.state.sort ||
235       lastState.page !== this.state.page
236     ) {
237       this.setState({ loading: true });
238       this.fetchData();
239     }
240   }
241
242   get documentTitle(): string {
243     return `${this.state.siteRes.site.name}`;
244   }
245
246   render() {
247     return (
248       <div class="container">
249         <HtmlTags
250           title={this.documentTitle}
251           path={this.context.router.route.match.url}
252         />
253         <div class="row">
254           <main role="main" class="col-12 col-md-8">
255             {this.posts()}
256           </main>
257           <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
258         </div>
259       </div>
260     );
261   }
262
263   mySidebar() {
264     return (
265       <div>
266         {!this.state.loading && (
267           <div>
268             <div class="card bg-transparent border-secondary mb-3">
269               <div class="card-header bg-transparent border-secondary">
270                 <div class="mb-2">
271                   {this.siteName()}
272                   {this.adminButtons()}
273                 </div>
274                 <BannerIconHeader banner={this.state.siteRes.site.banner} />
275               </div>
276               <div class="card-body">
277                 {this.trendingCommunities()}
278                 {this.createCommunityButton()}
279                 {/* TODO
280                 {this.subscribedCommunities()}
281                 */}
282               </div>
283             </div>
284
285             <div class="card bg-transparent border-secondary mb-3">
286               <div class="card-body">{this.sidebar()}</div>
287             </div>
288
289             <div class="card bg-transparent border-secondary">
290               <div class="card-body">{this.landing()}</div>
291             </div>
292           </div>
293         )}
294       </div>
295     );
296   }
297
298   createCommunityButton() {
299     return (
300       <Link className="btn btn-secondary btn-block" to="/create_community">
301         {i18n.t('create_a_community')}
302       </Link>
303     );
304   }
305
306   trendingCommunities() {
307     return (
308       <div>
309         <h5>
310           <T i18nKey="trending_communities">
311             #
312             <Link className="text-body" to="/communities">
313               #
314             </Link>
315           </T>
316         </h5>
317         <ul class="list-inline">
318           {this.state.trendingCommunities.map(community => (
319             <li class="list-inline-item">
320               <CommunityLink community={community} />
321             </li>
322           ))}
323         </ul>
324       </div>
325     );
326   }
327
328   subscribedCommunities() {
329     return (
330       UserService.Instance.user &&
331       this.state.subscribedCommunities.length > 0 && (
332         <div>
333           <h5>
334             <T i18nKey="subscribed_to_communities">
335               #
336               <Link className="text-body" to="/communities">
337                 #
338               </Link>
339             </T>
340           </h5>
341           <ul class="list-inline">
342             {this.state.subscribedCommunities.map(community => (
343               <li class="list-inline-item">
344                 <CommunityLink
345                   community={{
346                     name: community.community_name,
347                     id: community.community_id,
348                     local: community.community_local,
349                     actor_id: community.community_actor_id,
350                     icon: community.community_icon,
351                   }}
352                 />
353               </li>
354             ))}
355           </ul>
356         </div>
357       )
358     );
359   }
360
361   sidebar() {
362     return (
363       <div>
364         {!this.state.showEditSite ? (
365           this.siteInfo()
366         ) : (
367           <SiteForm
368             site={this.state.siteRes.site}
369             onCancel={this.handleEditCancel}
370           />
371         )}
372       </div>
373     );
374   }
375
376   updateUrl(paramUpdates: UrlParams) {
377     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
378     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
379     const sortStr = paramUpdates.sort || this.state.sort;
380     const page = paramUpdates.page || this.state.page;
381     this.props.history.push(
382       `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
383     );
384   }
385
386   siteInfo() {
387     return (
388       <div>
389         {this.state.siteRes.site.description && this.siteDescription()}
390         {this.badges()}
391         {this.admins()}
392       </div>
393     );
394   }
395
396   siteName() {
397     return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
398   }
399
400   admins() {
401     return (
402       <ul class="mt-1 list-inline small mb-0">
403         <li class="list-inline-item">{i18n.t('admins')}:</li>
404         {this.state.siteRes.admins.map(admin => (
405           <li class="list-inline-item">
406             <UserListing
407               user={{
408                 name: admin.name,
409                 preferred_username: admin.preferred_username,
410                 avatar: admin.avatar,
411                 local: admin.local,
412                 actor_id: admin.actor_id,
413                 id: admin.id,
414               }}
415             />
416           </li>
417         ))}
418       </ul>
419     );
420   }
421
422   badges() {
423     return (
424       <ul class="my-2 list-inline">
425         <li className="list-inline-item badge badge-light">
426           {i18n.t('number_online', { count: this.state.siteRes.online })}
427         </li>
428         <li className="list-inline-item badge badge-light">
429           {i18n.t('number_of_users', {
430             count: this.state.siteRes.site.number_of_users,
431           })}
432         </li>
433         <li className="list-inline-item badge badge-light">
434           {i18n.t('number_of_communities', {
435             count: this.state.siteRes.site.number_of_communities,
436           })}
437         </li>
438         <li className="list-inline-item badge badge-light">
439           {i18n.t('number_of_posts', {
440             count: this.state.siteRes.site.number_of_posts,
441           })}
442         </li>
443         <li className="list-inline-item badge badge-light">
444           {i18n.t('number_of_comments', {
445             count: this.state.siteRes.site.number_of_comments,
446           })}
447         </li>
448         <li className="list-inline-item">
449           <Link className="badge badge-light" to="/modlog">
450             {i18n.t('modlog')}
451           </Link>
452         </li>
453       </ul>
454     );
455   }
456
457   adminButtons() {
458     return (
459       this.canAdmin && (
460         <ul class="list-inline mb-1 text-muted font-weight-bold">
461           <li className="list-inline-item-action">
462             <span
463               class="pointer"
464               onClick={linkEvent(this, this.handleEditClick)}
465               data-tippy-content={i18n.t('edit')}
466             >
467               <svg class="icon icon-inline">
468                 <use xlinkHref="#icon-edit"></use>
469               </svg>
470             </span>
471           </li>
472         </ul>
473       )
474     );
475   }
476
477   siteDescription() {
478     return (
479       <div
480         className="md-div"
481         dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
482       />
483     );
484   }
485
486   landing() {
487     return (
488       <>
489         <h5>
490           {i18n.t('powered_by')}
491           <svg class="icon mx-2">
492             <use xlinkHref="#icon-mouse">#</use>
493           </svg>
494           <a href={repoUrl}>
495             Lemmy<sup>beta</sup>
496           </a>
497         </h5>
498         <p class="mb-0">
499           <T i18nKey="landing_0">
500             #
501             <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
502               #
503             </a>
504             <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
505             <br class="big"></br>
506             <code>#</code>
507             <br></br>
508             <b>#</b>
509             <br class="big"></br>
510             <a href={repoUrl}>#</a>
511             <br class="big"></br>
512             <a href="https://www.rust-lang.org">#</a>
513             <a href="https://actix.rs/">#</a>
514             <a href="https://infernojs.org">#</a>
515             <a href="https://www.typescriptlang.org/">#</a>
516             <br class="big"></br>
517             <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
518               #
519             </a>
520           </T>
521         </p>
522       </>
523     );
524   }
525
526   posts() {
527     return (
528       <div class="main-content-wrapper">
529         {this.state.loading ? (
530           <h5>
531             <svg class="icon icon-spinner spin">
532               <use xlinkHref="#icon-spinner"></use>
533             </svg>
534           </h5>
535         ) : (
536           <div>
537             {this.selects()}
538             {this.listings()}
539             {this.paginator()}
540           </div>
541         )}
542       </div>
543     );
544   }
545
546   listings() {
547     return this.state.dataType == DataType.Post ? (
548       <PostListings
549         posts={this.state.posts}
550         showCommunity
551         removeDuplicates
552         sort={this.state.sort}
553         enableDownvotes={this.state.siteRes.site.enable_downvotes}
554         enableNsfw={this.state.siteRes.site.enable_nsfw}
555       />
556     ) : (
557       <CommentNodes
558         nodes={commentsToFlatNodes(this.state.comments)}
559         noIndent
560         showCommunity
561         sortType={this.state.sort}
562         showContext
563         enableDownvotes={this.state.siteRes.site.enable_downvotes}
564       />
565     );
566   }
567
568   selects() {
569     return (
570       <div className="mb-3">
571         <span class="mr-3">
572           <DataTypeSelect
573             type_={this.state.dataType}
574             onChange={this.handleDataTypeChange}
575           />
576         </span>
577         <span class="mr-3">
578           <ListingTypeSelect
579             type_={this.state.listingType}
580             showLocal={
581               this.state.siteRes.federated_instances &&
582               this.state.siteRes.federated_instances.length > 0
583             }
584             onChange={this.handleListingTypeChange}
585           />
586         </span>
587         <span class="mr-2">
588           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
589         </span>
590         {this.state.listingType == ListingType.All && (
591           <a
592             href={`/feeds/all.xml?sort=${this.state.sort}`}
593             target="_blank"
594             rel="noopener"
595             title="RSS"
596           >
597             <svg class="icon text-muted small">
598               <use xlinkHref="#icon-rss">#</use>
599             </svg>
600           </a>
601         )}
602         {UserService.Instance.user &&
603           this.state.listingType == ListingType.Subscribed && (
604             <a
605               href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
606               target="_blank"
607               title="RSS"
608               rel="noopener"
609             >
610               <svg class="icon text-muted small">
611                 <use xlinkHref="#icon-rss">#</use>
612               </svg>
613             </a>
614           )}
615       </div>
616     );
617   }
618
619   paginator() {
620     return (
621       <div class="my-2">
622         {this.state.page > 1 && (
623           <button
624             class="btn btn-secondary mr-1"
625             onClick={linkEvent(this, this.prevPage)}
626           >
627             {i18n.t('prev')}
628           </button>
629         )}
630         {this.state.posts.length > 0 && (
631           <button
632             class="btn btn-secondary"
633             onClick={linkEvent(this, this.nextPage)}
634           >
635             {i18n.t('next')}
636           </button>
637         )}
638       </div>
639     );
640   }
641
642   get canAdmin(): boolean {
643     return (
644       UserService.Instance.user &&
645       this.state.siteRes.admins
646         .map(a => a.id)
647         .includes(UserService.Instance.user.id)
648     );
649   }
650
651   handleEditClick(i: Main) {
652     i.state.showEditSite = true;
653     i.setState(i.state);
654   }
655
656   handleEditCancel() {
657     this.state.showEditSite = false;
658     this.setState(this.state);
659   }
660
661   nextPage(i: Main) {
662     i.updateUrl({ page: i.state.page + 1 });
663     window.scrollTo(0, 0);
664   }
665
666   prevPage(i: Main) {
667     i.updateUrl({ page: i.state.page - 1 });
668     window.scrollTo(0, 0);
669   }
670
671   handleSortChange(val: SortType) {
672     this.updateUrl({ sort: val, page: 1 });
673     window.scrollTo(0, 0);
674   }
675
676   handleListingTypeChange(val: ListingType) {
677     this.updateUrl({ listingType: val, page: 1 });
678     window.scrollTo(0, 0);
679   }
680
681   handleDataTypeChange(val: DataType) {
682     this.updateUrl({ dataType: DataType[val], page: 1 });
683     window.scrollTo(0, 0);
684   }
685
686   fetchData() {
687     if (this.state.dataType == DataType.Post) {
688       let getPostsForm: GetPostsForm = {
689         page: this.state.page,
690         limit: fetchLimit,
691         sort: this.state.sort,
692         type_: this.state.listingType,
693       };
694       WebSocketService.Instance.getPosts(getPostsForm);
695     } else {
696       let getCommentsForm: GetCommentsForm = {
697         page: this.state.page,
698         limit: fetchLimit,
699         sort: this.state.sort,
700         type_: this.state.listingType,
701       };
702       WebSocketService.Instance.getComments(getCommentsForm);
703     }
704   }
705
706   parseMessage(msg: WebSocketJsonResponse) {
707     console.log(msg);
708     let res = wsJsonToRes(msg);
709     if (msg.error) {
710       toast(i18n.t(msg.error), 'danger');
711       return;
712     } else if (msg.reconnect) {
713       this.fetchData();
714     } else if (res.op == UserOperation.GetFollowedCommunities) {
715       let data = res.data as GetFollowedCommunitiesResponse;
716       this.state.subscribedCommunities = data.communities;
717       this.setState(this.state);
718     } else if (res.op == UserOperation.ListCommunities) {
719       let data = res.data as ListCommunitiesResponse;
720       this.state.trendingCommunities = data.communities;
721       this.setState(this.state);
722     } else if (res.op == UserOperation.GetSite) {
723       let data = res.data as GetSiteResponse;
724
725       // This means it hasn't been set up yet
726       if (!data.site) {
727         this.context.router.history.push('/setup');
728       }
729       this.state.siteRes.admins = data.admins;
730       this.state.siteRes.site = data.site;
731       this.state.siteRes.banned = data.banned;
732       this.state.siteRes.online = data.online;
733       this.setState(this.state);
734     } else if (res.op == UserOperation.EditSite) {
735       let data = res.data as SiteResponse;
736       this.state.siteRes.site = data.site;
737       this.state.showEditSite = false;
738       this.setState(this.state);
739       toast(i18n.t('site_saved'));
740     } else if (res.op == UserOperation.GetPosts) {
741       let data = res.data as GetPostsResponse;
742       this.state.posts = data.posts;
743       this.state.loading = false;
744       this.setState(this.state);
745       setupTippy();
746     } else if (res.op == UserOperation.CreatePost) {
747       let data = res.data as PostResponse;
748
749       // If you're on subscribed, only push it if you're subscribed.
750       if (this.state.listingType == ListingType.Subscribed) {
751         if (
752           this.state.subscribedCommunities
753             .map(c => c.community_id)
754             .includes(data.post.community_id)
755         ) {
756           this.state.posts.unshift(data.post);
757           notifyPost(data.post, this.context.router);
758         }
759       } else {
760         // NSFW posts
761         let nsfw = data.post.nsfw || data.post.community_nsfw;
762
763         // Don't push the post if its nsfw, and don't have that setting on
764         if (
765           !nsfw ||
766           (nsfw &&
767             UserService.Instance.user &&
768             UserService.Instance.user.show_nsfw)
769         ) {
770           this.state.posts.unshift(data.post);
771           notifyPost(data.post, this.context.router);
772         }
773       }
774       this.setState(this.state);
775     } else if (res.op == UserOperation.EditPost) {
776       let data = res.data as PostResponse;
777       editPostFindRes(data, this.state.posts);
778       this.setState(this.state);
779     } else if (res.op == UserOperation.CreatePostLike) {
780       let data = res.data as PostResponse;
781       createPostLikeFindRes(data, this.state.posts);
782       this.setState(this.state);
783     } else if (res.op == UserOperation.AddAdmin) {
784       let data = res.data as AddAdminResponse;
785       this.state.siteRes.admins = data.admins;
786       this.setState(this.state);
787     } else if (res.op == UserOperation.BanUser) {
788       let data = res.data as BanUserResponse;
789       let found = this.state.siteRes.banned.find(u => (u.id = data.user.id));
790
791       // Remove the banned if its found in the list, and the action is an unban
792       if (found && !data.banned) {
793         this.state.siteRes.banned = this.state.siteRes.banned.filter(
794           i => i.id !== data.user.id
795         );
796       } else {
797         this.state.siteRes.banned.push(data.user);
798       }
799
800       this.state.posts
801         .filter(p => p.creator_id == data.user.id)
802         .forEach(p => (p.banned = data.banned));
803
804       this.setState(this.state);
805     } else if (res.op == UserOperation.GetComments) {
806       let data = res.data as GetCommentsResponse;
807       this.state.comments = data.comments;
808       this.state.loading = false;
809       this.setState(this.state);
810     } else if (
811       res.op == UserOperation.EditComment ||
812       res.op == UserOperation.DeleteComment ||
813       res.op == UserOperation.RemoveComment
814     ) {
815       let data = res.data as CommentResponse;
816       editCommentRes(data, this.state.comments);
817       this.setState(this.state);
818     } else if (res.op == UserOperation.CreateComment) {
819       let data = res.data as CommentResponse;
820
821       // Necessary since it might be a user reply
822       if (data.recipient_ids.length == 0) {
823         // If you're on subscribed, only push it if you're subscribed.
824         if (this.state.listingType == ListingType.Subscribed) {
825           if (
826             this.state.subscribedCommunities
827               .map(c => c.community_id)
828               .includes(data.comment.community_id)
829           ) {
830             this.state.comments.unshift(data.comment);
831           }
832         } else {
833           this.state.comments.unshift(data.comment);
834         }
835         this.setState(this.state);
836       }
837     } else if (res.op == UserOperation.SaveComment) {
838       let data = res.data as CommentResponse;
839       saveCommentRes(data, this.state.comments);
840       this.setState(this.state);
841     } else if (res.op == UserOperation.CreateCommentLike) {
842       let data = res.data as CommentResponse;
843       createCommentLikeRes(data, this.state.comments);
844       this.setState(this.state);
845     }
846   }
847 }