]> Untitled Git - lemmy-ui.git/blob - src/shared/components/search.tsx
Get rid of "No Results" showing while search is still loading. (#997)
[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 && !this.state.searchLoading && (
427           <span>{i18n.t("no_results")}</span>
428         )}
429         <Paginator page={page} onChange={this.handlePageChange} />
430       </div>
431     );
432   }
433
434   displayResults(type: SearchType) {
435     switch (type) {
436       case SearchType.All:
437         return this.all;
438       case SearchType.Comments:
439         return this.comments;
440       case SearchType.Posts:
441       case SearchType.Url:
442         return this.posts;
443       case SearchType.Communities:
444         return this.communities;
445       case SearchType.Users:
446         return this.users;
447       default:
448         return <></>;
449     }
450   }
451
452   get searchForm() {
453     return (
454       <form
455         className="form-inline"
456         onSubmit={linkEvent(this, this.handleSearchSubmit)}
457       >
458         <input
459           type="text"
460           className="form-control mr-2 mb-2"
461           value={this.state.searchText}
462           placeholder={`${i18n.t("search")}...`}
463           aria-label={i18n.t("search")}
464           onInput={linkEvent(this, this.handleQChange)}
465           required
466           minLength={1}
467         />
468         <button type="submit" className="btn btn-secondary mr-2 mb-2">
469           {this.state.searchLoading ? (
470             <Spinner />
471           ) : (
472             <span>{i18n.t("search")}</span>
473           )}
474         </button>
475       </form>
476     );
477   }
478
479   get selects() {
480     const { type, listingType, sort, communityId, creatorId } =
481       getSearchQueryParams();
482     const {
483       communitySearchOptions,
484       creatorSearchOptions,
485       searchCommunitiesLoading,
486       searchCreatorLoading,
487     } = this.state;
488
489     return (
490       <div className="mb-2">
491         <select
492           value={type}
493           onChange={linkEvent(this, this.handleTypeChange)}
494           className="custom-select w-auto mb-2"
495           aria-label={i18n.t("type")}
496         >
497           <option disabled aria-hidden="true">
498             {i18n.t("type")}
499           </option>
500           {searchTypes.map(option => (
501             <option value={option} key={option}>
502               {i18n.t(option.toString().toLowerCase() as NoOptionI18nKeys)}
503             </option>
504           ))}
505         </select>
506         <span className="ml-2">
507           <ListingTypeSelect
508             type_={listingType}
509             showLocal={showLocal(this.isoData)}
510             showSubscribed
511             onChange={this.handleListingTypeChange}
512           />
513         </span>
514         <span className="ml-2">
515           <SortSelect
516             sort={sort}
517             onChange={this.handleSortChange}
518             hideHot
519             hideMostComments
520           />
521         </span>
522         <div className="form-row">
523           {this.state.communities.length > 0 && (
524             <Filter
525               filterType="community"
526               onChange={this.handleCommunityFilterChange}
527               onSearch={this.handleCommunitySearch}
528               options={communitySearchOptions}
529               loading={searchCommunitiesLoading}
530               value={communityId}
531             />
532           )}
533           <Filter
534             filterType="creator"
535             onChange={this.handleCreatorFilterChange}
536             onSearch={this.handleCreatorSearch}
537             options={creatorSearchOptions}
538             loading={searchCreatorLoading}
539             value={creatorId}
540           />
541         </div>
542       </div>
543     );
544   }
545
546   buildCombined(): Combined[] {
547     const combined: Combined[] = [];
548     const { resolveObjectResponse, searchResponse } = this.state;
549
550     // Push the possible resolve / federated objects first
551     if (resolveObjectResponse) {
552       const { comment, post, community, person } = resolveObjectResponse;
553
554       if (comment) {
555         combined.push(commentViewToCombined(comment));
556       }
557       if (post) {
558         combined.push(postViewToCombined(post));
559       }
560       if (community) {
561         combined.push(communityViewToCombined(community));
562       }
563       if (person) {
564         combined.push(personViewSafeToCombined(person));
565       }
566     }
567
568     // Push the search results
569     if (searchResponse) {
570       const { comments, posts, communities, users } = searchResponse;
571
572       combined.push(
573         ...[
574           ...(comments?.map(commentViewToCombined) ?? []),
575           ...(posts?.map(postViewToCombined) ?? []),
576           ...(communities?.map(communityViewToCombined) ?? []),
577           ...(users?.map(personViewSafeToCombined) ?? []),
578         ]
579       );
580     }
581
582     const { sort } = getSearchQueryParams();
583
584     // Sort it
585     if (sort === SortType.New) {
586       combined.sort((a, b) => b.published.localeCompare(a.published));
587     } else {
588       combined.sort(
589         (a, b) =>
590           ((b.data as CommentView | PostView).counts.score |
591             (b.data as CommunityView).counts.subscribers |
592             (b.data as PersonViewSafe).counts.comment_score) -
593           ((a.data as CommentView | PostView).counts.score |
594             (a.data as CommunityView).counts.subscribers |
595             (a.data as PersonViewSafe).counts.comment_score)
596       );
597     }
598
599     return combined;
600   }
601
602   get all() {
603     const combined = this.buildCombined();
604
605     return (
606       <div>
607         {combined.map(i => (
608           <div key={i.published} className="row">
609             <div className="col-12">
610               {i.type_ === "posts" && (
611                 <PostListing
612                   key={(i.data as PostView).post.id}
613                   post_view={i.data as PostView}
614                   showCommunity
615                   enableDownvotes={enableDownvotes(this.state.siteRes)}
616                   enableNsfw={enableNsfw(this.state.siteRes)}
617                   allLanguages={this.state.siteRes.all_languages}
618                   siteLanguages={this.state.siteRes.discussion_languages}
619                   viewOnly
620                 />
621               )}
622               {i.type_ === "comments" && (
623                 <CommentNodes
624                   key={(i.data as CommentView).comment.id}
625                   nodes={[
626                     {
627                       comment_view: i.data as CommentView,
628                       children: [],
629                       depth: 0,
630                     },
631                   ]}
632                   viewType={CommentViewType.Flat}
633                   viewOnly
634                   locked
635                   noIndent
636                   enableDownvotes={enableDownvotes(this.state.siteRes)}
637                   allLanguages={this.state.siteRes.all_languages}
638                   siteLanguages={this.state.siteRes.discussion_languages}
639                 />
640               )}
641               {i.type_ === "communities" && (
642                 <div>{communityListing(i.data as CommunityView)}</div>
643               )}
644               {i.type_ === "users" && (
645                 <div>{personListing(i.data as PersonViewSafe)}</div>
646               )}
647             </div>
648           </div>
649         ))}
650       </div>
651     );
652   }
653
654   get comments() {
655     const { searchResponse, resolveObjectResponse, siteRes } = this.state;
656     const comments = searchResponse?.comments ?? [];
657
658     if (resolveObjectResponse?.comment) {
659       comments.unshift(resolveObjectResponse?.comment);
660     }
661
662     return (
663       <CommentNodes
664         nodes={commentsToFlatNodes(comments)}
665         viewType={CommentViewType.Flat}
666         viewOnly
667         locked
668         noIndent
669         enableDownvotes={enableDownvotes(siteRes)}
670         allLanguages={siteRes.all_languages}
671         siteLanguages={siteRes.discussion_languages}
672       />
673     );
674   }
675
676   get posts() {
677     const { searchResponse, resolveObjectResponse, siteRes } = this.state;
678     const posts = searchResponse?.posts ?? [];
679
680     if (resolveObjectResponse?.post) {
681       posts.unshift(resolveObjectResponse.post);
682     }
683
684     return (
685       <>
686         {posts.map(pv => (
687           <div key={pv.post.id} className="row">
688             <div className="col-12">
689               <PostListing
690                 post_view={pv}
691                 showCommunity
692                 enableDownvotes={enableDownvotes(siteRes)}
693                 enableNsfw={enableNsfw(siteRes)}
694                 allLanguages={siteRes.all_languages}
695                 siteLanguages={siteRes.discussion_languages}
696                 viewOnly
697               />
698             </div>
699           </div>
700         ))}
701       </>
702     );
703   }
704
705   get communities() {
706     const { searchResponse, resolveObjectResponse } = this.state;
707     const communities = searchResponse?.communities ?? [];
708
709     if (resolveObjectResponse?.community) {
710       communities.unshift(resolveObjectResponse.community);
711     }
712
713     return (
714       <>
715         {communities.map(cv => (
716           <div key={cv.community.id} className="row">
717             <div className="col-12">{communityListing(cv)}</div>
718           </div>
719         ))}
720       </>
721     );
722   }
723
724   get users() {
725     const { searchResponse, resolveObjectResponse } = this.state;
726     const users = searchResponse?.users ?? [];
727
728     if (resolveObjectResponse?.person) {
729       users.unshift(resolveObjectResponse.person);
730     }
731
732     return (
733       <>
734         {users.map(pvs => (
735           <div key={pvs.person.id} className="row">
736             <div className="col-12">{personListing(pvs)}</div>
737           </div>
738         ))}
739       </>
740     );
741   }
742
743   get resultsCount(): number {
744     const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
745
746     const searchCount = r
747       ? r.posts.length +
748         r.comments.length +
749         r.communities.length +
750         r.users.length
751       : 0;
752
753     const resObjCount = resolveRes
754       ? resolveRes.post ||
755         resolveRes.person ||
756         resolveRes.community ||
757         resolveRes.comment
758         ? 1
759         : 0
760       : 0;
761
762     return resObjCount + searchCount;
763   }
764
765   search() {
766     const auth = myAuth(false);
767     const { searchText: q } = this.state;
768     const { communityId, creatorId, type, sort, listingType, page } =
769       getSearchQueryParams();
770
771     if (q && q !== "") {
772       const form: SearchForm = {
773         q,
774         community_id: communityId ?? undefined,
775         creator_id: creatorId ?? undefined,
776         type_: type,
777         sort,
778         listing_type: listingType,
779         page,
780         limit: fetchLimit,
781         auth,
782       };
783
784       const resolveObjectForm: ResolveObject = {
785         q,
786         auth,
787       };
788
789       this.setState({
790         searchResponse: undefined,
791         resolveObjectResponse: undefined,
792         searchLoading: true,
793       });
794
795       WebSocketService.Instance.send(wsClient.search(form));
796       WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
797     }
798   }
799
800   handleCreatorSearch = debounce(async (text: string) => {
801     const { creatorId } = getSearchQueryParams();
802     const { creatorSearchOptions } = this.state;
803     this.setState({
804       searchCreatorLoading: true,
805     });
806
807     const newOptions: Choice[] = [];
808
809     const selectedChoice = creatorSearchOptions.find(
810       choice => getIdFromString(choice.value) === creatorId
811     );
812
813     if (selectedChoice) {
814       newOptions.push(selectedChoice);
815     }
816
817     if (text.length > 0) {
818       newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
819     }
820
821     this.setState({
822       searchCreatorLoading: false,
823       creatorSearchOptions: newOptions,
824     });
825   });
826
827   handleCommunitySearch = debounce(async (text: string) => {
828     const { communityId } = getSearchQueryParams();
829     const { communitySearchOptions } = this.state;
830     this.setState({
831       searchCommunitiesLoading: true,
832     });
833
834     const newOptions: Choice[] = [];
835
836     const selectedChoice = communitySearchOptions.find(
837       choice => getIdFromString(choice.value) === communityId
838     );
839
840     if (selectedChoice) {
841       newOptions.push(selectedChoice);
842     }
843
844     if (text.length > 0) {
845       newOptions.push(
846         ...(await fetchCommunities(text)).communities.map(communityToChoice)
847       );
848     }
849
850     this.setState({
851       searchCommunitiesLoading: false,
852       communitySearchOptions: newOptions,
853     });
854   });
855
856   handleSortChange(sort: SortType) {
857     this.updateUrl({ sort, page: 1 });
858   }
859
860   handleTypeChange(i: Search, event: any) {
861     const type = SearchType[event.target.value];
862
863     i.updateUrl({
864       type,
865       page: 1,
866     });
867   }
868
869   handlePageChange(page: number) {
870     this.updateUrl({ page });
871   }
872
873   handleListingTypeChange(listingType: ListingType) {
874     this.updateUrl({
875       listingType,
876       page: 1,
877     });
878   }
879
880   handleCommunityFilterChange({ value }: Choice) {
881     this.updateUrl({
882       communityId: getIdFromString(value) ?? null,
883       page: 1,
884     });
885   }
886
887   handleCreatorFilterChange({ value }: Choice) {
888     this.updateUrl({
889       creatorId: getIdFromString(value) ?? null,
890       page: 1,
891     });
892   }
893
894   handleSearchSubmit(i: Search, event: any) {
895     event.preventDefault();
896
897     i.updateUrl({
898       q: i.state.searchText,
899       page: 1,
900     });
901   }
902
903   handleQChange(i: Search, event: any) {
904     i.setState({ searchText: event.target.value });
905   }
906
907   updateUrl({
908     q,
909     type,
910     listingType,
911     sort,
912     communityId,
913     creatorId,
914     page,
915   }: Partial<SearchProps>) {
916     const {
917       q: urlQ,
918       type: urlType,
919       listingType: urlListingType,
920       communityId: urlCommunityId,
921       sort: urlSort,
922       creatorId: urlCreatorId,
923       page: urlPage,
924     } = getSearchQueryParams();
925
926     let query = q ?? this.state.searchText ?? urlQ;
927
928     if (query && query.length > 0) {
929       query = encodeURIComponent(query);
930     }
931
932     const queryParams: QueryParams<SearchProps> = {
933       q: query,
934       type: type ?? urlType,
935       listingType: listingType ?? urlListingType,
936       communityId: getUpdatedSearchId(communityId, urlCommunityId),
937       creatorId: getUpdatedSearchId(creatorId, urlCreatorId),
938       page: (page ?? urlPage).toString(),
939       sort: sort ?? urlSort,
940     };
941
942     this.props.history.push(`/search${getQueryString(queryParams)}`);
943
944     this.search();
945   }
946
947   parseMessage(msg: any) {
948     console.log(msg);
949     const op = wsUserOp(msg);
950     if (msg.error) {
951       if (msg.error === "couldnt_find_object") {
952         this.setState({
953           resolveObjectResponse: {},
954         });
955         this.checkFinishedLoading();
956       } else {
957         toast(i18n.t(msg.error), "danger");
958       }
959     } else {
960       switch (op) {
961         case UserOperation.Search: {
962           const searchResponse = wsJsonToRes<SearchResponse>(msg);
963           this.setState({ searchResponse });
964           window.scrollTo(0, 0);
965           this.checkFinishedLoading();
966           restoreScrollPosition(this.context);
967
968           break;
969         }
970
971         case UserOperation.CreateCommentLike: {
972           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
973           createCommentLikeRes(
974             comment_view,
975             this.state.searchResponse?.comments
976           );
977
978           break;
979         }
980
981         case UserOperation.CreatePostLike: {
982           const { post_view } = wsJsonToRes<PostResponse>(msg);
983           createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
984
985           break;
986         }
987
988         case UserOperation.ListCommunities: {
989           const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
990           this.setState({ communities });
991
992           break;
993         }
994
995         case UserOperation.ResolveObject: {
996           const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
997           this.setState({ resolveObjectResponse });
998           this.checkFinishedLoading();
999
1000           break;
1001         }
1002       }
1003     }
1004   }
1005
1006   checkFinishedLoading() {
1007     if (this.state.searchResponse || this.state.resolveObjectResponse) {
1008       this.setState({ searchLoading: false });
1009     }
1010   }
1011 }