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