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