]> Untitled Git - lemmy-ui.git/blob - src/shared/components/search.tsx
Remove buggy navbar search. Fixes #921 (#950)
[lemmy-ui.git] / src / shared / components / search.tsx
1 import { Component, linkEvent } from "inferno";
2 import {
3   CommentResponse,
4   CommentView,
5   CommunityView,
6   GetCommunity,
7   GetCommunityResponse,
8   GetPersonDetails,
9   GetPersonDetailsResponse,
10   GetSiteResponse,
11   ListCommunities,
12   ListCommunitiesResponse,
13   ListingType,
14   PersonViewSafe,
15   PostResponse,
16   PostView,
17   ResolveObject,
18   ResolveObjectResponse,
19   Search as SearchForm,
20   SearchResponse,
21   SearchType,
22   SortType,
23   UserOperation,
24   wsJsonToRes,
25   wsUserOp,
26 } from "lemmy-js-client";
27 import { Subscription } from "rxjs";
28 import { i18n } from "../i18next";
29 import { CommentViewType, InitialFetchRequest } from "../interfaces";
30 import { WebSocketService } from "../services";
31 import {
32   capitalizeFirstLetter,
33   choicesConfig,
34   commentsToFlatNodes,
35   communitySelectName,
36   communityToChoice,
37   createCommentLikeRes,
38   createPostLikeFindRes,
39   debounce,
40   enableDownvotes,
41   enableNsfw,
42   fetchCommunities,
43   fetchLimit,
44   fetchUsers,
45   isBrowser,
46   myAuth,
47   numToSI,
48   personSelectName,
49   personToChoice,
50   pushNotNull,
51   restoreScrollPosition,
52   routeListingTypeToEnum,
53   routeSearchTypeToEnum,
54   routeSortTypeToEnum,
55   saveScrollPosition,
56   setIsoData,
57   showLocal,
58   toast,
59   wsClient,
60   wsSubscribe,
61 } from "../utils";
62 import { CommentNodes } from "./comment/comment-nodes";
63 import { HtmlTags } from "./common/html-tags";
64 import { Spinner } from "./common/icon";
65 import { ListingTypeSelect } from "./common/listing-type-select";
66 import { Paginator } from "./common/paginator";
67 import { SortSelect } from "./common/sort-select";
68 import { CommunityLink } from "./community/community-link";
69 import { PersonListing } from "./person/person-listing";
70 import { PostListing } from "./post/post-listing";
71
72 var Choices: any;
73 if (isBrowser()) {
74   Choices = require("choices.js");
75 }
76
77 interface SearchProps {
78   q?: string;
79   type_: SearchType;
80   sort: SortType;
81   listingType: ListingType;
82   communityId: number;
83   creatorId: number;
84   page: number;
85 }
86
87 interface SearchState {
88   q?: string;
89   type_: SearchType;
90   sort: SortType;
91   listingType: ListingType;
92   communityId: number;
93   creatorId: number;
94   page: number;
95   searchResponse?: SearchResponse;
96   communities: CommunityView[];
97   creatorDetails?: GetPersonDetailsResponse;
98   loading: boolean;
99   siteRes: GetSiteResponse;
100   searchText?: string;
101   resolveObjectResponse?: ResolveObjectResponse;
102 }
103
104 interface UrlParams {
105   q?: string;
106   type_?: SearchType;
107   sort?: SortType;
108   listingType?: ListingType;
109   communityId?: number;
110   creatorId?: number;
111   page?: number;
112 }
113
114 interface Combined {
115   type_: string;
116   data: CommentView | PostView | CommunityView | PersonViewSafe;
117   published: string;
118 }
119
120 export class Search extends Component<any, SearchState> {
121   private isoData = setIsoData(this.context);
122   private communityChoices: any;
123   private creatorChoices: any;
124   private subscription?: Subscription;
125   state: SearchState = {
126     q: Search.getSearchQueryFromProps(this.props.match.params.q),
127     type_: Search.getSearchTypeFromProps(this.props.match.params.type),
128     sort: Search.getSortTypeFromProps(this.props.match.params.sort),
129     listingType: Search.getListingTypeFromProps(
130       this.props.match.params.listing_type
131     ),
132     page: Search.getPageFromProps(this.props.match.params.page),
133     searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
134     communityId: Search.getCommunityIdFromProps(
135       this.props.match.params.community_id
136     ),
137     creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
138     loading: false,
139     siteRes: this.isoData.site_res,
140     communities: [],
141   };
142
143   static getSearchQueryFromProps(q?: string): string | undefined {
144     return q ? decodeURIComponent(q) : undefined;
145   }
146
147   static getSearchTypeFromProps(type_: string): SearchType {
148     return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
149   }
150
151   static getSortTypeFromProps(sort: string): SortType {
152     return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
153   }
154
155   static getListingTypeFromProps(listingType: string): ListingType {
156     return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
157   }
158
159   static getCommunityIdFromProps(id: string): number {
160     return id ? Number(id) : 0;
161   }
162
163   static getCreatorIdFromProps(id: string): number {
164     return id ? Number(id) : 0;
165   }
166
167   static getPageFromProps(page: string): number {
168     return page ? Number(page) : 1;
169   }
170
171   constructor(props: any, context: any) {
172     super(props, context);
173
174     this.handleSortChange = this.handleSortChange.bind(this);
175     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
176     this.handlePageChange = this.handlePageChange.bind(this);
177
178     this.parseMessage = this.parseMessage.bind(this);
179     this.subscription = wsSubscribe(this.parseMessage);
180
181     // Only fetch the data if coming from another route
182     if (this.isoData.path == this.context.router.route.match.url) {
183       let communityRes = this.isoData.routeData[0] as
184         | GetCommunityResponse
185         | undefined;
186       let communitiesRes = this.isoData.routeData[1] as
187         | ListCommunitiesResponse
188         | undefined;
189       // This can be single or multiple communities given
190       if (communitiesRes) {
191         this.state = {
192           ...this.state,
193           communities: communitiesRes.communities,
194         };
195       }
196
197       if (communityRes) {
198         this.state = {
199           ...this.state,
200           communities: [communityRes.community_view],
201         };
202       }
203
204       this.state = {
205         ...this.state,
206         creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse,
207       };
208
209       if (this.state.q != "") {
210         this.state = {
211           ...this.state,
212           searchResponse: this.isoData.routeData[3] as SearchResponse,
213           resolveObjectResponse: this.isoData
214             .routeData[4] as ResolveObjectResponse,
215           loading: false,
216         };
217       } else {
218         this.search();
219       }
220     } else {
221       this.fetchCommunities();
222
223       if (this.state.q) {
224         this.search();
225       }
226     }
227   }
228
229   componentWillUnmount() {
230     this.subscription?.unsubscribe();
231     saveScrollPosition(this.context);
232   }
233
234   componentDidMount() {
235     this.setupCommunityFilter();
236     this.setupCreatorFilter();
237   }
238
239   static getDerivedStateFromProps(props: any): SearchProps {
240     return {
241       q: Search.getSearchQueryFromProps(props.match.params.q),
242       type_: Search.getSearchTypeFromProps(props.match.params.type),
243       sort: Search.getSortTypeFromProps(props.match.params.sort),
244       listingType: Search.getListingTypeFromProps(
245         props.match.params.listing_type
246       ),
247       communityId: Search.getCommunityIdFromProps(
248         props.match.params.community_id
249       ),
250       creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
251       page: Search.getPageFromProps(props.match.params.page),
252     };
253   }
254
255   fetchCommunities() {
256     let listCommunitiesForm: ListCommunities = {
257       type_: ListingType.All,
258       sort: SortType.TopAll,
259       limit: fetchLimit,
260       auth: myAuth(false),
261     };
262     WebSocketService.Instance.send(
263       wsClient.listCommunities(listCommunitiesForm)
264     );
265   }
266
267   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
268     let pathSplit = req.path.split("/");
269     let promises: Promise<any>[] = [];
270     let auth = req.auth;
271
272     let communityId = this.getCommunityIdFromProps(pathSplit[11]);
273     let community_id = communityId == 0 ? undefined : communityId;
274     if (community_id) {
275       let getCommunityForm: GetCommunity = {
276         id: community_id,
277         auth,
278       };
279       promises.push(req.client.getCommunity(getCommunityForm));
280       promises.push(Promise.resolve());
281     } else {
282       let listCommunitiesForm: ListCommunities = {
283         type_: ListingType.All,
284         sort: SortType.TopAll,
285         limit: fetchLimit,
286         auth: req.auth,
287       };
288       promises.push(Promise.resolve());
289       promises.push(req.client.listCommunities(listCommunitiesForm));
290     }
291
292     let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
293     let creator_id = creatorId == 0 ? undefined : creatorId;
294     if (creator_id) {
295       let getCreatorForm: GetPersonDetails = {
296         person_id: creator_id,
297         auth: req.auth,
298       };
299       promises.push(req.client.getPersonDetails(getCreatorForm));
300     } else {
301       promises.push(Promise.resolve());
302     }
303
304     let q = this.getSearchQueryFromProps(pathSplit[3]);
305
306     if (q) {
307       let form: SearchForm = {
308         q,
309         community_id,
310         creator_id,
311         type_: this.getSearchTypeFromProps(pathSplit[5]),
312         sort: this.getSortTypeFromProps(pathSplit[7]),
313         listing_type: this.getListingTypeFromProps(pathSplit[9]),
314         page: this.getPageFromProps(pathSplit[15]),
315         limit: fetchLimit,
316         auth: req.auth,
317       };
318
319       let resolveObjectForm: ResolveObject = {
320         q,
321         auth: req.auth,
322       };
323
324       if (form.q != "") {
325         promises.push(req.client.search(form));
326         promises.push(req.client.resolveObject(resolveObjectForm));
327       } else {
328         promises.push(Promise.resolve());
329         promises.push(Promise.resolve());
330       }
331     }
332
333     return promises;
334   }
335
336   componentDidUpdate(_: any, lastState: SearchState) {
337     if (
338       lastState.q !== this.state.q ||
339       lastState.type_ !== this.state.type_ ||
340       lastState.sort !== this.state.sort ||
341       lastState.listingType !== this.state.listingType ||
342       lastState.communityId !== this.state.communityId ||
343       lastState.creatorId !== this.state.creatorId ||
344       lastState.page !== this.state.page
345     ) {
346       if (this.state.q) {
347         this.setState({
348           loading: true,
349           searchText: this.state.q,
350         });
351         this.search();
352       }
353     }
354   }
355
356   get documentTitle(): string {
357     let siteName = this.state.siteRes.site_view.site.name;
358     return this.state.q
359       ? `${i18n.t("search")} - ${this.state.q} - ${siteName}`
360       : `${i18n.t("search")} - ${siteName}`;
361   }
362
363   render() {
364     return (
365       <div className="container-lg">
366         <HtmlTags
367           title={this.documentTitle}
368           path={this.context.router.route.match.url}
369         />
370         <h5>{i18n.t("search")}</h5>
371         {this.selects()}
372         {this.searchForm()}
373         {this.state.type_ == SearchType.All && this.all()}
374         {this.state.type_ == SearchType.Comments && this.comments()}
375         {this.state.type_ == SearchType.Posts && this.posts()}
376         {this.state.type_ == SearchType.Communities && this.communities()}
377         {this.state.type_ == SearchType.Users && this.users()}
378         {this.state.type_ == SearchType.Url && this.posts()}
379         {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
380         <Paginator page={this.state.page} onChange={this.handlePageChange} />
381       </div>
382     );
383   }
384
385   searchForm() {
386     return (
387       <form
388         className="form-inline"
389         onSubmit={linkEvent(this, this.handleSearchSubmit)}
390       >
391         <input
392           type="text"
393           className="form-control mr-2 mb-2"
394           value={this.state.searchText}
395           placeholder={`${i18n.t("search")}...`}
396           aria-label={i18n.t("search")}
397           onInput={linkEvent(this, this.handleQChange)}
398           required
399           minLength={1}
400         />
401         <button type="submit" className="btn btn-secondary mr-2 mb-2">
402           {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
403         </button>
404       </form>
405     );
406   }
407
408   selects() {
409     return (
410       <div className="mb-2">
411         <select
412           value={this.state.type_}
413           onChange={linkEvent(this, this.handleTypeChange)}
414           className="custom-select w-auto mb-2"
415           aria-label={i18n.t("type")}
416         >
417           <option disabled aria-hidden="true">
418             {i18n.t("type")}
419           </option>
420           <option value={SearchType.All}>{i18n.t("all")}</option>
421           <option value={SearchType.Comments}>{i18n.t("comments")}</option>
422           <option value={SearchType.Posts}>{i18n.t("posts")}</option>
423           <option value={SearchType.Communities}>
424             {i18n.t("communities")}
425           </option>
426           <option value={SearchType.Users}>{i18n.t("users")}</option>
427           <option value={SearchType.Url}>{i18n.t("url")}</option>
428         </select>
429         <span className="ml-2">
430           <ListingTypeSelect
431             type_={this.state.listingType}
432             showLocal={showLocal(this.isoData)}
433             showSubscribed
434             onChange={this.handleListingTypeChange}
435           />
436         </span>
437         <span className="ml-2">
438           <SortSelect
439             sort={this.state.sort}
440             onChange={this.handleSortChange}
441             hideHot
442             hideMostComments
443           />
444         </span>
445         <div className="form-row">
446           {this.state.communities.length > 0 && this.communityFilter()}
447           {this.creatorFilter()}
448         </div>
449       </div>
450     );
451   }
452
453   postViewToCombined(postView: PostView): Combined {
454     return {
455       type_: "posts",
456       data: postView,
457       published: postView.post.published,
458     };
459   }
460
461   commentViewToCombined(commentView: CommentView): Combined {
462     return {
463       type_: "comments",
464       data: commentView,
465       published: commentView.comment.published,
466     };
467   }
468
469   communityViewToCombined(communityView: CommunityView): Combined {
470     return {
471       type_: "communities",
472       data: communityView,
473       published: communityView.community.published,
474     };
475   }
476
477   personViewSafeToCombined(personViewSafe: PersonViewSafe): Combined {
478     return {
479       type_: "users",
480       data: personViewSafe,
481       published: personViewSafe.person.published,
482     };
483   }
484
485   buildCombined(): Combined[] {
486     let combined: Combined[] = [];
487
488     let resolveRes = this.state.resolveObjectResponse;
489     // Push the possible resolve / federated objects first
490     if (resolveRes) {
491       let resolveComment = resolveRes.comment;
492       if (resolveComment) {
493         combined.push(this.commentViewToCombined(resolveComment));
494       }
495       let resolvePost = resolveRes.post;
496       if (resolvePost) {
497         combined.push(this.postViewToCombined(resolvePost));
498       }
499       let resolveCommunity = resolveRes.community;
500       if (resolveCommunity) {
501         combined.push(this.communityViewToCombined(resolveCommunity));
502       }
503       let resolveUser = resolveRes.person;
504       if (resolveUser) {
505         combined.push(this.personViewSafeToCombined(resolveUser));
506       }
507     }
508
509     // Push the search results
510     let searchRes = this.state.searchResponse;
511     if (searchRes) {
512       pushNotNull(
513         combined,
514         searchRes.comments?.map(e => this.commentViewToCombined(e))
515       );
516       pushNotNull(
517         combined,
518         searchRes.posts?.map(e => this.postViewToCombined(e))
519       );
520       pushNotNull(
521         combined,
522         searchRes.communities?.map(e => this.communityViewToCombined(e))
523       );
524       pushNotNull(
525         combined,
526         searchRes.users?.map(e => this.personViewSafeToCombined(e))
527       );
528     }
529
530     // Sort it
531     if (this.state.sort == SortType.New) {
532       combined.sort((a, b) => b.published.localeCompare(a.published));
533     } else {
534       combined.sort(
535         (a, b) =>
536           ((b.data as CommentView | PostView).counts.score |
537             (b.data as CommunityView).counts.subscribers |
538             (b.data as PersonViewSafe).counts.comment_score) -
539           ((a.data as CommentView | PostView).counts.score |
540             (a.data as CommunityView).counts.subscribers |
541             (a.data as PersonViewSafe).counts.comment_score)
542       );
543     }
544     return combined;
545   }
546
547   all() {
548     let combined = this.buildCombined();
549     return (
550       <div>
551         {combined.map(i => (
552           <div key={i.published} className="row">
553             <div className="col-12">
554               {i.type_ == "posts" && (
555                 <PostListing
556                   key={(i.data as PostView).post.id}
557                   post_view={i.data as PostView}
558                   showCommunity
559                   enableDownvotes={enableDownvotes(this.state.siteRes)}
560                   enableNsfw={enableNsfw(this.state.siteRes)}
561                   allLanguages={this.state.siteRes.all_languages}
562                   siteLanguages={this.state.siteRes.discussion_languages}
563                   viewOnly
564                 />
565               )}
566               {i.type_ == "comments" && (
567                 <CommentNodes
568                   key={(i.data as CommentView).comment.id}
569                   nodes={[
570                     {
571                       comment_view: i.data as CommentView,
572                       children: [],
573                       depth: 0,
574                     },
575                   ]}
576                   viewType={CommentViewType.Flat}
577                   viewOnly
578                   locked
579                   noIndent
580                   enableDownvotes={enableDownvotes(this.state.siteRes)}
581                   allLanguages={this.state.siteRes.all_languages}
582                   siteLanguages={this.state.siteRes.discussion_languages}
583                 />
584               )}
585               {i.type_ == "communities" && (
586                 <div>{this.communityListing(i.data as CommunityView)}</div>
587               )}
588               {i.type_ == "users" && (
589                 <div>{this.personListing(i.data as PersonViewSafe)}</div>
590               )}
591             </div>
592           </div>
593         ))}
594       </div>
595     );
596   }
597
598   comments() {
599     let comments: CommentView[] = [];
600     pushNotNull(comments, this.state.resolveObjectResponse?.comment);
601     pushNotNull(comments, this.state.searchResponse?.comments);
602
603     return (
604       <CommentNodes
605         nodes={commentsToFlatNodes(comments)}
606         viewType={CommentViewType.Flat}
607         viewOnly
608         locked
609         noIndent
610         enableDownvotes={enableDownvotes(this.state.siteRes)}
611         allLanguages={this.state.siteRes.all_languages}
612         siteLanguages={this.state.siteRes.discussion_languages}
613       />
614     );
615   }
616
617   posts() {
618     let posts: PostView[] = [];
619
620     pushNotNull(posts, this.state.resolveObjectResponse?.post);
621     pushNotNull(posts, this.state.searchResponse?.posts);
622
623     return (
624       <>
625         {posts.map(pv => (
626           <div key={pv.post.id} className="row">
627             <div className="col-12">
628               <PostListing
629                 post_view={pv}
630                 showCommunity
631                 enableDownvotes={enableDownvotes(this.state.siteRes)}
632                 enableNsfw={enableNsfw(this.state.siteRes)}
633                 allLanguages={this.state.siteRes.all_languages}
634                 siteLanguages={this.state.siteRes.discussion_languages}
635                 viewOnly
636               />
637             </div>
638           </div>
639         ))}
640       </>
641     );
642   }
643
644   communities() {
645     let communities: CommunityView[] = [];
646
647     pushNotNull(communities, this.state.resolveObjectResponse?.community);
648     pushNotNull(communities, this.state.searchResponse?.communities);
649
650     return (
651       <>
652         {communities.map(cv => (
653           <div key={cv.community.id} className="row">
654             <div className="col-12">{this.communityListing(cv)}</div>
655           </div>
656         ))}
657       </>
658     );
659   }
660
661   users() {
662     let users: PersonViewSafe[] = [];
663
664     pushNotNull(users, this.state.resolveObjectResponse?.person);
665     pushNotNull(users, this.state.searchResponse?.users);
666
667     return (
668       <>
669         {users.map(pvs => (
670           <div key={pvs.person.id} className="row">
671             <div className="col-12">{this.personListing(pvs)}</div>
672           </div>
673         ))}
674       </>
675     );
676   }
677
678   communityListing(community_view: CommunityView) {
679     return (
680       <>
681         <span>
682           <CommunityLink community={community_view.community} />
683         </span>
684         <span>{` -
685         ${i18n.t("number_of_subscribers", {
686           count: community_view.counts.subscribers,
687           formattedCount: numToSI(community_view.counts.subscribers),
688         })}
689       `}</span>
690       </>
691     );
692   }
693
694   personListing(person_view: PersonViewSafe) {
695     return (
696       <>
697         <span>
698           <PersonListing person={person_view.person} showApubName />
699         </span>
700         <span>{` - ${i18n.t("number_of_comments", {
701           count: person_view.counts.comment_count,
702           formattedCount: numToSI(person_view.counts.comment_count),
703         })}`}</span>
704       </>
705     );
706   }
707
708   communityFilter() {
709     return (
710       <div className="form-group col-sm-6">
711         <label className="col-form-label" htmlFor="community-filter">
712           {i18n.t("community")}
713         </label>
714         <div>
715           <select
716             className="form-control"
717             id="community-filter"
718             value={this.state.communityId}
719           >
720             <option value="0">{i18n.t("all")}</option>
721             {this.state.communities.map(cv => (
722               <option key={cv.community.id} value={cv.community.id}>
723                 {communitySelectName(cv)}
724               </option>
725             ))}
726           </select>
727         </div>
728       </div>
729     );
730   }
731
732   creatorFilter() {
733     let creatorPv = this.state.creatorDetails?.person_view;
734     return (
735       <div className="form-group col-sm-6">
736         <label className="col-form-label" htmlFor="creator-filter">
737           {capitalizeFirstLetter(i18n.t("creator"))}
738         </label>
739         <div>
740           <select
741             className="form-control"
742             id="creator-filter"
743             value={this.state.creatorId}
744           >
745             <option value="0">{i18n.t("all")}</option>
746             {creatorPv && (
747               <option value={creatorPv.person.id}>
748                 {personSelectName(creatorPv)}
749               </option>
750             )}
751           </select>
752         </div>
753       </div>
754     );
755   }
756
757   resultsCount(): number {
758     let r = this.state.searchResponse;
759
760     let searchCount = r
761       ? r.posts?.length +
762         r.comments?.length +
763         r.communities?.length +
764         r.users?.length
765       : 0;
766
767     let resolveRes = this.state.resolveObjectResponse;
768     let resObjCount = resolveRes
769       ? resolveRes.post ||
770         resolveRes.person ||
771         resolveRes.community ||
772         resolveRes.comment
773         ? 1
774         : 0
775       : 0;
776
777     return resObjCount + searchCount;
778   }
779
780   handlePageChange(page: number) {
781     this.updateUrl({ page });
782   }
783
784   search() {
785     let community_id =
786       this.state.communityId == 0 ? undefined : this.state.communityId;
787     let creator_id =
788       this.state.creatorId == 0 ? undefined : this.state.creatorId;
789
790     let auth = myAuth(false);
791     if (this.state.q && this.state.q != "") {
792       let form: SearchForm = {
793         q: this.state.q,
794         community_id,
795         creator_id,
796         type_: this.state.type_,
797         sort: this.state.sort,
798         listing_type: this.state.listingType,
799         page: this.state.page,
800         limit: fetchLimit,
801         auth,
802       };
803
804       let resolveObjectForm: ResolveObject = {
805         q: this.state.q,
806         auth,
807       };
808
809       this.setState({
810         searchResponse: undefined,
811         resolveObjectResponse: undefined,
812         loading: true,
813       });
814       WebSocketService.Instance.send(wsClient.search(form));
815       WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
816     }
817   }
818
819   setupCommunityFilter() {
820     if (isBrowser()) {
821       let selectId: any = document.getElementById("community-filter");
822       if (selectId) {
823         this.communityChoices = new Choices(selectId, choicesConfig);
824         this.communityChoices.passedElement.element.addEventListener(
825           "choice",
826           (e: any) => {
827             this.handleCommunityFilterChange(Number(e.detail.choice.value));
828           },
829           false
830         );
831         this.communityChoices.passedElement.element.addEventListener(
832           "search",
833           debounce(async (e: any) => {
834             try {
835               let communities = (await fetchCommunities(e.detail.value))
836                 .communities;
837               let choices = communities.map(cv => communityToChoice(cv));
838               choices.unshift({ value: "0", label: i18n.t("all") });
839               this.communityChoices.setChoices(choices, "value", "label", true);
840             } catch (err) {
841               console.error(err);
842             }
843           }),
844           false
845         );
846       }
847     }
848   }
849
850   setupCreatorFilter() {
851     if (isBrowser()) {
852       let selectId: any = document.getElementById("creator-filter");
853       if (selectId) {
854         this.creatorChoices = new Choices(selectId, choicesConfig);
855         this.creatorChoices.passedElement.element.addEventListener(
856           "choice",
857           (e: any) => {
858             this.handleCreatorFilterChange(Number(e.detail.choice.value));
859           },
860           false
861         );
862         this.creatorChoices.passedElement.element.addEventListener(
863           "search",
864           debounce(async (e: any) => {
865             try {
866               let creators = (await fetchUsers(e.detail.value)).users;
867               let choices = creators.map(pvs => personToChoice(pvs));
868               choices.unshift({ value: "0", label: i18n.t("all") });
869               this.creatorChoices.setChoices(choices, "value", "label", true);
870             } catch (err) {
871               console.log(err);
872             }
873           }),
874           false
875         );
876       }
877     }
878   }
879
880   handleSortChange(val: SortType) {
881     this.updateUrl({ sort: val, page: 1 });
882   }
883
884   handleTypeChange(i: Search, event: any) {
885     i.updateUrl({
886       type_: SearchType[event.target.value],
887       page: 1,
888     });
889   }
890
891   handleListingTypeChange(val: ListingType) {
892     this.updateUrl({
893       listingType: val,
894       page: 1,
895     });
896   }
897
898   handleCommunityFilterChange(communityId: number) {
899     this.updateUrl({
900       communityId,
901       page: 1,
902     });
903   }
904
905   handleCreatorFilterChange(creatorId: number) {
906     this.updateUrl({
907       creatorId,
908       page: 1,
909     });
910   }
911
912   handleSearchSubmit(i: Search, event: any) {
913     event.preventDefault();
914     i.updateUrl({
915       q: i.state.searchText,
916       type_: i.state.type_,
917       listingType: i.state.listingType,
918       communityId: i.state.communityId,
919       creatorId: i.state.creatorId,
920       sort: i.state.sort,
921       page: i.state.page,
922     });
923   }
924
925   handleQChange(i: Search, event: any) {
926     i.setState({ searchText: event.target.value });
927   }
928
929   updateUrl(paramUpdates: UrlParams) {
930     const qStr = paramUpdates.q || this.state.q;
931     const qStrEncoded = encodeURIComponent(qStr || "");
932     const typeStr = paramUpdates.type_ || this.state.type_;
933     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
934     const sortStr = paramUpdates.sort || this.state.sort;
935     const communityId =
936       paramUpdates.communityId == 0
937         ? 0
938         : paramUpdates.communityId || this.state.communityId;
939     const creatorId =
940       paramUpdates.creatorId == 0
941         ? 0
942         : paramUpdates.creatorId || this.state.creatorId;
943     const page = paramUpdates.page || this.state.page;
944     this.props.history.push(
945       `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
946     );
947   }
948
949   parseMessage(msg: any) {
950     console.log(msg);
951     let op = wsUserOp(msg);
952     if (msg.error) {
953       if (msg.error == "couldnt_find_object") {
954         this.setState({
955           resolveObjectResponse: {},
956         });
957         this.checkFinishedLoading();
958       } else {
959         toast(i18n.t(msg.error), "danger");
960         return;
961       }
962     } else if (op == UserOperation.Search) {
963       let data = wsJsonToRes<SearchResponse>(msg);
964       this.setState({ searchResponse: data });
965       window.scrollTo(0, 0);
966       this.checkFinishedLoading();
967       restoreScrollPosition(this.context);
968     } else if (op == UserOperation.CreateCommentLike) {
969       let data = wsJsonToRes<CommentResponse>(msg);
970       createCommentLikeRes(
971         data.comment_view,
972         this.state.searchResponse?.comments
973       );
974       this.setState(this.state);
975     } else if (op == UserOperation.CreatePostLike) {
976       let data = wsJsonToRes<PostResponse>(msg);
977       createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
978       this.setState(this.state);
979     } else if (op == UserOperation.ListCommunities) {
980       let data = wsJsonToRes<ListCommunitiesResponse>(msg);
981       this.setState({ communities: data.communities });
982       this.setupCommunityFilter();
983     } else if (op == UserOperation.ResolveObject) {
984       let data = wsJsonToRes<ResolveObjectResponse>(msg);
985       this.setState({ resolveObjectResponse: data });
986       this.checkFinishedLoading();
987     }
988   }
989
990   checkFinishedLoading() {
991     if (this.state.searchResponse && this.state.resolveObjectResponse) {
992       this.setState({ loading: false });
993     }
994   }
995 }