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