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