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