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