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