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