]> Untitled Git - lemmy-ui.git/blob - src/shared/components/search.tsx
Fix I18 next circular reference
[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 (!this.state.isIsomorphic) {
336       const promises = [this.fetchCommunities()];
337       if (this.state.searchText) {
338         promises.push(this.search());
339       }
340
341       await Promise.all(promises);
342     }
343   }
344
345   async fetchCommunities() {
346     this.setState({ communitiesRes: { state: "loading" } });
347     this.setState({
348       communitiesRes: await HttpService.client.listCommunities({
349         type_: defaultListingType,
350         sort: defaultSortType,
351         limit: fetchLimit,
352         auth: myAuth(),
353       }),
354     });
355   }
356
357   componentWillUnmount() {
358     saveScrollPosition(this.context);
359   }
360
361   static async fetchInitialData({
362     client,
363     auth,
364     query: { communityId, creatorId, q, type, sort, listingType, page },
365   }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
366     const community_id = getIdFromString(communityId);
367     let communityResponse: RequestState<GetCommunityResponse> = {
368       state: "empty",
369     };
370     let listCommunitiesResponse: RequestState<ListCommunitiesResponse> = {
371       state: "empty",
372     };
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: RequestState<GetPersonDetailsResponse> = {
395       state: "empty",
396     };
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> = { state: "empty" };
409     let resolveObjectResponse: RequestState<ResolveObjectResponse> = {
410       state: "empty",
411     };
412
413     if (query) {
414       const form: SearchForm = {
415         q: query,
416         community_id,
417         creator_id,
418         type_: getSearchTypeFromQuery(type),
419         sort: getSortTypeFromQuery(sort),
420         listing_type: getListingTypeFromQuery(listingType),
421         page: getPageFromString(page),
422         limit: fetchLimit,
423         auth,
424       };
425
426       if (query !== "") {
427         searchResponse = await client.search(form);
428         if (auth) {
429           const resolveObjectForm: ResolveObject = {
430             q: query,
431             auth,
432           };
433           resolveObjectResponse = await client.resolveObject(resolveObjectForm);
434         }
435       }
436     }
437
438     return {
439       communityResponse,
440       creatorDetailsResponse,
441       listCommunitiesResponse,
442       resolveObjectResponse,
443       searchResponse,
444     };
445   }
446
447   get documentTitle(): string {
448     const { q } = getSearchQueryParams();
449     const name = this.state.siteRes.site_view.site.name;
450     return `${I18NextService.i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`;
451   }
452
453   render() {
454     const { type, page } = getSearchQueryParams();
455
456     return (
457       <div className="search container-lg">
458         <HtmlTags
459           title={this.documentTitle}
460           path={this.context.router.route.match.url}
461         />
462         <h5>{I18NextService.i18n.t("search")}</h5>
463         {this.selects}
464         {this.searchForm}
465         {this.displayResults(type)}
466         {this.resultsCount === 0 &&
467           this.state.searchRes.state === "success" && (
468             <span>{I18NextService.i18n.t("no_results")}</span>
469           )}
470         <Paginator page={page} onChange={this.handlePageChange} />
471       </div>
472     );
473   }
474
475   displayResults(type: SearchType) {
476     switch (type) {
477       case "All":
478         return this.all;
479       case "Comments":
480         return this.comments;
481       case "Posts":
482       case "Url":
483         return this.posts;
484       case "Communities":
485         return this.communities;
486       case "Users":
487         return this.users;
488       default:
489         return <></>;
490     }
491   }
492
493   get searchForm() {
494     return (
495       <form className="row" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
496         <div className="col-auto">
497           <input
498             type="text"
499             className="form-control me-2 mb-2 col-sm-8"
500             value={this.state.searchText}
501             placeholder={`${I18NextService.i18n.t("search")}...`}
502             aria-label={I18NextService.i18n.t("search")}
503             onInput={linkEvent(this, this.handleQChange)}
504             required
505             minLength={1}
506           />
507         </div>
508         <div className="col-auto">
509           <button type="submit" className="btn btn-secondary mb-2">
510             {this.state.searchRes.state === "loading" ? (
511               <Spinner />
512             ) : (
513               <span>{I18NextService.i18n.t("search")}</span>
514             )}
515           </button>
516         </div>
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="form-select d-inline-block w-auto mb-2"
542           aria-label={I18NextService.i18n.t("type")}
543         >
544           <option disabled aria-hidden="true">
545             {I18NextService.i18n.t("type")}
546           </option>
547           {searchTypes.map(option => (
548             <option value={option} key={option}>
549               {I18NextService.i18n.t(
550                 option.toString().toLowerCase() as NoOptionI18nKeys
551               )}
552             </option>
553           ))}
554         </select>
555         <span className="ms-2">
556           <ListingTypeSelect
557             type_={listingType}
558             showLocal={showLocal(this.isoData)}
559             showSubscribed
560             onChange={this.handleListingTypeChange}
561           />
562         </span>
563         <span className="ms-2">
564           <SortSelect
565             sort={sort}
566             onChange={this.handleSortChange}
567             hideHot
568             hideMostComments
569           />
570         </span>
571         <div className="row">
572           {hasCommunities && (
573             <Filter
574               filterType="community"
575               onChange={this.handleCommunityFilterChange}
576               onSearch={this.handleCommunitySearch}
577               options={communitySearchOptions}
578               value={communityId}
579               loading={searchCommunitiesLoading}
580             />
581           )}
582           <Filter
583             filterType="creator"
584             onChange={this.handleCreatorFilterChange}
585             onSearch={this.handleCreatorSearch}
586             options={creatorSearchOptions}
587             value={creatorId}
588             loading={searchCreatorLoading}
589           />
590         </div>
591       </div>
592     );
593   }
594
595   buildCombined(): Combined[] {
596     const combined: Combined[] = [];
597     const {
598       resolveObjectRes: resolveObjectResponse,
599       searchRes: searchResponse,
600     } = this.state;
601
602     // Push the possible resolve / federated objects first
603     if (resolveObjectResponse.state == "success") {
604       const { comment, post, community, person } = resolveObjectResponse.data;
605
606       if (comment) {
607         combined.push(commentViewToCombined(comment));
608       }
609       if (post) {
610         combined.push(postViewToCombined(post));
611       }
612       if (community) {
613         combined.push(communityViewToCombined(community));
614       }
615       if (person) {
616         combined.push(personViewSafeToCombined(person));
617       }
618     }
619
620     // Push the search results
621     if (searchResponse.state === "success") {
622       const { comments, posts, communities, users } = searchResponse.data;
623
624       combined.push(
625         ...[
626           ...(comments?.map(commentViewToCombined) ?? []),
627           ...(posts?.map(postViewToCombined) ?? []),
628           ...(communities?.map(communityViewToCombined) ?? []),
629           ...(users?.map(personViewSafeToCombined) ?? []),
630         ]
631       );
632     }
633
634     const { sort } = getSearchQueryParams();
635
636     // Sort it
637     if (sort === "New") {
638       combined.sort((a, b) => b.published.localeCompare(a.published));
639     } else {
640       combined.sort((a, b) =>
641         Number(
642           ((b.data as CommentView | PostView).counts.score |
643             (b.data as CommunityView).counts.subscribers |
644             (b.data as PersonView).counts.comment_score) -
645             ((a.data as CommentView | PostView).counts.score |
646               (a.data as CommunityView).counts.subscribers |
647               (a.data as PersonView).counts.comment_score)
648         )
649       );
650     }
651
652     return combined;
653   }
654
655   get all() {
656     const combined = this.buildCombined();
657
658     return (
659       <div>
660         {combined.map(i => (
661           <div key={i.published} className="row">
662             <div className="col-12">
663               {i.type_ === "posts" && (
664                 <PostListing
665                   key={(i.data as PostView).post.id}
666                   post_view={i.data as PostView}
667                   showCommunity
668                   enableDownvotes={enableDownvotes(this.state.siteRes)}
669                   enableNsfw={enableNsfw(this.state.siteRes)}
670                   allLanguages={this.state.siteRes.all_languages}
671                   siteLanguages={this.state.siteRes.discussion_languages}
672                   viewOnly
673                   // All of these are unused, since its view only
674                   onPostEdit={() => {}}
675                   onPostVote={() => {}}
676                   onPostReport={() => {}}
677                   onBlockPerson={() => {}}
678                   onLockPost={() => {}}
679                   onDeletePost={() => {}}
680                   onRemovePost={() => {}}
681                   onSavePost={() => {}}
682                   onFeaturePost={() => {}}
683                   onPurgePerson={() => {}}
684                   onPurgePost={() => {}}
685                   onBanPersonFromCommunity={() => {}}
686                   onBanPerson={() => {}}
687                   onAddModToCommunity={() => {}}
688                   onAddAdmin={() => {}}
689                   onTransferCommunity={() => {}}
690                 />
691               )}
692               {i.type_ === "comments" && (
693                 <CommentNodes
694                   key={(i.data as CommentView).comment.id}
695                   nodes={[
696                     {
697                       comment_view: i.data as CommentView,
698                       children: [],
699                       depth: 0,
700                     },
701                   ]}
702                   viewType={CommentViewType.Flat}
703                   viewOnly
704                   locked
705                   noIndent
706                   enableDownvotes={enableDownvotes(this.state.siteRes)}
707                   allLanguages={this.state.siteRes.all_languages}
708                   siteLanguages={this.state.siteRes.discussion_languages}
709                   // All of these are unused, since its viewonly
710                   finished={new Map()}
711                   onSaveComment={() => {}}
712                   onBlockPerson={() => {}}
713                   onDeleteComment={() => {}}
714                   onRemoveComment={() => {}}
715                   onCommentVote={() => {}}
716                   onCommentReport={() => {}}
717                   onDistinguishComment={() => {}}
718                   onAddModToCommunity={() => {}}
719                   onAddAdmin={() => {}}
720                   onTransferCommunity={() => {}}
721                   onPurgeComment={() => {}}
722                   onPurgePerson={() => {}}
723                   onCommentReplyRead={() => {}}
724                   onPersonMentionRead={() => {}}
725                   onBanPersonFromCommunity={() => {}}
726                   onBanPerson={() => {}}
727                   onCreateComment={() => Promise.resolve({ state: "empty" })}
728                   onEditComment={() => Promise.resolve({ state: "empty" })}
729                 />
730               )}
731               {i.type_ === "communities" && (
732                 <div>{communityListing(i.data as CommunityView)}</div>
733               )}
734               {i.type_ === "users" && (
735                 <div>{personListing(i.data as PersonView)}</div>
736               )}
737             </div>
738           </div>
739         ))}
740       </div>
741     );
742   }
743
744   get comments() {
745     const {
746       searchRes: searchResponse,
747       resolveObjectRes: resolveObjectResponse,
748       siteRes,
749     } = this.state;
750     const comments =
751       searchResponse.state === "success" ? searchResponse.data.comments : [];
752
753     if (
754       resolveObjectResponse.state === "success" &&
755       resolveObjectResponse.data.comment
756     ) {
757       comments.unshift(resolveObjectResponse.data.comment);
758     }
759
760     return (
761       <CommentNodes
762         nodes={commentsToFlatNodes(comments)}
763         viewType={CommentViewType.Flat}
764         viewOnly
765         locked
766         noIndent
767         enableDownvotes={enableDownvotes(siteRes)}
768         allLanguages={siteRes.all_languages}
769         siteLanguages={siteRes.discussion_languages}
770         // All of these are unused, since its viewonly
771         finished={new Map()}
772         onSaveComment={() => {}}
773         onBlockPerson={() => {}}
774         onDeleteComment={() => {}}
775         onRemoveComment={() => {}}
776         onCommentVote={() => {}}
777         onCommentReport={() => {}}
778         onDistinguishComment={() => {}}
779         onAddModToCommunity={() => {}}
780         onAddAdmin={() => {}}
781         onTransferCommunity={() => {}}
782         onPurgeComment={() => {}}
783         onPurgePerson={() => {}}
784         onCommentReplyRead={() => {}}
785         onPersonMentionRead={() => {}}
786         onBanPersonFromCommunity={() => {}}
787         onBanPerson={() => {}}
788         onCreateComment={() => Promise.resolve({ state: "empty" })}
789         onEditComment={() => Promise.resolve({ state: "empty" })}
790       />
791     );
792   }
793
794   get posts() {
795     const {
796       searchRes: searchResponse,
797       resolveObjectRes: resolveObjectResponse,
798       siteRes,
799     } = this.state;
800     const posts =
801       searchResponse.state === "success" ? searchResponse.data.posts : [];
802
803     if (
804       resolveObjectResponse.state === "success" &&
805       resolveObjectResponse.data.post
806     ) {
807       posts.unshift(resolveObjectResponse.data.post);
808     }
809
810     return (
811       <>
812         {posts.map(pv => (
813           <div key={pv.post.id} className="row">
814             <div className="col-12">
815               <PostListing
816                 post_view={pv}
817                 showCommunity
818                 enableDownvotes={enableDownvotes(siteRes)}
819                 enableNsfw={enableNsfw(siteRes)}
820                 allLanguages={siteRes.all_languages}
821                 siteLanguages={siteRes.discussion_languages}
822                 viewOnly
823                 // All of these are unused, since its view only
824                 onPostEdit={() => {}}
825                 onPostVote={() => {}}
826                 onPostReport={() => {}}
827                 onBlockPerson={() => {}}
828                 onLockPost={() => {}}
829                 onDeletePost={() => {}}
830                 onRemovePost={() => {}}
831                 onSavePost={() => {}}
832                 onFeaturePost={() => {}}
833                 onPurgePerson={() => {}}
834                 onPurgePost={() => {}}
835                 onBanPersonFromCommunity={() => {}}
836                 onBanPerson={() => {}}
837                 onAddModToCommunity={() => {}}
838                 onAddAdmin={() => {}}
839                 onTransferCommunity={() => {}}
840               />
841             </div>
842           </div>
843         ))}
844       </>
845     );
846   }
847
848   get communities() {
849     const {
850       searchRes: searchResponse,
851       resolveObjectRes: resolveObjectResponse,
852     } = this.state;
853     const communities =
854       searchResponse.state === "success" ? searchResponse.data.communities : [];
855
856     if (
857       resolveObjectResponse.state === "success" &&
858       resolveObjectResponse.data.community
859     ) {
860       communities.unshift(resolveObjectResponse.data.community);
861     }
862
863     return (
864       <>
865         {communities.map(cv => (
866           <div key={cv.community.id} className="row">
867             <div className="col-12">{communityListing(cv)}</div>
868           </div>
869         ))}
870       </>
871     );
872   }
873
874   get users() {
875     const {
876       searchRes: searchResponse,
877       resolveObjectRes: resolveObjectResponse,
878     } = this.state;
879     const users =
880       searchResponse.state === "success" ? searchResponse.data.users : [];
881
882     if (
883       resolveObjectResponse.state === "success" &&
884       resolveObjectResponse.data.person
885     ) {
886       users.unshift(resolveObjectResponse.data.person);
887     }
888
889     return (
890       <>
891         {users.map(pvs => (
892           <div key={pvs.person.id} className="row">
893             <div className="col-12">{personListing(pvs)}</div>
894           </div>
895         ))}
896       </>
897     );
898   }
899
900   get resultsCount(): number {
901     const { searchRes: r, resolveObjectRes: resolveRes } = this.state;
902
903     const searchCount =
904       r.state === "success"
905         ? r.data.posts.length +
906           r.data.comments.length +
907           r.data.communities.length +
908           r.data.users.length
909         : 0;
910
911     const resObjCount =
912       resolveRes.state === "success"
913         ? resolveRes.data.post ||
914           resolveRes.data.person ||
915           resolveRes.data.community ||
916           resolveRes.data.comment
917           ? 1
918           : 0
919         : 0;
920
921     return resObjCount + searchCount;
922   }
923
924   async search() {
925     const auth = myAuth();
926     const { searchText: q } = this.state;
927     const { communityId, creatorId, type, sort, listingType, page } =
928       getSearchQueryParams();
929
930     if (q) {
931       this.setState({ searchRes: { state: "loading" } });
932       this.setState({
933         searchRes: await HttpService.client.search({
934           q,
935           community_id: communityId ?? undefined,
936           creator_id: creatorId ?? undefined,
937           type_: type,
938           sort,
939           listing_type: listingType,
940           page,
941           limit: fetchLimit,
942           auth,
943         }),
944       });
945       window.scrollTo(0, 0);
946       restoreScrollPosition(this.context);
947
948       if (auth) {
949         this.setState({ resolveObjectRes: { state: "loading" } });
950         this.setState({
951           resolveObjectRes: await HttpService.client.resolveObject({
952             q,
953             auth,
954           }),
955         });
956       }
957     }
958   }
959
960   handleCreatorSearch = debounce(async (text: string) => {
961     const { creatorId } = getSearchQueryParams();
962     const { creatorSearchOptions } = this.state;
963     const newOptions: Choice[] = [];
964
965     this.setState({ searchCreatorLoading: true });
966
967     const selectedChoice = creatorSearchOptions.find(
968       choice => getIdFromString(choice.value) === creatorId
969     );
970
971     if (selectedChoice) {
972       newOptions.push(selectedChoice);
973     }
974
975     if (text.length > 0) {
976       newOptions.push(...(await fetchUsers(text)).map(personToChoice));
977     }
978
979     this.setState({
980       searchCreatorLoading: false,
981       creatorSearchOptions: newOptions,
982     });
983   });
984
985   handleCommunitySearch = debounce(async (text: string) => {
986     const { communityId } = getSearchQueryParams();
987     const { communitySearchOptions } = this.state;
988     this.setState({
989       searchCommunitiesLoading: true,
990     });
991
992     const newOptions: Choice[] = [];
993
994     const selectedChoice = communitySearchOptions.find(
995       choice => getIdFromString(choice.value) === communityId
996     );
997
998     if (selectedChoice) {
999       newOptions.push(selectedChoice);
1000     }
1001
1002     if (text.length > 0) {
1003       newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
1004     }
1005
1006     this.setState({
1007       searchCommunitiesLoading: false,
1008       communitySearchOptions: newOptions,
1009     });
1010   });
1011
1012   handleSortChange(sort: SortType) {
1013     this.updateUrl({ sort, page: 1 });
1014   }
1015
1016   handleTypeChange(i: Search, event: any) {
1017     const type = event.target.value as SearchType;
1018
1019     i.updateUrl({
1020       type,
1021       page: 1,
1022     });
1023   }
1024
1025   handlePageChange(page: number) {
1026     this.updateUrl({ page });
1027   }
1028
1029   handleListingTypeChange(listingType: ListingType) {
1030     this.updateUrl({
1031       listingType,
1032       page: 1,
1033     });
1034   }
1035
1036   handleCommunityFilterChange({ value }: Choice) {
1037     this.updateUrl({
1038       communityId: getIdFromString(value) ?? null,
1039       page: 1,
1040     });
1041   }
1042
1043   handleCreatorFilterChange({ value }: Choice) {
1044     this.updateUrl({
1045       creatorId: getIdFromString(value) ?? null,
1046       page: 1,
1047     });
1048   }
1049
1050   handleSearchSubmit(i: Search, event: any) {
1051     event.preventDefault();
1052
1053     i.updateUrl({
1054       q: i.state.searchText,
1055       page: 1,
1056     });
1057   }
1058
1059   handleQChange(i: Search, event: any) {
1060     i.setState({ searchText: event.target.value });
1061   }
1062
1063   async updateUrl({
1064     q,
1065     type,
1066     listingType,
1067     sort,
1068     communityId,
1069     creatorId,
1070     page,
1071   }: Partial<SearchProps>) {
1072     const {
1073       q: urlQ,
1074       type: urlType,
1075       listingType: urlListingType,
1076       communityId: urlCommunityId,
1077       sort: urlSort,
1078       creatorId: urlCreatorId,
1079       page: urlPage,
1080     } = getSearchQueryParams();
1081
1082     let query = q ?? this.state.searchText ?? urlQ;
1083
1084     if (query && query.length > 0) {
1085       query = encodeURIComponent(query);
1086     }
1087
1088     const queryParams: QueryParams<SearchProps> = {
1089       q: query,
1090       type: type ?? urlType,
1091       listingType: listingType ?? urlListingType,
1092       communityId: getUpdatedSearchId(communityId, urlCommunityId),
1093       creatorId: getUpdatedSearchId(creatorId, urlCreatorId),
1094       page: (page ?? urlPage).toString(),
1095       sort: sort ?? urlSort,
1096     };
1097
1098     this.props.history.push(`/search${getQueryString(queryParams)}`);
1099
1100     await this.search();
1101   }
1102 }