]> Untitled Git - lemmy-ui.git/blob - src/shared/components/search.tsx
Add default post listing (#645)
[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             showSubscribed
407             onChange={this.handleListingTypeChange}
408           />
409         </span>
410         <span class="ml-2">
411           <SortSelect
412             sort={this.state.sort}
413             onChange={this.handleSortChange}
414             hideHot
415             hideMostComments
416           />
417         </span>
418         <div class="form-row">
419           {this.state.communities.length > 0 && this.communityFilter()}
420           {this.creatorFilter()}
421         </div>
422       </div>
423     );
424   }
425
426   postViewToCombined(postView: PostView): Combined {
427     return {
428       type_: "posts",
429       data: postView,
430       published: postView.post.published,
431     };
432   }
433
434   commentViewToCombined(commentView: CommentView): Combined {
435     return {
436       type_: "comments",
437       data: commentView,
438       published: commentView.comment.published,
439     };
440   }
441
442   communityViewToCombined(communityView: CommunityView): Combined {
443     return {
444       type_: "communities",
445       data: communityView,
446       published: communityView.community.published,
447     };
448   }
449
450   personViewSafeToCombined(personViewSafe: PersonViewSafe): Combined {
451     return {
452       type_: "users",
453       data: personViewSafe,
454       published: personViewSafe.person.published,
455     };
456   }
457
458   buildCombined(): Combined[] {
459     let combined: Combined[] = [];
460
461     // Push the possible resolve / federated objects first
462     let resolveComment = this.state.resolveObjectResponse?.comment;
463     if (resolveComment) {
464       combined.push(this.commentViewToCombined(resolveComment));
465     }
466     let resolvePost = this.state.resolveObjectResponse?.post;
467     if (resolvePost) {
468       combined.push(this.postViewToCombined(resolvePost));
469     }
470     let resolveCommunity = this.state.resolveObjectResponse?.community;
471     if (resolveCommunity) {
472       combined.push(this.communityViewToCombined(resolveCommunity));
473     }
474     let resolveUser = this.state.resolveObjectResponse?.person;
475     if (resolveUser) {
476       combined.push(this.personViewSafeToCombined(resolveUser));
477     }
478
479     // Push the search results
480     pushNotNull(
481       combined,
482       this.state.searchResponse?.comments?.map(e =>
483         this.commentViewToCombined(e)
484       )
485     );
486     pushNotNull(
487       combined,
488       this.state.searchResponse?.posts?.map(e => this.postViewToCombined(e))
489     );
490     pushNotNull(
491       combined,
492       this.state.searchResponse?.communities?.map(e =>
493         this.communityViewToCombined(e)
494       )
495     );
496     pushNotNull(
497       combined,
498       this.state.searchResponse?.users?.map(e =>
499         this.personViewSafeToCombined(e)
500       )
501     );
502
503     // Sort it
504     if (this.state.sort == SortType.New) {
505       combined.sort((a, b) => b.published.localeCompare(a.published));
506     } else {
507       combined.sort(
508         (a, b) =>
509           ((b.data as CommentView | PostView).counts.score |
510             (b.data as CommunityView).counts.subscribers |
511             (b.data as PersonViewSafe).counts.comment_score) -
512           ((a.data as CommentView | PostView).counts.score |
513             (a.data as CommunityView).counts.subscribers |
514             (a.data as PersonViewSafe).counts.comment_score)
515       );
516     }
517     return combined;
518   }
519
520   all() {
521     let combined = this.buildCombined();
522     return (
523       <div>
524         {combined.map(i => (
525           <div class="row">
526             <div class="col-12">
527               {i.type_ == "posts" && (
528                 <PostListing
529                   key={(i.data as PostView).post.id}
530                   post_view={i.data as PostView}
531                   showCommunity
532                   enableDownvotes={this.state.site.enable_downvotes}
533                   enableNsfw={this.state.site.enable_nsfw}
534                 />
535               )}
536               {i.type_ == "comments" && (
537                 <CommentNodes
538                   key={(i.data as CommentView).comment.id}
539                   nodes={[{ comment_view: i.data as CommentView }]}
540                   locked
541                   noIndent
542                   enableDownvotes={this.state.site.enable_downvotes}
543                 />
544               )}
545               {i.type_ == "communities" && (
546                 <div>{this.communityListing(i.data as CommunityView)}</div>
547               )}
548               {i.type_ == "users" && (
549                 <div>{this.userListing(i.data as PersonViewSafe)}</div>
550               )}
551             </div>
552           </div>
553         ))}
554       </div>
555     );
556   }
557
558   comments() {
559     let comments: CommentView[] = [];
560
561     pushNotNull(comments, this.state.resolveObjectResponse?.comment);
562     pushNotNull(comments, this.state.searchResponse?.comments);
563
564     return (
565       <CommentNodes
566         nodes={commentsToFlatNodes(comments)}
567         locked
568         noIndent
569         enableDownvotes={this.state.site.enable_downvotes}
570       />
571     );
572   }
573
574   posts() {
575     let posts: PostView[] = [];
576
577     pushNotNull(posts, this.state.resolveObjectResponse?.post);
578     pushNotNull(posts, this.state.searchResponse?.posts);
579
580     return (
581       <>
582         {posts.map(post => (
583           <div class="row">
584             <div class="col-12">
585               <PostListing
586                 post_view={post}
587                 showCommunity
588                 enableDownvotes={this.state.site.enable_downvotes}
589                 enableNsfw={this.state.site.enable_nsfw}
590               />
591             </div>
592           </div>
593         ))}
594       </>
595     );
596   }
597
598   communities() {
599     let communities: CommunityView[] = [];
600
601     pushNotNull(communities, this.state.resolveObjectResponse?.community);
602     pushNotNull(communities, this.state.searchResponse?.communities);
603
604     return (
605       <>
606         {communities.map(community => (
607           <div class="row">
608             <div class="col-12">{this.communityListing(community)}</div>
609           </div>
610         ))}
611       </>
612     );
613   }
614
615   users() {
616     let users: PersonViewSafe[] = [];
617
618     pushNotNull(users, this.state.resolveObjectResponse?.person);
619     pushNotNull(users, this.state.searchResponse?.users);
620
621     return (
622       <>
623         {users.map(user => (
624           <div class="row">
625             <div class="col-12">{this.userListing(user)}</div>
626           </div>
627         ))}
628       </>
629     );
630   }
631
632   communityListing(community_view: CommunityView) {
633     return (
634       <>
635         <span>
636           <CommunityLink community={community_view.community} />
637         </span>
638         <span>{` -
639         ${i18n.t("number_of_subscribers", {
640           count: community_view.counts.subscribers,
641           formattedCount: numToSI(community_view.counts.subscribers),
642         })}
643       `}</span>
644       </>
645     );
646   }
647
648   userListing(person_view: PersonViewSafe) {
649     return [
650       <span>
651         <PersonListing person={person_view.person} showApubName />
652       </span>,
653       <span>{` - ${i18n.t("number_of_comments", {
654         count: person_view.counts.comment_count,
655         formattedCount: numToSI(person_view.counts.comment_count),
656       })}`}</span>,
657     ];
658   }
659
660   communityFilter() {
661     return (
662       <div class="form-group col-sm-6">
663         <label class="col-form-label" htmlFor="community-filter">
664           {i18n.t("community")}
665         </label>
666         <div>
667           <select
668             class="form-control"
669             id="community-filter"
670             value={this.state.communityId}
671           >
672             <option value="0">{i18n.t("all")}</option>
673             {this.state.communities.map(cv => (
674               <option value={cv.community.id}>{communitySelectName(cv)}</option>
675             ))}
676           </select>
677         </div>
678       </div>
679     );
680   }
681
682   creatorFilter() {
683     return (
684       <div class="form-group col-sm-6">
685         <label class="col-form-label" htmlFor="creator-filter">
686           {capitalizeFirstLetter(i18n.t("creator"))}
687         </label>
688         <div>
689           <select
690             class="form-control"
691             id="creator-filter"
692             value={this.state.creatorId}
693           >
694             <option value="0">{i18n.t("all")}</option>
695             {this.state.creator && (
696               <option value={this.state.creator.person.id}>
697                 {personSelectName(this.state.creator)}
698               </option>
699             )}
700           </select>
701         </div>
702       </div>
703     );
704   }
705
706   resultsCount(): number {
707     let res = this.state.searchResponse;
708     let resObj = this.state.resolveObjectResponse;
709     let resObjCount =
710       resObj?.post || resObj?.person || resObj?.community || resObj?.comment
711         ? 1
712         : 0;
713     return (
714       res?.posts?.length +
715       res?.comments?.length +
716       res?.communities?.length +
717       res?.users?.length +
718       resObjCount
719     );
720   }
721
722   handlePageChange(page: number) {
723     this.updateUrl({ page });
724   }
725
726   search() {
727     let form: SearchForm = {
728       q: this.state.q,
729       type_: this.state.type_,
730       sort: this.state.sort,
731       listing_type: this.state.listingType,
732       page: this.state.page,
733       limit: fetchLimit,
734       auth: authField(false),
735     };
736     if (this.state.communityId !== 0) {
737       form.community_id = this.state.communityId;
738     }
739     if (this.state.creatorId !== 0) {
740       form.creator_id = this.state.creatorId;
741     }
742
743     let resolveObjectForm: ResolveObject = {
744       q: this.state.q,
745       auth: authField(false),
746     };
747
748     if (this.state.q != "") {
749       this.state.searchResponse = null;
750       this.state.resolveObjectResponse = null;
751       this.state.loading = true;
752       this.setState(this.state);
753       WebSocketService.Instance.send(wsClient.search(form));
754       WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
755     }
756   }
757
758   setupCommunityFilter() {
759     if (isBrowser()) {
760       let selectId: any = document.getElementById("community-filter");
761       if (selectId) {
762         this.communityChoices = new Choices(selectId, choicesConfig);
763         this.communityChoices.passedElement.element.addEventListener(
764           "choice",
765           (e: any) => {
766             this.handleCommunityFilterChange(Number(e.detail.choice.value));
767           },
768           false
769         );
770         this.communityChoices.passedElement.element.addEventListener(
771           "search",
772           debounce(async (e: any) => {
773             try {
774               let communities = (await fetchCommunities(e.detail.value))
775                 .communities;
776               let choices = communities.map(cv => communityToChoice(cv));
777               choices.unshift({ value: "0", label: i18n.t("all") });
778               this.communityChoices.setChoices(choices, "value", "label", true);
779             } catch (err) {
780               console.error(err);
781             }
782           }),
783           false
784         );
785       }
786     }
787   }
788
789   setupCreatorFilter() {
790     if (isBrowser()) {
791       let selectId: any = document.getElementById("creator-filter");
792       if (selectId) {
793         this.creatorChoices = new Choices(selectId, choicesConfig);
794         this.creatorChoices.passedElement.element.addEventListener(
795           "choice",
796           (e: any) => {
797             this.handleCreatorFilterChange(Number(e.detail.choice.value));
798           },
799           false
800         );
801         this.creatorChoices.passedElement.element.addEventListener(
802           "search",
803           debounce(async (e: any) => {
804             try {
805               let creators = (await fetchUsers(e.detail.value)).users;
806               let choices = creators.map(pvs => personToChoice(pvs));
807               choices.unshift({ value: "0", label: i18n.t("all") });
808               this.creatorChoices.setChoices(choices, "value", "label", true);
809             } catch (err) {
810               console.log(err);
811             }
812           }),
813           false
814         );
815       }
816     }
817   }
818
819   handleSortChange(val: SortType) {
820     this.updateUrl({ sort: val, page: 1 });
821   }
822
823   handleTypeChange(i: Search, event: any) {
824     i.updateUrl({
825       type_: SearchType[event.target.value],
826       page: 1,
827     });
828   }
829
830   handleListingTypeChange(val: ListingType) {
831     this.updateUrl({
832       listingType: val,
833       page: 1,
834     });
835   }
836
837   handleCommunityFilterChange(communityId: number) {
838     this.updateUrl({
839       communityId,
840       page: 1,
841     });
842   }
843
844   handleCreatorFilterChange(creatorId: number) {
845     this.updateUrl({
846       creatorId,
847       page: 1,
848     });
849   }
850
851   handleSearchSubmit(i: Search, event: any) {
852     event.preventDefault();
853     i.updateUrl({
854       q: i.state.searchText,
855       type_: i.state.type_,
856       listingType: i.state.listingType,
857       communityId: i.state.communityId,
858       creatorId: i.state.creatorId,
859       sort: i.state.sort,
860       page: i.state.page,
861     });
862   }
863
864   handleQChange(i: Search, event: any) {
865     i.setState({ searchText: event.target.value });
866   }
867
868   updateUrl(paramUpdates: UrlParams) {
869     const qStr = paramUpdates.q || this.state.q;
870     const qStrEncoded = encodeURIComponent(qStr);
871     const typeStr = paramUpdates.type_ || this.state.type_;
872     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
873     const sortStr = paramUpdates.sort || this.state.sort;
874     const communityId =
875       paramUpdates.communityId == 0
876         ? 0
877         : paramUpdates.communityId || this.state.communityId;
878     const creatorId =
879       paramUpdates.creatorId == 0
880         ? 0
881         : paramUpdates.creatorId || this.state.creatorId;
882     const page = paramUpdates.page || this.state.page;
883     this.props.history.push(
884       `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
885     );
886   }
887
888   parseMessage(msg: any) {
889     console.log(msg);
890     let op = wsUserOp(msg);
891     if (msg.error) {
892       if (msg.error == "couldnt_find_object") {
893         this.state.resolveObjectResponse = {
894           comment: null,
895           post: null,
896           community: null,
897           person: null,
898         };
899         this.checkFinishedLoading();
900       } else {
901         toast(i18n.t(msg.error), "danger");
902         return;
903       }
904     } else if (op == UserOperation.Search) {
905       let data = wsJsonToRes<SearchResponse>(msg).data;
906       this.state.searchResponse = data;
907       window.scrollTo(0, 0);
908       this.checkFinishedLoading();
909       restoreScrollPosition(this.context);
910     } else if (op == UserOperation.CreateCommentLike) {
911       let data = wsJsonToRes<CommentResponse>(msg).data;
912       createCommentLikeRes(
913         data.comment_view,
914         this.state.searchResponse?.comments
915       );
916       this.setState(this.state);
917     } else if (op == UserOperation.CreatePostLike) {
918       let data = wsJsonToRes<PostResponse>(msg).data;
919       createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
920       this.setState(this.state);
921     } else if (op == UserOperation.ListCommunities) {
922       let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
923       this.state.communities = data.communities;
924       this.setState(this.state);
925       this.setupCommunityFilter();
926     } else if (op == UserOperation.ResolveObject) {
927       let data = wsJsonToRes<ResolveObjectResponse>(msg).data;
928       this.state.resolveObjectResponse = data;
929       this.checkFinishedLoading();
930     }
931   }
932
933   checkFinishedLoading() {
934     if (
935       this.state.searchResponse != null &&
936       this.state.resolveObjectResponse != null
937     ) {
938       this.state.loading = false;
939       this.setState(this.state);
940     }
941   }
942 }