]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Adding custom language setting.
[lemmy.git] / ui / src / components / user.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import {
6   UserOperation,
7   Post,
8   Comment,
9   CommunityUser,
10   GetUserDetailsForm,
11   SortType,
12   ListingType,
13   UserDetailsResponse,
14   UserView,
15   CommentResponse,
16   UserSettingsForm,
17   LoginResponse,
18   BanUserResponse,
19   AddAdminResponse,
20   DeleteAccountForm,
21 } from '../interfaces';
22 import { WebSocketService, UserService } from '../services';
23 import {
24   msgOp,
25   fetchLimit,
26   routeSortTypeToEnum,
27   capitalizeFirstLetter,
28   themes,
29   setTheme,
30   languages,
31 } from '../utils';
32 import { PostListing } from './post-listing';
33 import { SortSelect } from './sort-select';
34 import { ListingTypeSelect } from './listing-type-select';
35 import { CommentNodes } from './comment-nodes';
36 import { MomentTime } from './moment-time';
37 import { i18n } from '../i18next';
38 import { T } from 'inferno-i18next';
39
40 enum View {
41   Overview,
42   Comments,
43   Posts,
44   Saved,
45 }
46
47 interface UserState {
48   user: UserView;
49   user_id: number;
50   username: string;
51   follows: Array<CommunityUser>;
52   moderates: Array<CommunityUser>;
53   comments: Array<Comment>;
54   posts: Array<Post>;
55   saved?: Array<Post>;
56   admins: Array<UserView>;
57   view: View;
58   sort: SortType;
59   page: number;
60   loading: boolean;
61   userSettingsForm: UserSettingsForm;
62   userSettingsLoading: boolean;
63   deleteAccountLoading: boolean;
64   deleteAccountShowConfirm: boolean;
65   deleteAccountForm: DeleteAccountForm;
66 }
67
68 export class User extends Component<any, UserState> {
69   private subscription: Subscription;
70   private emptyState: UserState = {
71     user: {
72       id: null,
73       name: null,
74       fedi_name: null,
75       published: null,
76       number_of_posts: null,
77       post_score: null,
78       number_of_comments: null,
79       comment_score: null,
80       banned: null,
81     },
82     user_id: null,
83     username: null,
84     follows: [],
85     moderates: [],
86     comments: [],
87     posts: [],
88     admins: [],
89     loading: true,
90     view: this.getViewFromProps(this.props),
91     sort: this.getSortTypeFromProps(this.props),
92     page: this.getPageFromProps(this.props),
93     userSettingsForm: {
94       show_nsfw: null,
95       theme: null,
96       default_sort_type: null,
97       default_listing_type: null,
98       lang: null,
99       auth: null,
100     },
101     userSettingsLoading: null,
102     deleteAccountLoading: null,
103     deleteAccountShowConfirm: false,
104     deleteAccountForm: {
105       password: null,
106     },
107   };
108
109   constructor(props: any, context: any) {
110     super(props, context);
111
112     this.state = this.emptyState;
113     this.handleSortChange = this.handleSortChange.bind(this);
114     this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
115       this
116     );
117     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
118       this
119     );
120
121     this.state.user_id = Number(this.props.match.params.id);
122     this.state.username = this.props.match.params.username;
123
124     this.subscription = WebSocketService.Instance.subject
125       .pipe(
126         retryWhen(errors =>
127           errors.pipe(
128             delay(3000),
129             take(10)
130           )
131         )
132       )
133       .subscribe(
134         msg => this.parseMessage(msg),
135         err => console.error(err),
136         () => console.log('complete')
137       );
138
139     this.refetch();
140   }
141
142   get isCurrentUser() {
143     return (
144       UserService.Instance.user &&
145       UserService.Instance.user.id == this.state.user.id
146     );
147   }
148
149   getViewFromProps(props: any): View {
150     return props.match.params.view
151       ? View[capitalizeFirstLetter(props.match.params.view)]
152       : View.Overview;
153   }
154
155   getSortTypeFromProps(props: any): SortType {
156     return props.match.params.sort
157       ? routeSortTypeToEnum(props.match.params.sort)
158       : SortType.New;
159   }
160
161   getPageFromProps(props: any): number {
162     return props.match.params.page ? Number(props.match.params.page) : 1;
163   }
164
165   componentWillUnmount() {
166     this.subscription.unsubscribe();
167   }
168
169   // Necessary for back button for some reason
170   componentWillReceiveProps(nextProps: any) {
171     if (
172       nextProps.history.action == 'POP' ||
173       nextProps.history.action == 'PUSH'
174     ) {
175       this.state.view = this.getViewFromProps(nextProps);
176       this.state.sort = this.getSortTypeFromProps(nextProps);
177       this.state.page = this.getPageFromProps(nextProps);
178       this.setState(this.state);
179       this.refetch();
180     }
181   }
182
183   componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
184     // Necessary if you are on a post and you click another post (same route)
185     if (
186       lastProps.location.pathname.split('/')[2] !==
187       lastProps.history.location.pathname.split('/')[2]
188     ) {
189       // Couldnt get a refresh working. This does for now.
190       location.reload();
191     }
192   }
193
194   render() {
195     return (
196       <div class="container">
197         {this.state.loading ? (
198           <h5>
199             <svg class="icon icon-spinner spin">
200               <use xlinkHref="#icon-spinner"></use>
201             </svg>
202           </h5>
203         ) : (
204           <div class="row">
205             <div class="col-12 col-md-8">
206               <h5>/u/{this.state.user.name}</h5>
207               {this.selects()}
208               {this.state.view == View.Overview && this.overview()}
209               {this.state.view == View.Comments && this.comments()}
210               {this.state.view == View.Posts && this.posts()}
211               {this.state.view == View.Saved && this.overview()}
212               {this.paginator()}
213             </div>
214             <div class="col-12 col-md-4">
215               {this.userInfo()}
216               {this.isCurrentUser && this.userSettings()}
217               {this.moderates()}
218               {this.follows()}
219             </div>
220           </div>
221         )}
222       </div>
223     );
224   }
225
226   selects() {
227     return (
228       <div className="mb-2">
229         <select
230           value={this.state.view}
231           onChange={linkEvent(this, this.handleViewChange)}
232           class="custom-select custom-select-sm w-auto"
233         >
234           <option disabled>
235             <T i18nKey="view">#</T>
236           </option>
237           <option value={View.Overview}>
238             <T i18nKey="overview">#</T>
239           </option>
240           <option value={View.Comments}>
241             <T i18nKey="comments">#</T>
242           </option>
243           <option value={View.Posts}>
244             <T i18nKey="posts">#</T>
245           </option>
246           <option value={View.Saved}>
247             <T i18nKey="saved">#</T>
248           </option>
249         </select>
250         <span class="ml-2">
251           <SortSelect
252             sort={this.state.sort}
253             onChange={this.handleSortChange}
254             hideHot
255           />
256         </span>
257         <a
258           href={`/feeds/u/${this.state.username}.xml?sort=${
259             SortType[this.state.sort]
260           }`}
261           target="_blank"
262         >
263           <svg class="icon mx-2 text-muted small">
264             <use xlinkHref="#icon-rss">#</use>
265           </svg>
266         </a>
267       </div>
268     );
269   }
270
271   overview() {
272     let combined: Array<{ type_: string; data: Comment | Post }> = [];
273     let comments = this.state.comments.map(e => {
274       return { type_: 'comments', data: e };
275     });
276     let posts = this.state.posts.map(e => {
277       return { type_: 'posts', data: e };
278     });
279
280     combined.push(...comments);
281     combined.push(...posts);
282
283     // Sort it
284     if (this.state.sort == SortType.New) {
285       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
286     } else {
287       combined.sort((a, b) => b.data.score - a.data.score);
288     }
289
290     return (
291       <div>
292         {combined.map(i => (
293           <div>
294             {i.type_ == 'posts' ? (
295               <PostListing
296                 post={i.data as Post}
297                 admins={this.state.admins}
298                 showCommunity
299                 viewOnly
300               />
301             ) : (
302               <CommentNodes
303                 nodes={[{ comment: i.data as Comment }]}
304                 admins={this.state.admins}
305                 noIndent
306               />
307             )}
308           </div>
309         ))}
310       </div>
311     );
312   }
313
314   comments() {
315     return (
316       <div>
317         {this.state.comments.map(comment => (
318           <CommentNodes
319             nodes={[{ comment: comment }]}
320             admins={this.state.admins}
321             noIndent
322           />
323         ))}
324       </div>
325     );
326   }
327
328   posts() {
329     return (
330       <div>
331         {this.state.posts.map(post => (
332           <PostListing
333             post={post}
334             admins={this.state.admins}
335             showCommunity
336             viewOnly
337           />
338         ))}
339       </div>
340     );
341   }
342
343   userInfo() {
344     let user = this.state.user;
345     return (
346       <div>
347         <div class="card border-secondary mb-3">
348           <div class="card-body">
349             <h5>
350               <ul class="list-inline mb-0">
351                 <li className="list-inline-item">{user.name}</li>
352                 {user.banned && (
353                   <li className="list-inline-item badge badge-danger">
354                     <T i18nKey="banned">#</T>
355                   </li>
356                 )}
357               </ul>
358             </h5>
359             <div>
360               {i18n.t('joined')} <MomentTime data={user} />
361             </div>
362             <div class="table-responsive">
363               <table class="table table-bordered table-sm mt-2 mb-0">
364                 <tr>
365                   <td>
366                     <T
367                       i18nKey="number_of_points"
368                       interpolation={{ count: user.post_score }}
369                     >
370                       #
371                     </T>
372                   </td>
373                   <td>
374                     <T
375                       i18nKey="number_of_posts"
376                       interpolation={{ count: user.number_of_posts }}
377                     >
378                       #
379                     </T>
380                   </td>
381                 </tr>
382                 <tr>
383                   <td>
384                     <T
385                       i18nKey="number_of_points"
386                       interpolation={{ count: user.comment_score }}
387                     >
388                       #
389                     </T>
390                   </td>
391                   <td>
392                     <T
393                       i18nKey="number_of_comments"
394                       interpolation={{ count: user.number_of_comments }}
395                     >
396                       #
397                     </T>
398                   </td>
399                 </tr>
400               </table>
401             </div>
402             {this.isCurrentUser && (
403               <button
404                 class="btn btn-block btn-secondary mt-3"
405                 onClick={linkEvent(this, this.handleLogoutClick)}
406               >
407                 <T i18nKey="logout">#</T>
408               </button>
409             )}
410           </div>
411         </div>
412       </div>
413     );
414   }
415
416   userSettings() {
417     return (
418       <div>
419         <div class="card border-secondary mb-3">
420           <div class="card-body">
421             <h5>
422               <T i18nKey="settings">#</T>
423             </h5>
424             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
425               <div class="form-group">
426                 <div class="col-12">
427                   <label>
428                     <T i18nKey="language">#</T>
429                   </label>
430                   <select
431                     value={this.state.userSettingsForm.lang}
432                     onChange={linkEvent(
433                       this,
434                       this.handleUserSettingsLangChange
435                     )}
436                     class="ml-2 custom-select custom-select-sm w-auto"
437                   >
438                     <option disabled>
439                       <T i18nKey="language">#</T>
440                     </option>
441                     <option value="browser">
442                       <T i18nKey="browser_default">#</T>
443                     </option>
444                     <option disabled>──</option>
445                     {languages.map(lang => (
446                       <option value={lang.code}>{lang.name}</option>
447                     ))}
448                   </select>
449                 </div>
450               </div>
451               <div class="form-group">
452                 <div class="col-12">
453                   <label>
454                     <T i18nKey="theme">#</T>
455                   </label>
456                   <select
457                     value={this.state.userSettingsForm.theme}
458                     onChange={linkEvent(
459                       this,
460                       this.handleUserSettingsThemeChange
461                     )}
462                     class="ml-2 custom-select custom-select-sm w-auto"
463                   >
464                     <option disabled>
465                       <T i18nKey="theme">#</T>
466                     </option>
467                     {themes.map(theme => (
468                       <option value={theme}>{theme}</option>
469                     ))}
470                   </select>
471                 </div>
472               </div>
473               <form className="form-group">
474                 <div class="col-12">
475                   <label>
476                     <T i18nKey="sort_type" class="mr-2">
477                       #
478                     </T>
479                   </label>
480                   <ListingTypeSelect
481                     type_={this.state.userSettingsForm.default_listing_type}
482                     onChange={this.handleUserSettingsListingTypeChange}
483                   />
484                 </div>
485               </form>
486               <form className="form-group">
487                 <div class="col-12">
488                   <label>
489                     <T i18nKey="type" class="mr-2">
490                       #
491                     </T>
492                   </label>
493                   <SortSelect
494                     sort={this.state.userSettingsForm.default_sort_type}
495                     onChange={this.handleUserSettingsSortTypeChange}
496                   />
497                 </div>
498               </form>
499               <div class="form-group">
500                 <div class="col-12">
501                   <div class="form-check">
502                     <input
503                       class="form-check-input"
504                       type="checkbox"
505                       checked={this.state.userSettingsForm.show_nsfw}
506                       onChange={linkEvent(
507                         this,
508                         this.handleUserSettingsShowNsfwChange
509                       )}
510                     />
511                     <label class="form-check-label">
512                       <T i18nKey="show_nsfw">#</T>
513                     </label>
514                   </div>
515                 </div>
516               </div>
517               <div class="form-group">
518                 <div class="col-12">
519                   <button
520                     type="submit"
521                     class="btn btn-block btn-secondary mr-4"
522                   >
523                     {this.state.userSettingsLoading ? (
524                       <svg class="icon icon-spinner spin">
525                         <use xlinkHref="#icon-spinner"></use>
526                       </svg>
527                     ) : (
528                       capitalizeFirstLetter(i18n.t('save'))
529                     )}
530                   </button>
531                 </div>
532               </div>
533               <hr />
534               <div class="form-group mb-0">
535                 <div class="col-12">
536                   <button
537                     class="btn btn-block btn-danger"
538                     onClick={linkEvent(
539                       this,
540                       this.handleDeleteAccountShowConfirmToggle
541                     )}
542                   >
543                     <T i18nKey="delete_account">#</T>
544                   </button>
545                   {this.state.deleteAccountShowConfirm && (
546                     <>
547                       <div class="my-2 alert alert-danger" role="alert">
548                         <T i18nKey="delete_account_confirm">#</T>
549                       </div>
550                       <input
551                         type="password"
552                         value={this.state.deleteAccountForm.password}
553                         onInput={linkEvent(
554                           this,
555                           this.handleDeleteAccountPasswordChange
556                         )}
557                         class="form-control my-2"
558                       />
559                       <button
560                         class="btn btn-danger mr-4"
561                         disabled={!this.state.deleteAccountForm.password}
562                         onClick={linkEvent(this, this.handleDeleteAccount)}
563                       >
564                         {this.state.deleteAccountLoading ? (
565                           <svg class="icon icon-spinner spin">
566                             <use xlinkHref="#icon-spinner"></use>
567                           </svg>
568                         ) : (
569                           capitalizeFirstLetter(i18n.t('delete'))
570                         )}
571                       </button>
572                       <button
573                         class="btn btn-secondary"
574                         onClick={linkEvent(
575                           this,
576                           this.handleDeleteAccountShowConfirmToggle
577                         )}
578                       >
579                         <T i18nKey="cancel">#</T>
580                       </button>
581                     </>
582                   )}
583                 </div>
584               </div>
585             </form>
586           </div>
587         </div>
588       </div>
589     );
590   }
591
592   moderates() {
593     return (
594       <div>
595         {this.state.moderates.length > 0 && (
596           <div class="card border-secondary mb-3">
597             <div class="card-body">
598               <h5>
599                 <T i18nKey="moderates">#</T>
600               </h5>
601               <ul class="list-unstyled mb-0">
602                 {this.state.moderates.map(community => (
603                   <li>
604                     <Link to={`/c/${community.community_name}`}>
605                       {community.community_name}
606                     </Link>
607                   </li>
608                 ))}
609               </ul>
610             </div>
611           </div>
612         )}
613       </div>
614     );
615   }
616
617   follows() {
618     return (
619       <div>
620         {this.state.follows.length > 0 && (
621           <div class="card border-secondary mb-3">
622             <div class="card-body">
623               <h5>
624                 <T i18nKey="subscribed">#</T>
625               </h5>
626               <ul class="list-unstyled mb-0">
627                 {this.state.follows.map(community => (
628                   <li>
629                     <Link to={`/c/${community.community_name}`}>
630                       {community.community_name}
631                     </Link>
632                   </li>
633                 ))}
634               </ul>
635             </div>
636           </div>
637         )}
638       </div>
639     );
640   }
641
642   paginator() {
643     return (
644       <div class="my-2">
645         {this.state.page > 1 && (
646           <button
647             class="btn btn-sm btn-secondary mr-1"
648             onClick={linkEvent(this, this.prevPage)}
649           >
650             <T i18nKey="prev">#</T>
651           </button>
652         )}
653         <button
654           class="btn btn-sm btn-secondary"
655           onClick={linkEvent(this, this.nextPage)}
656         >
657           <T i18nKey="next">#</T>
658         </button>
659       </div>
660     );
661   }
662
663   updateUrl() {
664     let viewStr = View[this.state.view].toLowerCase();
665     let sortStr = SortType[this.state.sort].toLowerCase();
666     this.props.history.push(
667       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
668     );
669   }
670
671   nextPage(i: User) {
672     i.state.page++;
673     i.setState(i.state);
674     i.updateUrl();
675     i.refetch();
676   }
677
678   prevPage(i: User) {
679     i.state.page--;
680     i.setState(i.state);
681     i.updateUrl();
682     i.refetch();
683   }
684
685   refetch() {
686     let form: GetUserDetailsForm = {
687       user_id: this.state.user_id,
688       username: this.state.username,
689       sort: SortType[this.state.sort],
690       saved_only: this.state.view == View.Saved,
691       page: this.state.page,
692       limit: fetchLimit,
693     };
694     WebSocketService.Instance.getUserDetails(form);
695   }
696
697   handleSortChange(val: SortType) {
698     this.state.sort = val;
699     this.state.page = 1;
700     this.setState(this.state);
701     this.updateUrl();
702     this.refetch();
703   }
704
705   handleViewChange(i: User, event: any) {
706     i.state.view = Number(event.target.value);
707     i.state.page = 1;
708     i.setState(i.state);
709     i.updateUrl();
710     i.refetch();
711   }
712
713   handleUserSettingsShowNsfwChange(i: User, event: any) {
714     i.state.userSettingsForm.show_nsfw = event.target.checked;
715     i.setState(i.state);
716   }
717
718   handleUserSettingsThemeChange(i: User, event: any) {
719     i.state.userSettingsForm.theme = event.target.value;
720     setTheme(event.target.value);
721     i.setState(i.state);
722   }
723
724   handleUserSettingsLangChange(i: User, event: any) {
725     i.state.userSettingsForm.lang = event.target.value;
726     i18n.changeLanguage(i.state.userSettingsForm.lang);
727     i.setState(i.state);
728   }
729
730   handleUserSettingsSortTypeChange(val: SortType) {
731     this.state.userSettingsForm.default_sort_type = val;
732     this.setState(this.state);
733   }
734
735   handleUserSettingsListingTypeChange(val: ListingType) {
736     this.state.userSettingsForm.default_listing_type = val;
737     this.setState(this.state);
738   }
739
740   handleUserSettingsSubmit(i: User, event: any) {
741     event.preventDefault();
742     i.state.userSettingsLoading = true;
743     i.setState(i.state);
744
745     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
746   }
747
748   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
749     event.preventDefault();
750     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
751     i.setState(i.state);
752   }
753
754   handleDeleteAccountPasswordChange(i: User, event: any) {
755     i.state.deleteAccountForm.password = event.target.value;
756     i.setState(i.state);
757   }
758
759   handleLogoutClick(i: User) {
760     UserService.Instance.logout();
761     i.context.router.history.push('/');
762   }
763
764   handleDeleteAccount(i: User, event: any) {
765     event.preventDefault();
766     i.state.deleteAccountLoading = true;
767     i.setState(i.state);
768
769     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
770   }
771
772   parseMessage(msg: any) {
773     console.log(msg);
774     let op: UserOperation = msgOp(msg);
775     if (msg.error) {
776       alert(i18n.t(msg.error));
777       this.state.deleteAccountLoading = false;
778       this.setState(this.state);
779       return;
780     } else if (op == UserOperation.GetUserDetails) {
781       let res: UserDetailsResponse = msg;
782       this.state.user = res.user;
783       this.state.comments = res.comments;
784       this.state.follows = res.follows;
785       this.state.moderates = res.moderates;
786       this.state.posts = res.posts;
787       this.state.admins = res.admins;
788       this.state.loading = false;
789       if (this.isCurrentUser) {
790         this.state.userSettingsForm.show_nsfw =
791           UserService.Instance.user.show_nsfw;
792         this.state.userSettingsForm.theme = UserService.Instance.user.theme
793           ? UserService.Instance.user.theme
794           : 'darkly';
795         this.state.userSettingsForm.default_sort_type =
796           UserService.Instance.user.default_sort_type;
797         this.state.userSettingsForm.default_listing_type =
798           UserService.Instance.user.default_listing_type;
799         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
800       }
801       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
802       window.scrollTo(0, 0);
803       this.setState(this.state);
804     } else if (op == UserOperation.EditComment) {
805       let res: CommentResponse = msg;
806
807       let found = this.state.comments.find(c => c.id == res.comment.id);
808       found.content = res.comment.content;
809       found.updated = res.comment.updated;
810       found.removed = res.comment.removed;
811       found.deleted = res.comment.deleted;
812       found.upvotes = res.comment.upvotes;
813       found.downvotes = res.comment.downvotes;
814       found.score = res.comment.score;
815
816       this.setState(this.state);
817     } else if (op == UserOperation.CreateComment) {
818       // let res: CommentResponse = msg;
819       alert(i18n.t('reply_sent'));
820       // this.state.comments.unshift(res.comment); // TODO do this right
821       // this.setState(this.state);
822     } else if (op == UserOperation.SaveComment) {
823       let res: CommentResponse = msg;
824       let found = this.state.comments.find(c => c.id == res.comment.id);
825       found.saved = res.comment.saved;
826       this.setState(this.state);
827     } else if (op == UserOperation.CreateCommentLike) {
828       let res: CommentResponse = msg;
829       let found: Comment = this.state.comments.find(
830         c => c.id === res.comment.id
831       );
832       found.score = res.comment.score;
833       found.upvotes = res.comment.upvotes;
834       found.downvotes = res.comment.downvotes;
835       if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
836       this.setState(this.state);
837     } else if (op == UserOperation.BanUser) {
838       let res: BanUserResponse = msg;
839       this.state.comments
840         .filter(c => c.creator_id == res.user.id)
841         .forEach(c => (c.banned = res.banned));
842       this.state.posts
843         .filter(c => c.creator_id == res.user.id)
844         .forEach(c => (c.banned = res.banned));
845       this.setState(this.state);
846     } else if (op == UserOperation.AddAdmin) {
847       let res: AddAdminResponse = msg;
848       this.state.admins = res.admins;
849       this.setState(this.state);
850     } else if (op == UserOperation.SaveUserSettings) {
851       this.state = this.emptyState;
852       this.state.userSettingsLoading = false;
853       this.setState(this.state);
854       let res: LoginResponse = msg;
855       UserService.Instance.login(res);
856     } else if (op == UserOperation.DeleteAccount) {
857       this.state.deleteAccountLoading = false;
858       this.state.deleteAccountShowConfirm = false;
859       this.setState(this.state);
860       this.context.router.history.push('/');
861     }
862   }
863 }