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