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