]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/communities.tsx
New communities fetch limit is 50. (#711)
[lemmy-ui.git] / src / shared / components / community / communities.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import {
4   CommunityResponse,
5   FollowCommunity,
6   GetSiteResponse,
7   ListCommunities,
8   ListCommunitiesResponse,
9   ListingType,
10   SortType,
11   SubscribedType,
12   UserOperation,
13   wsJsonToRes,
14   wsUserOp,
15 } from "lemmy-js-client";
16 import { Subscription } from "rxjs";
17 import { InitialFetchRequest } from "shared/interfaces";
18 import { i18n } from "../../i18next";
19 import { WebSocketService } from "../../services";
20 import {
21   auth,
22   getListingTypeFromPropsNoDefault,
23   getPageFromProps,
24   isBrowser,
25   numToSI,
26   setIsoData,
27   showLocal,
28   toast,
29   wsClient,
30   wsSubscribe,
31 } from "../../utils";
32 import { HtmlTags } from "../common/html-tags";
33 import { Spinner } from "../common/icon";
34 import { ListingTypeSelect } from "../common/listing-type-select";
35 import { Paginator } from "../common/paginator";
36 import { CommunityLink } from "./community-link";
37
38 const communityLimit = 50;
39
40 interface CommunitiesState {
41   listCommunitiesResponse: Option<ListCommunitiesResponse>;
42   page: number;
43   loading: boolean;
44   siteRes: GetSiteResponse;
45   searchText: string;
46   listingType: ListingType;
47 }
48
49 interface CommunitiesProps {
50   listingType?: ListingType;
51   page?: number;
52 }
53
54 export class Communities extends Component<any, CommunitiesState> {
55   private subscription: Subscription;
56   private isoData = setIsoData(this.context, ListCommunitiesResponse);
57   private emptyState: CommunitiesState = {
58     listCommunitiesResponse: None,
59     loading: true,
60     page: getPageFromProps(this.props),
61     listingType: getListingTypeFromPropsNoDefault(this.props),
62     siteRes: this.isoData.site_res,
63     searchText: "",
64   };
65
66   constructor(props: any, context: any) {
67     super(props, context);
68     this.state = this.emptyState;
69     this.handlePageChange = this.handlePageChange.bind(this);
70     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
71
72     this.parseMessage = this.parseMessage.bind(this);
73     this.subscription = wsSubscribe(this.parseMessage);
74
75     // Only fetch the data if coming from another route
76     if (this.isoData.path == this.context.router.route.match.url) {
77       let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse);
78       this.state.listCommunitiesResponse = listRes;
79       this.state.loading = false;
80     } else {
81       this.refetch();
82     }
83   }
84
85   componentWillUnmount() {
86     if (isBrowser()) {
87       this.subscription.unsubscribe();
88     }
89   }
90
91   static getDerivedStateFromProps(props: any): CommunitiesProps {
92     return {
93       listingType: getListingTypeFromPropsNoDefault(props),
94       page: getPageFromProps(props),
95     };
96   }
97
98   componentDidUpdate(_: any, lastState: CommunitiesState) {
99     if (
100       lastState.page !== this.state.page ||
101       lastState.listingType !== this.state.listingType
102     ) {
103       this.setState({ loading: true });
104       this.refetch();
105     }
106   }
107
108   get documentTitle(): string {
109     return this.state.siteRes.site_view.match({
110       some: siteView => `${i18n.t("communities")} - ${siteView.site.name}`,
111       none: "",
112     });
113   }
114
115   render() {
116     return (
117       <div class="container">
118         <HtmlTags
119           title={this.documentTitle}
120           path={this.context.router.route.match.url}
121           description={None}
122           image={None}
123         />
124         {this.state.loading ? (
125           <h5>
126             <Spinner large />
127           </h5>
128         ) : (
129           <div>
130             <div class="row">
131               <div class="col-md-6">
132                 <h4>{i18n.t("list_of_communities")}</h4>
133                 <span class="mb-2">
134                   <ListingTypeSelect
135                     type_={this.state.listingType}
136                     showLocal={showLocal(this.isoData)}
137                     showSubscribed
138                     onChange={this.handleListingTypeChange}
139                   />
140                 </span>
141               </div>
142               <div class="col-md-6">
143                 <div class="float-md-right">{this.searchForm()}</div>
144               </div>
145             </div>
146
147             <div class="table-responsive">
148               <table id="community_table" class="table table-sm table-hover">
149                 <thead class="pointer">
150                   <tr>
151                     <th>{i18n.t("name")}</th>
152                     <th class="text-right">{i18n.t("subscribers")}</th>
153                     <th class="text-right">
154                       {i18n.t("users")} / {i18n.t("month")}
155                     </th>
156                     <th class="text-right d-none d-lg-table-cell">
157                       {i18n.t("posts")}
158                     </th>
159                     <th class="text-right d-none d-lg-table-cell">
160                       {i18n.t("comments")}
161                     </th>
162                     <th></th>
163                   </tr>
164                 </thead>
165                 <tbody>
166                   {this.state.listCommunitiesResponse
167                     .map(l => l.communities)
168                     .unwrapOr([])
169                     .map(cv => (
170                       <tr>
171                         <td>
172                           <CommunityLink community={cv.community} />
173                         </td>
174                         <td class="text-right">
175                           {numToSI(cv.counts.subscribers)}
176                         </td>
177                         <td class="text-right">
178                           {numToSI(cv.counts.users_active_month)}
179                         </td>
180                         <td class="text-right d-none d-lg-table-cell">
181                           {numToSI(cv.counts.posts)}
182                         </td>
183                         <td class="text-right d-none d-lg-table-cell">
184                           {numToSI(cv.counts.comments)}
185                         </td>
186                         <td class="text-right">
187                           {cv.subscribed == SubscribedType.Subscribed && (
188                             <button
189                               class="btn btn-link d-inline-block"
190                               onClick={linkEvent(
191                                 cv.community.id,
192                                 this.handleUnsubscribe
193                               )}
194                             >
195                               {i18n.t("unsubscribe")}
196                             </button>
197                           )}
198                           {cv.subscribed == SubscribedType.NotSubscribed && (
199                             <button
200                               class="btn btn-link d-inline-block"
201                               onClick={linkEvent(
202                                 cv.community.id,
203                                 this.handleSubscribe
204                               )}
205                             >
206                               {i18n.t("subscribe")}
207                             </button>
208                           )}
209                           {cv.subscribed == SubscribedType.Pending && (
210                             <div class="text-warning d-inline-block">
211                               {i18n.t("subscribe_pending")}
212                             </div>
213                           )}
214                         </td>
215                       </tr>
216                     ))}
217                 </tbody>
218               </table>
219             </div>
220             <Paginator
221               page={this.state.page}
222               onChange={this.handlePageChange}
223             />
224           </div>
225         )}
226       </div>
227     );
228   }
229
230   searchForm() {
231     return (
232       <form
233         class="form-inline"
234         onSubmit={linkEvent(this, this.handleSearchSubmit)}
235       >
236         <input
237           type="text"
238           id="communities-search"
239           class="form-control mr-2 mb-2"
240           value={this.state.searchText}
241           placeholder={`${i18n.t("search")}...`}
242           onInput={linkEvent(this, this.handleSearchChange)}
243           required
244           minLength={3}
245         />
246         <label class="sr-only" htmlFor="communities-search">
247           {i18n.t("search")}
248         </label>
249         <button type="submit" class="btn btn-secondary mr-2 mb-2">
250           <span>{i18n.t("search")}</span>
251         </button>
252       </form>
253     );
254   }
255
256   updateUrl(paramUpdates: CommunitiesProps) {
257     const page = paramUpdates.page || this.state.page;
258     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
259     this.props.history.push(
260       `/communities/listing_type/${listingTypeStr}/page/${page}`
261     );
262   }
263
264   handlePageChange(page: number) {
265     this.updateUrl({ page });
266   }
267
268   handleListingTypeChange(val: ListingType) {
269     this.updateUrl({
270       listingType: val,
271       page: 1,
272     });
273   }
274
275   handleUnsubscribe(communityId: number) {
276     let form = new FollowCommunity({
277       community_id: communityId,
278       follow: false,
279       auth: auth().unwrap(),
280     });
281     WebSocketService.Instance.send(wsClient.followCommunity(form));
282   }
283
284   handleSubscribe(communityId: number) {
285     let form = new FollowCommunity({
286       community_id: communityId,
287       follow: true,
288       auth: auth().unwrap(),
289     });
290     WebSocketService.Instance.send(wsClient.followCommunity(form));
291   }
292
293   handleSearchChange(i: Communities, event: any) {
294     i.setState({ searchText: event.target.value });
295   }
296
297   handleSearchSubmit(i: Communities) {
298     const searchParamEncoded = encodeURIComponent(i.state.searchText);
299     i.context.router.history.push(
300       `/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
301     );
302   }
303
304   refetch() {
305     let listCommunitiesForm = new ListCommunities({
306       type_: Some(this.state.listingType),
307       sort: Some(SortType.TopMonth),
308       limit: Some(communityLimit),
309       page: Some(this.state.page),
310       auth: auth(false).ok(),
311     });
312
313     WebSocketService.Instance.send(
314       wsClient.listCommunities(listCommunitiesForm)
315     );
316   }
317
318   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
319     let pathSplit = req.path.split("/");
320     let type_: Option<ListingType> = Some(
321       pathSplit[3] ? ListingType[pathSplit[3]] : ListingType.Local
322     );
323     let page = Some(pathSplit[5] ? Number(pathSplit[5]) : 1);
324     let listCommunitiesForm = new ListCommunities({
325       type_,
326       sort: Some(SortType.TopMonth),
327       limit: Some(communityLimit),
328       page,
329       auth: req.auth,
330     });
331
332     return [req.client.listCommunities(listCommunitiesForm)];
333   }
334
335   parseMessage(msg: any) {
336     let op = wsUserOp(msg);
337     console.log(msg);
338     if (msg.error) {
339       toast(i18n.t(msg.error), "danger");
340       return;
341     } else if (op == UserOperation.ListCommunities) {
342       let data = wsJsonToRes<ListCommunitiesResponse>(
343         msg,
344         ListCommunitiesResponse
345       );
346       this.state.listCommunitiesResponse = Some(data);
347       this.state.loading = false;
348       window.scrollTo(0, 0);
349       this.setState(this.state);
350     } else if (op == UserOperation.FollowCommunity) {
351       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
352       this.state.listCommunitiesResponse.match({
353         some: res => {
354           let found = res.communities.find(
355             c => c.community.id == data.community_view.community.id
356           );
357           found.subscribed = data.community_view.subscribed;
358           found.counts.subscribers = data.community_view.counts.subscribers;
359         },
360         none: void 0,
361       });
362       this.setState(this.state);
363     }
364   }
365 }