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