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