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