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