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