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