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