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