]> Untitled Git - lemmy-ui.git/blob - src/shared/components/search.tsx
Merge branch 'custom-emojis' of https://github.com/makotech222/lemmy-ui into makotech...
[lemmy-ui.git] / src / shared / components / search.tsx
1 import { Component, linkEvent } from "inferno";
2 import {
3   CommentResponse,
4   CommentView,
5   CommunityView,
6   GetCommunity,
7   GetCommunityResponse,
8   GetPersonDetails,
9   GetPersonDetailsResponse,
10   GetSiteResponse,
11   ListCommunities,
12   ListCommunitiesResponse,
13   ListingType,
14   PersonViewSafe,
15   PostResponse,
16   PostView,
17   ResolveObject,
18   ResolveObjectResponse,
19   Search as SearchForm,
20   SearchResponse,
21   SearchType,
22   SortType,
23   UserOperation,
24   wsJsonToRes,
25   wsUserOp,
26 } from "lemmy-js-client";
27 import { Subscription } from "rxjs";
28 import { i18n } from "../i18next";
29 import { CommentViewType, InitialFetchRequest } from "../interfaces";
30 import { WebSocketService } from "../services";
31 import {
32   capitalizeFirstLetter,
33   choicesConfig,
34   commentsToFlatNodes,
35   communitySelectName,
36   communityToChoice,
37   createCommentLikeRes,
38   createPostLikeFindRes,
39   debounce,
40   enableDownvotes,
41   enableNsfw,
42   fetchCommunities,
43   fetchLimit,
44   fetchUsers,
45   isBrowser,
46   myAuth,
47   numToSI,
48   personSelectName,
49   personToChoice,
50   pushNotNull,
51   restoreScrollPosition,
52   routeListingTypeToEnum,
53   routeSearchTypeToEnum,
54   routeSortTypeToEnum,
55   saveScrollPosition,
56   setIsoData,
57   showLocal,
58   toast,
59   wsClient,
60   wsSubscribe,
61 } from "../utils";
62 import { CommentNodes } from "./comment/comment-nodes";
63 import { HtmlTags } from "./common/html-tags";
64 import { Spinner } from "./common/icon";
65 import { ListingTypeSelect } from "./common/listing-type-select";
66 import { Paginator } from "./common/paginator";
67 import { SortSelect } from "./common/sort-select";
68 import { CommunityLink } from "./community/community-link";
69 import { PersonListing } from "./person/person-listing";
70 import { PostListing } from "./post/post-listing";
71
72 var Choices: any;
73 if (isBrowser()) {
74   Choices = require("choices.js");
75 }
76
77 interface SearchProps {
78   q?: string;
79   type_: SearchType;
80   sort: SortType;
81   listingType: ListingType;
82   communityId: number;
83   creatorId: number;
84   page: number;
85 }
86
87 interface SearchState {
88   q?: string;
89   type_: SearchType;
90   sort: SortType;
91   listingType: ListingType;
92   communityId: number;
93   creatorId: number;
94   page: number;
95   searchResponse?: SearchResponse;
96   communities: CommunityView[];
97   creatorDetails?: GetPersonDetailsResponse;
98   loading: boolean;
99   siteRes: GetSiteResponse;
100   searchText?: string;
101   resolveObjectResponse?: ResolveObjectResponse;
102 }
103
104 interface UrlParams {
105   q?: string;
106   type_?: SearchType;
107   sort?: SortType;
108   listingType?: ListingType;
109   communityId?: number;
110   creatorId?: number;
111   page?: number;
112 }
113
114 interface Combined {
115   type_: string;
116   data: CommentView | PostView | CommunityView | PersonViewSafe;
117   published: string;
118 }
119
120 export class Search extends Component<any, SearchState> {
121   private isoData = setIsoData(this.context);
122   private communityChoices: any;
123   private creatorChoices: any;
124   private subscription?: Subscription;
125   state: SearchState = {
126     q: Search.getSearchQueryFromProps(this.props.match.params.q),
127     type_: Search.getSearchTypeFromProps(this.props.match.params.type),
128     sort: Search.getSortTypeFromProps(this.props.match.params.sort),
129     listingType: Search.getListingTypeFromProps(
130       this.props.match.params.listing_type
131     ),
132     page: Search.getPageFromProps(this.props.match.params.page),
133     searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
134     communityId: Search.getCommunityIdFromProps(
135       this.props.match.params.community_id
136     ),
137     creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
138     loading: false,
139     siteRes: this.isoData.site_res,
140     communities: [],
141   };
142
143   static getSearchQueryFromProps(q?: string): string | undefined {
144     return q ? decodeURIComponent(q) : undefined;
145   }
146
147   static getSearchTypeFromProps(type_: string): SearchType {
148     return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
149   }
150
151   static getSortTypeFromProps(sort: string): SortType {
152     return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
153   }
154
155   static getListingTypeFromProps(listingType: string): ListingType {
156     return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
157   }
158
159   static getCommunityIdFromProps(id: string): number {
160     return id ? Number(id) : 0;
161   }
162
163   static getCreatorIdFromProps(id: string): number {
164     return id ? Number(id) : 0;
165   }
166
167   static getPageFromProps(page: string): number {
168     return page ? Number(page) : 1;
169   }
170
171   constructor(props: any, context: any) {
172     super(props, context);
173
174     this.handleSortChange = this.handleSortChange.bind(this);
175     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
176     this.handlePageChange = this.handlePageChange.bind(this);
177
178     this.parseMessage = this.parseMessage.bind(this);
179     this.subscription = wsSubscribe(this.parseMessage);
180
181     // Only fetch the data if coming from another route
182     if (this.isoData.path == this.context.router.route.match.url) {
183       let communityRes = this.isoData.routeData[0] as
184         | GetCommunityResponse
185         | undefined;
186       let communitiesRes = this.isoData.routeData[1] as
187         | ListCommunitiesResponse
188         | undefined;
189       // This can be single or multiple communities given
190       if (communitiesRes) {
191         this.state = {
192           ...this.state,
193           communities: communitiesRes.communities,
194         };
195       }
196
197       if (communityRes) {
198         this.state = {
199           ...this.state,
200           communities: [communityRes.community_view],
201         };
202       }
203
204       this.state = {
205         ...this.state,
206         creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse,
207       };
208
209       if (this.state.q != "") {
210         this.state = {
211           ...this.state,
212           searchResponse: this.isoData.routeData[3] as SearchResponse,
213           resolveObjectResponse: this.isoData
214             .routeData[4] as ResolveObjectResponse,
215           loading: false,
216         };
217       } else {
218         this.search();
219       }
220     } else {
221       this.fetchCommunities();
222
223       if (this.state.q) {
224         this.search();
225       }
226     }
227   }
228
229   componentWillUnmount() {
230     this.subscription?.unsubscribe();
231     saveScrollPosition(this.context);
232   }
233
234   componentDidMount() {
235     this.setupCommunityFilter();
236     this.setupCreatorFilter();
237   }
238
239   static getDerivedStateFromProps(
240     props: any,
241     prevState: SearchState
242   ): SearchProps {
243     return {
244       q: Search.getSearchQueryFromProps(props.match.params.q),
245       type_:
246         prevState.type_ ??
247         Search.getSearchTypeFromProps(props.match.params.type),
248       sort:
249         prevState.sort ?? Search.getSortTypeFromProps(props.match.params.sort),
250       listingType:
251         prevState.listingType ??
252         Search.getListingTypeFromProps(props.match.params.listing_type),
253       communityId: Search.getCommunityIdFromProps(
254         props.match.params.community_id
255       ),
256       creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
257       page: Search.getPageFromProps(props.match.params.page),
258     };
259   }
260
261   fetchCommunities() {
262     let listCommunitiesForm: ListCommunities = {
263       type_: ListingType.All,
264       sort: SortType.TopAll,
265       limit: fetchLimit,
266       auth: myAuth(false),
267     };
268     WebSocketService.Instance.send(
269       wsClient.listCommunities(listCommunitiesForm)
270     );
271   }
272
273   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
274     let pathSplit = req.path.split("/");
275     let promises: Promise<any>[] = [];
276     let auth = req.auth;
277
278     let communityId = this.getCommunityIdFromProps(pathSplit[11]);
279     let community_id = communityId == 0 ? undefined : communityId;
280     if (community_id) {
281       let getCommunityForm: GetCommunity = {
282         id: community_id,
283         auth,
284       };
285       promises.push(req.client.getCommunity(getCommunityForm));
286       promises.push(Promise.resolve());
287     } else {
288       let listCommunitiesForm: ListCommunities = {
289         type_: ListingType.All,
290         sort: SortType.TopAll,
291         limit: fetchLimit,
292         auth: req.auth,
293       };
294       promises.push(Promise.resolve());
295       promises.push(req.client.listCommunities(listCommunitiesForm));
296     }
297
298     let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
299     let creator_id = creatorId == 0 ? undefined : creatorId;
300     if (creator_id) {
301       let getCreatorForm: GetPersonDetails = {
302         person_id: creator_id,
303         auth: req.auth,
304       };
305       promises.push(req.client.getPersonDetails(getCreatorForm));
306     } else {
307       promises.push(Promise.resolve());
308     }
309
310     let q = this.getSearchQueryFromProps(pathSplit[3]);
311
312     if (q) {
313       let form: SearchForm = {
314         q,
315         community_id,
316         creator_id,
317         type_: this.getSearchTypeFromProps(pathSplit[5]),
318         sort: this.getSortTypeFromProps(pathSplit[7]),
319         listing_type: this.getListingTypeFromProps(pathSplit[9]),
320         page: this.getPageFromProps(pathSplit[15]),
321         limit: fetchLimit,
322         auth: req.auth,
323       };
324
325       let resolveObjectForm: ResolveObject = {
326         q,
327         auth: req.auth,
328       };
329
330       if (form.q != "") {
331         promises.push(req.client.search(form));
332         promises.push(req.client.resolveObject(resolveObjectForm));
333       } else {
334         promises.push(Promise.resolve());
335         promises.push(Promise.resolve());
336       }
337     }
338
339     return promises;
340   }
341
342   componentDidUpdate(_: any, lastState: SearchState) {
343     if (
344       lastState.q !== this.state.q ||
345       lastState.type_ !== this.state.type_ ||
346       lastState.sort !== this.state.sort ||
347       lastState.listingType !== this.state.listingType ||
348       lastState.communityId !== this.state.communityId ||
349       lastState.creatorId !== this.state.creatorId ||
350       lastState.page !== this.state.page
351     ) {
352       if (this.state.q) {
353         this.setState({
354           loading: true,
355           searchText: this.state.q,
356         });
357         this.search();
358       }
359     }
360   }
361
362   get documentTitle(): string {
363     let siteName = this.state.siteRes.site_view.site.name;
364     return this.state.q
365       ? `${i18n.t("search")} - ${this.state.q} - ${siteName}`
366       : `${i18n.t("search")} - ${siteName}`;
367   }
368
369   render() {
370     return (
371       <div className="container-lg">
372         <HtmlTags
373           title={this.documentTitle}
374           path={this.context.router.route.match.url}
375         />
376         <h5>{i18n.t("search")}</h5>
377         {this.selects()}
378         {this.searchForm()}
379         {this.state.type_ == SearchType.All && this.all()}
380         {this.state.type_ == SearchType.Comments && this.comments()}
381         {this.state.type_ == SearchType.Posts && this.posts()}
382         {this.state.type_ == SearchType.Communities && this.communities()}
383         {this.state.type_ == SearchType.Users && this.users()}
384         {this.state.type_ == SearchType.Url && this.posts()}
385         {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
386         <Paginator page={this.state.page} onChange={this.handlePageChange} />
387       </div>
388     );
389   }
390
391   searchForm() {
392     return (
393       <form
394         className="form-inline"
395         onSubmit={linkEvent(this, this.handleSearchSubmit)}
396       >
397         <input
398           type="text"
399           className="form-control mr-2 mb-2"
400           value={this.state.searchText}
401           placeholder={`${i18n.t("search")}...`}
402           aria-label={i18n.t("search")}
403           onInput={linkEvent(this, this.handleQChange)}
404           required
405           minLength={1}
406         />
407         <button type="submit" className="btn btn-secondary mr-2 mb-2">
408           {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
409         </button>
410       </form>
411     );
412   }
413
414   selects() {
415     return (
416       <div className="mb-2">
417         <select
418           value={this.state.type_}
419           onChange={linkEvent(this, this.handleTypeChange)}
420           className="custom-select w-auto mb-2"
421           aria-label={i18n.t("type")}
422         >
423           <option disabled aria-hidden="true">
424             {i18n.t("type")}
425           </option>
426           <option value={SearchType.All}>{i18n.t("all")}</option>
427           <option value={SearchType.Comments}>{i18n.t("comments")}</option>
428           <option value={SearchType.Posts}>{i18n.t("posts")}</option>
429           <option value={SearchType.Communities}>
430             {i18n.t("communities")}
431           </option>
432           <option value={SearchType.Users}>{i18n.t("users")}</option>
433           <option value={SearchType.Url}>{i18n.t("url")}</option>
434         </select>
435         <span className="ml-2">
436           <ListingTypeSelect
437             type_={this.state.listingType}
438             showLocal={showLocal(this.isoData)}
439             showSubscribed
440             onChange={this.handleListingTypeChange}
441           />
442         </span>
443         <span className="ml-2">
444           <SortSelect
445             sort={this.state.sort}
446             onChange={this.handleSortChange}
447             hideHot
448             hideMostComments
449           />
450         </span>
451         <div className="form-row">
452           {this.state.communities.length > 0 && this.communityFilter()}
453           {this.creatorFilter()}
454         </div>
455       </div>
456     );
457   }
458
459   postViewToCombined(postView: PostView): Combined {
460     return {
461       type_: "posts",
462       data: postView,
463       published: postView.post.published,
464     };
465   }
466
467   commentViewToCombined(commentView: CommentView): Combined {
468     return {
469       type_: "comments",
470       data: commentView,
471       published: commentView.comment.published,
472     };
473   }
474
475   communityViewToCombined(communityView: CommunityView): Combined {
476     return {
477       type_: "communities",
478       data: communityView,
479       published: communityView.community.published,
480     };
481   }
482
483   personViewSafeToCombined(personViewSafe: PersonViewSafe): Combined {
484     return {
485       type_: "users",
486       data: personViewSafe,
487       published: personViewSafe.person.published,
488     };
489   }
490
491   buildCombined(): Combined[] {
492     let combined: Combined[] = [];
493
494     let resolveRes = this.state.resolveObjectResponse;
495     // Push the possible resolve / federated objects first
496     if (resolveRes) {
497       let resolveComment = resolveRes.comment;
498       if (resolveComment) {
499         combined.push(this.commentViewToCombined(resolveComment));
500       }
501       let resolvePost = resolveRes.post;
502       if (resolvePost) {
503         combined.push(this.postViewToCombined(resolvePost));
504       }
505       let resolveCommunity = resolveRes.community;
506       if (resolveCommunity) {
507         combined.push(this.communityViewToCombined(resolveCommunity));
508       }
509       let resolveUser = resolveRes.person;
510       if (resolveUser) {
511         combined.push(this.personViewSafeToCombined(resolveUser));
512       }
513     }
514
515     // Push the search results
516     let searchRes = this.state.searchResponse;
517     if (searchRes) {
518       pushNotNull(
519         combined,
520         searchRes.comments?.map(e => this.commentViewToCombined(e))
521       );
522       pushNotNull(
523         combined,
524         searchRes.posts?.map(e => this.postViewToCombined(e))
525       );
526       pushNotNull(
527         combined,
528         searchRes.communities?.map(e => this.communityViewToCombined(e))
529       );
530       pushNotNull(
531         combined,
532         searchRes.users?.map(e => this.personViewSafeToCombined(e))
533       );
534     }
535
536     // Sort it
537     if (this.state.sort == SortType.New) {
538       combined.sort((a, b) => b.published.localeCompare(a.published));
539     } else {
540       combined.sort(
541         (a, b) =>
542           ((b.data as CommentView | PostView).counts.score |
543             (b.data as CommunityView).counts.subscribers |
544             (b.data as PersonViewSafe).counts.comment_score) -
545           ((a.data as CommentView | PostView).counts.score |
546             (a.data as CommunityView).counts.subscribers |
547             (a.data as PersonViewSafe).counts.comment_score)
548       );
549     }
550     return combined;
551   }
552
553   all() {
554     let combined = this.buildCombined();
555     return (
556       <div>
557         {combined.map(i => (
558           <div key={i.published} className="row">
559             <div className="col-12">
560               {i.type_ == "posts" && (
561                 <PostListing
562                   key={(i.data as PostView).post.id}
563                   post_view={i.data as PostView}
564                   showCommunity
565                   enableDownvotes={enableDownvotes(this.state.siteRes)}
566                   enableNsfw={enableNsfw(this.state.siteRes)}
567                   allLanguages={this.state.siteRes.all_languages}
568                   siteLanguages={this.state.siteRes.discussion_languages}
569                   viewOnly
570                 />
571               )}
572               {i.type_ == "comments" && (
573                 <CommentNodes
574                   key={(i.data as CommentView).comment.id}
575                   nodes={[
576                     {
577                       comment_view: i.data as CommentView,
578                       children: [],
579                       depth: 0,
580                     },
581                   ]}
582                   viewType={CommentViewType.Flat}
583                   viewOnly
584                   locked
585                   noIndent
586                   enableDownvotes={enableDownvotes(this.state.siteRes)}
587                   allLanguages={this.state.siteRes.all_languages}
588                   siteLanguages={this.state.siteRes.discussion_languages}
589                 />
590               )}
591               {i.type_ == "communities" && (
592                 <div>{this.communityListing(i.data as CommunityView)}</div>
593               )}
594               {i.type_ == "users" && (
595                 <div>{this.personListing(i.data as PersonViewSafe)}</div>
596               )}
597             </div>
598           </div>
599         ))}
600       </div>
601     );
602   }
603
604   comments() {
605     let comments: CommentView[] = [];
606     pushNotNull(comments, this.state.resolveObjectResponse?.comment);
607     pushNotNull(comments, this.state.searchResponse?.comments);
608
609     return (
610       <CommentNodes
611         nodes={commentsToFlatNodes(comments)}
612         viewType={CommentViewType.Flat}
613         viewOnly
614         locked
615         noIndent
616         enableDownvotes={enableDownvotes(this.state.siteRes)}
617         allLanguages={this.state.siteRes.all_languages}
618         siteLanguages={this.state.siteRes.discussion_languages}
619       />
620     );
621   }
622
623   posts() {
624     let posts: PostView[] = [];
625
626     pushNotNull(posts, this.state.resolveObjectResponse?.post);
627     pushNotNull(posts, this.state.searchResponse?.posts);
628
629     return (
630       <>
631         {posts.map(pv => (
632           <div key={pv.post.id} className="row">
633             <div className="col-12">
634               <PostListing
635                 post_view={pv}
636                 showCommunity
637                 enableDownvotes={enableDownvotes(this.state.siteRes)}
638                 enableNsfw={enableNsfw(this.state.siteRes)}
639                 allLanguages={this.state.siteRes.all_languages}
640                 siteLanguages={this.state.siteRes.discussion_languages}
641                 viewOnly
642               />
643             </div>
644           </div>
645         ))}
646       </>
647     );
648   }
649
650   communities() {
651     let communities: CommunityView[] = [];
652
653     pushNotNull(communities, this.state.resolveObjectResponse?.community);
654     pushNotNull(communities, this.state.searchResponse?.communities);
655
656     return (
657       <>
658         {communities.map(cv => (
659           <div key={cv.community.id} className="row">
660             <div className="col-12">{this.communityListing(cv)}</div>
661           </div>
662         ))}
663       </>
664     );
665   }
666
667   users() {
668     let users: PersonViewSafe[] = [];
669
670     pushNotNull(users, this.state.resolveObjectResponse?.person);
671     pushNotNull(users, this.state.searchResponse?.users);
672
673     return (
674       <>
675         {users.map(pvs => (
676           <div key={pvs.person.id} className="row">
677             <div className="col-12">{this.personListing(pvs)}</div>
678           </div>
679         ))}
680       </>
681     );
682   }
683
684   communityListing(community_view: CommunityView) {
685     return (
686       <>
687         <span>
688           <CommunityLink community={community_view.community} />
689         </span>
690         <span>{` -
691         ${i18n.t("number_of_subscribers", {
692           count: community_view.counts.subscribers,
693           formattedCount: numToSI(community_view.counts.subscribers),
694         })}
695       `}</span>
696       </>
697     );
698   }
699
700   personListing(person_view: PersonViewSafe) {
701     return (
702       <>
703         <span>
704           <PersonListing person={person_view.person} showApubName />
705         </span>
706         <span>{` - ${i18n.t("number_of_comments", {
707           count: person_view.counts.comment_count,
708           formattedCount: numToSI(person_view.counts.comment_count),
709         })}`}</span>
710       </>
711     );
712   }
713
714   communityFilter() {
715     return (
716       <div className="form-group col-sm-6">
717         <label className="col-form-label" htmlFor="community-filter">
718           {i18n.t("community")}
719         </label>
720         <div>
721           <select
722             className="form-control"
723             id="community-filter"
724             value={this.state.communityId}
725           >
726             <option value="0">{i18n.t("all")}</option>
727             {this.state.communities.map(cv => (
728               <option key={cv.community.id} value={cv.community.id}>
729                 {communitySelectName(cv)}
730               </option>
731             ))}
732           </select>
733         </div>
734       </div>
735     );
736   }
737
738   creatorFilter() {
739     let creatorPv = this.state.creatorDetails?.person_view;
740     return (
741       <div className="form-group col-sm-6">
742         <label className="col-form-label" htmlFor="creator-filter">
743           {capitalizeFirstLetter(i18n.t("creator"))}
744         </label>
745         <div>
746           <select
747             className="form-control"
748             id="creator-filter"
749             value={this.state.creatorId}
750           >
751             <option value="0">{i18n.t("all")}</option>
752             {creatorPv && (
753               <option value={creatorPv.person.id}>
754                 {personSelectName(creatorPv)}
755               </option>
756             )}
757           </select>
758         </div>
759       </div>
760     );
761   }
762
763   resultsCount(): number {
764     let r = this.state.searchResponse;
765
766     let searchCount = r
767       ? r.posts?.length +
768         r.comments?.length +
769         r.communities?.length +
770         r.users?.length
771       : 0;
772
773     let resolveRes = this.state.resolveObjectResponse;
774     let resObjCount = resolveRes
775       ? resolveRes.post ||
776         resolveRes.person ||
777         resolveRes.community ||
778         resolveRes.comment
779         ? 1
780         : 0
781       : 0;
782
783     return resObjCount + searchCount;
784   }
785
786   handlePageChange(page: number) {
787     this.updateUrl({ page });
788   }
789
790   search() {
791     let community_id =
792       this.state.communityId == 0 ? undefined : this.state.communityId;
793     let creator_id =
794       this.state.creatorId == 0 ? undefined : this.state.creatorId;
795
796     let auth = myAuth(false);
797     if (this.state.q && this.state.q != "") {
798       let form: SearchForm = {
799         q: this.state.q,
800         community_id,
801         creator_id,
802         type_: this.state.type_,
803         sort: this.state.sort,
804         listing_type: this.state.listingType,
805         page: this.state.page,
806         limit: fetchLimit,
807         auth,
808       };
809
810       let resolveObjectForm: ResolveObject = {
811         q: this.state.q,
812         auth,
813       };
814
815       this.setState({
816         searchResponse: undefined,
817         resolveObjectResponse: undefined,
818         loading: true,
819       });
820       WebSocketService.Instance.send(wsClient.search(form));
821       WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
822     }
823   }
824
825   setupCommunityFilter() {
826     if (isBrowser()) {
827       let selectId: any = document.getElementById("community-filter");
828       if (selectId) {
829         this.communityChoices = new Choices(selectId, choicesConfig);
830         this.communityChoices.passedElement.element.addEventListener(
831           "choice",
832           (e: any) => {
833             this.handleCommunityFilterChange(Number(e.detail.choice.value));
834           },
835           false
836         );
837         this.communityChoices.passedElement.element.addEventListener(
838           "search",
839           debounce(async (e: any) => {
840             try {
841               let communities = (await fetchCommunities(e.detail.value))
842                 .communities;
843               let choices = communities.map(cv => communityToChoice(cv));
844               choices.unshift({ value: "0", label: i18n.t("all") });
845               this.communityChoices.setChoices(choices, "value", "label", true);
846             } catch (err) {
847               console.error(err);
848             }
849           }),
850           false
851         );
852       }
853     }
854   }
855
856   setupCreatorFilter() {
857     if (isBrowser()) {
858       let selectId: any = document.getElementById("creator-filter");
859       if (selectId) {
860         this.creatorChoices = new Choices(selectId, choicesConfig);
861         this.creatorChoices.passedElement.element.addEventListener(
862           "choice",
863           (e: any) => {
864             this.handleCreatorFilterChange(Number(e.detail.choice.value));
865           },
866           false
867         );
868         this.creatorChoices.passedElement.element.addEventListener(
869           "search",
870           debounce(async (e: any) => {
871             try {
872               let creators = (await fetchUsers(e.detail.value)).users;
873               let choices = creators.map(pvs => personToChoice(pvs));
874               choices.unshift({ value: "0", label: i18n.t("all") });
875               this.creatorChoices.setChoices(choices, "value", "label", true);
876             } catch (err) {
877               console.log(err);
878             }
879           }),
880           false
881         );
882       }
883     }
884   }
885
886   handleSortChange(val: SortType) {
887     const updateObj = { sort: val, page: 1 };
888     this.setState(updateObj);
889     this.updateUrl(updateObj);
890   }
891
892   handleTypeChange(i: Search, event: any) {
893     const updateObj = {
894       type_: SearchType[event.target.value],
895       page: 1,
896     };
897     i.setState(updateObj);
898     i.updateUrl(updateObj);
899   }
900
901   handleListingTypeChange(val: ListingType) {
902     const updateObj = {
903       listingType: val,
904       page: 1,
905     };
906     this.setState(updateObj);
907     this.updateUrl(updateObj);
908   }
909
910   handleCommunityFilterChange(communityId: number) {
911     this.updateUrl({
912       communityId,
913       page: 1,
914     });
915   }
916
917   handleCreatorFilterChange(creatorId: number) {
918     this.updateUrl({
919       creatorId,
920       page: 1,
921     });
922   }
923
924   handleSearchSubmit(i: Search, event: any) {
925     event.preventDefault();
926     i.updateUrl({
927       q: i.state.searchText,
928       type_: i.state.type_,
929       listingType: i.state.listingType,
930       communityId: i.state.communityId,
931       creatorId: i.state.creatorId,
932       sort: i.state.sort,
933       page: i.state.page,
934     });
935   }
936
937   handleQChange(i: Search, event: any) {
938     i.setState({ searchText: event.target.value });
939   }
940
941   updateUrl(paramUpdates: UrlParams) {
942     const qStr = paramUpdates.q || this.state.q;
943     const qStrEncoded = encodeURIComponent(qStr || "");
944     const typeStr = paramUpdates.type_ || this.state.type_;
945     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
946     const sortStr = paramUpdates.sort || this.state.sort;
947     const communityId =
948       paramUpdates.communityId == 0
949         ? 0
950         : paramUpdates.communityId || this.state.communityId;
951     const creatorId =
952       paramUpdates.creatorId == 0
953         ? 0
954         : paramUpdates.creatorId || this.state.creatorId;
955     const page = paramUpdates.page || this.state.page;
956     this.props.history.push(
957       `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
958     );
959   }
960
961   parseMessage(msg: any) {
962     console.log(msg);
963     let op = wsUserOp(msg);
964     if (msg.error) {
965       if (msg.error == "couldnt_find_object") {
966         this.setState({
967           resolveObjectResponse: {},
968         });
969         this.checkFinishedLoading();
970       } else {
971         toast(i18n.t(msg.error), "danger");
972         return;
973       }
974     } else if (op == UserOperation.Search) {
975       let data = wsJsonToRes<SearchResponse>(msg);
976       this.setState({ searchResponse: data });
977       window.scrollTo(0, 0);
978       this.checkFinishedLoading();
979       restoreScrollPosition(this.context);
980     } else if (op == UserOperation.CreateCommentLike) {
981       let data = wsJsonToRes<CommentResponse>(msg);
982       createCommentLikeRes(
983         data.comment_view,
984         this.state.searchResponse?.comments
985       );
986       this.setState(this.state);
987     } else if (op == UserOperation.CreatePostLike) {
988       let data = wsJsonToRes<PostResponse>(msg);
989       createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
990       this.setState(this.state);
991     } else if (op == UserOperation.ListCommunities) {
992       let data = wsJsonToRes<ListCommunitiesResponse>(msg);
993       this.setState({ communities: data.communities });
994       this.setupCommunityFilter();
995     } else if (op == UserOperation.ResolveObject) {
996       let data = wsJsonToRes<ResolveObjectResponse>(msg);
997       this.setState({ resolveObjectResponse: data });
998       this.checkFinishedLoading();
999     }
1000   }
1001
1002   checkFinishedLoading() {
1003     if (this.state.searchResponse && this.state.resolveObjectResponse) {
1004       this.setState({ loading: false });
1005     }
1006   }
1007 }