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