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