]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/communities.tsx
313491ee0b26742fb47fb8caf9e50711f26506da
[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">{this.searchForm()}</div>
117             </div>
118
119             <div className="table-responsive">
120               <table
121                 id="community_table"
122                 className="table table-sm table-hover"
123               >
124                 <thead className="pointer">
125                   <tr>
126                     <th>{i18n.t("name")}</th>
127                     <th className="text-right">{i18n.t("subscribers")}</th>
128                     <th className="text-right">
129                       {i18n.t("users")} / {i18n.t("month")}
130                     </th>
131                     <th className="text-right d-none d-lg-table-cell">
132                       {i18n.t("posts")}
133                     </th>
134                     <th className="text-right d-none d-lg-table-cell">
135                       {i18n.t("comments")}
136                     </th>
137                     <th></th>
138                   </tr>
139                 </thead>
140                 <tbody>
141                   {this.state.listCommunitiesResponse.data.communities.map(
142                     cv => (
143                       <tr key={cv.community.id}>
144                         <td>
145                           <CommunityLink community={cv.community} />
146                         </td>
147                         <td className="text-right">
148                           {numToSI(cv.counts.subscribers)}
149                         </td>
150                         <td className="text-right">
151                           {numToSI(cv.counts.users_active_month)}
152                         </td>
153                         <td className="text-right d-none d-lg-table-cell">
154                           {numToSI(cv.counts.posts)}
155                         </td>
156                         <td className="text-right d-none d-lg-table-cell">
157                           {numToSI(cv.counts.comments)}
158                         </td>
159                         <td className="text-right">
160                           {cv.subscribed == "Subscribed" && (
161                             <button
162                               className="btn btn-link d-inline-block"
163                               onClick={linkEvent(
164                                 {
165                                   i: this,
166                                   communityId: cv.community.id,
167                                   follow: false,
168                                 },
169                                 this.handleFollow
170                               )}
171                             >
172                               {i18n.t("unsubscribe")}
173                             </button>
174                           )}
175                           {cv.subscribed === "NotSubscribed" && (
176                             <button
177                               className="btn btn-link d-inline-block"
178                               onClick={linkEvent(
179                                 {
180                                   i: this,
181                                   communityId: cv.community.id,
182                                   follow: true,
183                                 },
184                                 this.handleFollow
185                               )}
186                             >
187                               {i18n.t("subscribe")}
188                             </button>
189                           )}
190                           {cv.subscribed === "Pending" && (
191                             <div className="text-warning d-inline-block">
192                               {i18n.t("subscribe_pending")}
193                             </div>
194                           )}
195                         </td>
196                       </tr>
197                     )
198                   )}
199                 </tbody>
200               </table>
201             </div>
202             <Paginator page={page} onChange={this.handlePageChange} />
203           </div>
204         );
205       }
206     }
207   }
208
209   render() {
210     return (
211       <div className="container-lg">
212         <HtmlTags
213           title={this.documentTitle}
214           path={this.context.router.route.match.url}
215         />
216         {this.renderListings()}
217       </div>
218     );
219   }
220
221   searchForm() {
222     return (
223       <form
224         className="row justify-content-end"
225         onSubmit={linkEvent(this, this.handleSearchSubmit)}
226       >
227         <div className="col-auto">
228           <input
229             type="text"
230             id="communities-search"
231             className="form-control me-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         </div>
239         <div className="col-auto">
240           <label className="visually-hidden" htmlFor="communities-search">
241             {i18n.t("search")}
242           </label>
243           <button type="submit" className="btn btn-secondary mb-2">
244             <span>{i18n.t("search")}</span>
245           </button>
246         </div>
247       </form>
248     );
249   }
250
251   async updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
252     const { listingType: urlListingType, page: urlPage } =
253       this.getCommunitiesQueryParams();
254
255     const queryParams: QueryParams<CommunitiesProps> = {
256       listingType: listingType ?? urlListingType,
257       page: (page ?? urlPage)?.toString(),
258     };
259
260     this.props.history.push(`/communities${getQueryString(queryParams)}`);
261
262     await this.refetch();
263   }
264
265   handlePageChange(page: number) {
266     this.updateUrl({ page });
267   }
268
269   handleListingTypeChange(val: ListingType) {
270     this.updateUrl({
271       listingType: val,
272       page: 1,
273     });
274   }
275
276   handleSearchChange(i: Communities, event: any) {
277     i.setState({ searchText: event.target.value });
278   }
279
280   handleSearchSubmit(i: Communities, event: any) {
281     event.preventDefault();
282     const searchParamEncoded = encodeURIComponent(i.state.searchText);
283     i.context.router.history.push(`/search?q=${searchParamEncoded}`);
284   }
285
286   static async fetchInitialData({
287     query: { listingType, page },
288     client,
289     auth,
290   }: InitialFetchRequest<
291     QueryParams<CommunitiesProps>
292   >): Promise<CommunitiesData> {
293     const listCommunitiesForm: ListCommunities = {
294       type_: getListingTypeFromQuery(listingType),
295       sort: "TopMonth",
296       limit: communityLimit,
297       page: getPageFromString(page),
298       auth: auth,
299     };
300
301     return {
302       listCommunitiesResponse: await client.listCommunities(
303         listCommunitiesForm
304       ),
305     };
306   }
307
308   getCommunitiesQueryParams() {
309     return getQueryParams<CommunitiesProps>({
310       listingType: getListingTypeFromQuery,
311       page: getPageFromString,
312     });
313   }
314
315   async handleFollow(data: {
316     i: Communities;
317     communityId: number;
318     follow: boolean;
319   }) {
320     const res = await HttpService.client.followCommunity({
321       community_id: data.communityId,
322       follow: data.follow,
323       auth: myAuthRequired(),
324     });
325     data.i.findAndUpdateCommunity(res);
326   }
327
328   async refetch() {
329     this.setState({ listCommunitiesResponse: { state: "loading" } });
330
331     const { listingType, page } = this.getCommunitiesQueryParams();
332
333     this.setState({
334       listCommunitiesResponse: await HttpService.client.listCommunities({
335         type_: listingType,
336         sort: "TopMonth",
337         limit: communityLimit,
338         page,
339         auth: myAuth(),
340       }),
341     });
342
343     window.scrollTo(0, 0);
344   }
345
346   findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
347     this.setState(s => {
348       if (
349         s.listCommunitiesResponse.state == "success" &&
350         res.state == "success"
351       ) {
352         s.listCommunitiesResponse.data.communities = editCommunity(
353           res.data.community_view,
354           s.listCommunitiesResponse.data.communities
355         );
356       }
357       return s;
358     });
359   }
360 }