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