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