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