]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/communities.tsx
9a4e836a46c73647d28974267648512dd3af9d0a
[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 { i18n } from "../../i18next";
25 import { InitialFetchRequest } from "../../interfaces";
26 import { FirstLoadService } from "../../services/FirstLoadService";
27 import { HttpService, RequestState } from "../../services/HttpService";
28 import { HtmlTags } from "../common/html-tags";
29 import { Spinner } from "../common/icon";
30 import { ListingTypeSelect } from "../common/listing-type-select";
31 import { Paginator } from "../common/paginator";
32 import { CommunityLink } from "./community-link";
33
34 const communityLimit = 50;
35
36 type CommunitiesData = RouteDataResponse<{
37   listCommunitiesResponse: ListCommunitiesResponse;
38 }>;
39
40 interface CommunitiesState {
41   listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
42   siteRes: GetSiteResponse;
43   searchText: string;
44   isIsomorphic: boolean;
45 }
46
47 interface CommunitiesProps {
48   listingType: ListingType;
49   page: number;
50 }
51
52 function getListingTypeFromQuery(listingType?: string): ListingType {
53   return listingType ? (listingType as ListingType) : "Local";
54 }
55
56 export class Communities extends Component<any, CommunitiesState> {
57   private isoData = setIsoData<CommunitiesData>(this.context);
58   state: CommunitiesState = {
59     listCommunitiesResponse: { state: "empty" },
60     siteRes: this.isoData.site_res,
61     searchText: "",
62     isIsomorphic: false,
63   };
64
65   constructor(props: any, context: any) {
66     super(props, context);
67     this.handlePageChange = this.handlePageChange.bind(this);
68     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
69
70     // Only fetch the data if coming from another route
71     if (FirstLoadService.isFirstLoad) {
72       const { listCommunitiesResponse } = this.isoData.routeData;
73
74       this.state = {
75         ...this.state,
76         listCommunitiesResponse,
77         isIsomorphic: true,
78       };
79     }
80   }
81
82   async componentDidMount() {
83     if (!this.state.isIsomorphic) {
84       await this.refetch();
85     }
86   }
87
88   get documentTitle(): string {
89     return `${i18n.t("communities")} - ${
90       this.state.siteRes.site_view.site.name
91     }`;
92   }
93
94   renderListings() {
95     switch (this.state.listCommunitiesResponse.state) {
96       case "loading":
97         return (
98           <h5>
99             <Spinner large />
100           </h5>
101         );
102       case "success": {
103         const { listingType, page } = this.getCommunitiesQueryParams();
104         return (
105           <div>
106             <h1 className="h4">{i18n.t("list_of_communities")}</h1>
107             <div className="row g-2 justify-content-between">
108               <div className="col-auto">
109                 <ListingTypeSelect
110                   type_={listingType}
111                   showLocal={showLocal(this.isoData)}
112                   showSubscribed
113                   onChange={this.handleListingTypeChange}
114                 />
115               </div>
116               <div className="col-auto">{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="communities 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 mb-2"
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"
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">
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 }