]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/admin-settings.tsx
Merge remote-tracking branch 'origin/main' into feat/add-post-body-preview-to-desktop
[lemmy-ui.git] / src / shared / components / home / admin-settings.tsx
1 import {
2   fetchThemeList,
3   myAuthRequired,
4   setIsoData,
5   showLocal,
6 } from "@utils/app";
7 import { capitalizeFirstLetter } from "@utils/helpers";
8 import { RouteDataResponse } from "@utils/types";
9 import classNames from "classnames";
10 import { Component, linkEvent } from "inferno";
11 import {
12   BannedPersonsResponse,
13   CreateCustomEmoji,
14   DeleteCustomEmoji,
15   EditCustomEmoji,
16   EditSite,
17   GetFederatedInstancesResponse,
18   GetSiteResponse,
19   PersonView,
20 } from "lemmy-js-client";
21 import { InitialFetchRequest } from "../../interfaces";
22 import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
23 import { FirstLoadService, I18NextService } from "../../services";
24 import { HttpService, RequestState } from "../../services/HttpService";
25 import { toast } from "../../toast";
26 import { HtmlTags } from "../common/html-tags";
27 import { Spinner } from "../common/icon";
28 import Tabs from "../common/tabs";
29 import { PersonListing } from "../person/person-listing";
30 import { EmojiForm } from "./emojis-form";
31 import RateLimitForm from "./rate-limit-form";
32 import { SiteForm } from "./site-form";
33 import { TaglineForm } from "./tagline-form";
34
35 type AdminSettingsData = RouteDataResponse<{
36   bannedRes: BannedPersonsResponse;
37   instancesRes: GetFederatedInstancesResponse;
38 }>;
39
40 interface AdminSettingsState {
41   siteRes: GetSiteResponse;
42   banned: PersonView[];
43   currentTab: string;
44   instancesRes: RequestState<GetFederatedInstancesResponse>;
45   bannedRes: RequestState<BannedPersonsResponse>;
46   leaveAdminTeamRes: RequestState<GetSiteResponse>;
47   emojiLoading: boolean;
48   loading: boolean;
49   themeList: string[];
50   isIsomorphic: boolean;
51 }
52
53 export class AdminSettings extends Component<any, AdminSettingsState> {
54   private isoData = setIsoData<AdminSettingsData>(this.context);
55   state: AdminSettingsState = {
56     siteRes: this.isoData.site_res,
57     banned: [],
58     currentTab: "site",
59     bannedRes: { state: "empty" },
60     instancesRes: { state: "empty" },
61     leaveAdminTeamRes: { state: "empty" },
62     emojiLoading: false,
63     loading: false,
64     themeList: [],
65     isIsomorphic: false,
66   };
67
68   constructor(props: any, context: any) {
69     super(props, context);
70
71     this.handleEditSite = this.handleEditSite.bind(this);
72     this.handleEditEmoji = this.handleEditEmoji.bind(this);
73     this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
74     this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
75
76     // Only fetch the data if coming from another route
77     if (FirstLoadService.isFirstLoad) {
78       const { bannedRes, instancesRes } = this.isoData.routeData;
79
80       this.state = {
81         ...this.state,
82         bannedRes,
83         instancesRes,
84         isIsomorphic: true,
85       };
86     }
87   }
88
89   static async fetchInitialData({
90     auth,
91     client,
92   }: InitialFetchRequest): Promise<AdminSettingsData> {
93     return {
94       bannedRes: await client.getBannedPersons({
95         auth: auth as string,
96       }),
97       instancesRes: await client.getFederatedInstances({
98         auth: auth as string,
99       }),
100     };
101   }
102
103   async componentDidMount() {
104     if (!this.state.isIsomorphic) {
105       await this.fetchData();
106     }
107   }
108
109   get documentTitle(): string {
110     return `${I18NextService.i18n.t("admin_settings")} - ${
111       this.state.siteRes.site_view.site.name
112     }`;
113   }
114
115   render() {
116     const federationData =
117       this.state.instancesRes.state === "success"
118         ? this.state.instancesRes.data.federated_instances
119         : undefined;
120
121     return (
122       <div className="admin-settings container-lg">
123         <HtmlTags
124           title={this.documentTitle}
125           path={this.context.router.route.match.url}
126         />
127         <Tabs
128           tabs={[
129             {
130               key: "site",
131               label: I18NextService.i18n.t("site"),
132               getNode: isSelected => (
133                 <div
134                   className={classNames("tab-pane show", {
135                     active: isSelected,
136                   })}
137                   role="tabpanel"
138                   id="site-tab-pane"
139                 >
140                   <div className="row">
141                     <div className="col-12 col-md-6">
142                       <SiteForm
143                         showLocal={showLocal(this.isoData)}
144                         allowedInstances={federationData?.allowed}
145                         blockedInstances={federationData?.blocked}
146                         onSaveSite={this.handleEditSite}
147                         siteRes={this.state.siteRes}
148                         themeList={this.state.themeList}
149                         loading={this.state.loading}
150                       />
151                     </div>
152                     <div className="col-12 col-md-6">
153                       {this.admins()}
154                       {this.bannedUsers()}
155                     </div>
156                   </div>
157                 </div>
158               ),
159             },
160             {
161               key: "rate_limiting",
162               label: "Rate Limiting",
163               getNode: isSelected => (
164                 <div
165                   className={classNames("tab-pane", {
166                     active: isSelected,
167                   })}
168                   role="tabpanel"
169                   id="rate_limiting-tab-pane"
170                 >
171                   <RateLimitForm
172                     rateLimits={
173                       this.state.siteRes.site_view.local_site_rate_limit
174                     }
175                     onSaveSite={this.handleEditSite}
176                     loading={this.state.loading}
177                   />
178                 </div>
179               ),
180             },
181             {
182               key: "taglines",
183               label: I18NextService.i18n.t("taglines"),
184               getNode: isSelected => (
185                 <div
186                   className={classNames("tab-pane", {
187                     active: isSelected,
188                   })}
189                   role="tabpanel"
190                   id="taglines-tab-pane"
191                 >
192                   <div className="row">
193                     <TaglineForm
194                       taglines={this.state.siteRes.taglines}
195                       onSaveSite={this.handleEditSite}
196                       loading={this.state.loading}
197                     />
198                   </div>
199                 </div>
200               ),
201             },
202             {
203               key: "emojis",
204               label: I18NextService.i18n.t("emojis"),
205               getNode: isSelected => (
206                 <div
207                   className={classNames("tab-pane", {
208                     active: isSelected,
209                   })}
210                   role="tabpanel"
211                   id="emojis-tab-pane"
212                 >
213                   <div className="row">
214                     <EmojiForm
215                       onCreate={this.handleCreateEmoji}
216                       onDelete={this.handleDeleteEmoji}
217                       onEdit={this.handleEditEmoji}
218                       loading={this.state.emojiLoading}
219                     />
220                   </div>
221                 </div>
222               ),
223             },
224           ]}
225         />
226       </div>
227     );
228   }
229
230   async fetchData() {
231     this.setState({
232       bannedRes: { state: "loading" },
233       instancesRes: { state: "loading" },
234       themeList: [],
235     });
236
237     const auth = myAuthRequired();
238
239     const [bannedRes, instancesRes, themeList] = await Promise.all([
240       HttpService.client.getBannedPersons({ auth }),
241       HttpService.client.getFederatedInstances({ auth }),
242       fetchThemeList(),
243     ]);
244
245     this.setState({
246       bannedRes,
247       instancesRes,
248       themeList,
249     });
250   }
251
252   admins() {
253     return (
254       <>
255         <h5>{capitalizeFirstLetter(I18NextService.i18n.t("admins"))}</h5>
256         <ul className="list-unstyled">
257           {this.state.siteRes.admins.map(admin => (
258             <li key={admin.person.id} className="list-inline-item">
259               <PersonListing person={admin.person} />
260             </li>
261           ))}
262         </ul>
263         {this.leaveAdmin()}
264       </>
265     );
266   }
267
268   leaveAdmin() {
269     return (
270       <button
271         onClick={linkEvent(this, this.handleLeaveAdminTeam)}
272         className="btn btn-danger mb-2"
273       >
274         {this.state.leaveAdminTeamRes.state == "loading" ? (
275           <Spinner />
276         ) : (
277           I18NextService.i18n.t("leave_admin_team")
278         )}
279       </button>
280     );
281   }
282
283   bannedUsers() {
284     switch (this.state.bannedRes.state) {
285       case "loading":
286         return (
287           <h5>
288             <Spinner large />
289           </h5>
290         );
291       case "success": {
292         const bans = this.state.bannedRes.data.banned;
293         return (
294           <>
295             <h5>{I18NextService.i18n.t("banned_users")}</h5>
296             <ul className="list-unstyled">
297               {bans.map(banned => (
298                 <li key={banned.person.id} className="list-inline-item">
299                   <PersonListing person={banned.person} />
300                 </li>
301               ))}
302             </ul>
303           </>
304         );
305       }
306     }
307   }
308
309   async handleEditSite(form: EditSite) {
310     this.setState({ loading: true });
311
312     const editRes = await HttpService.client.editSite(form);
313
314     if (editRes.state === "success") {
315       this.setState(s => {
316         s.siteRes.site_view = editRes.data.site_view;
317         // TODO: Where to get taglines from?
318         s.siteRes.taglines = editRes.data.taglines;
319         return s;
320       });
321       toast(I18NextService.i18n.t("site_saved"));
322     }
323
324     this.setState({ loading: false });
325
326     return editRes;
327   }
328
329   handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
330     i.ctx.setState({ currentTab: i.tab });
331   }
332
333   async handleLeaveAdminTeam(i: AdminSettings) {
334     i.setState({ leaveAdminTeamRes: { state: "loading" } });
335     this.setState({
336       leaveAdminTeamRes: await HttpService.client.leaveAdmin({
337         auth: myAuthRequired(),
338       }),
339     });
340
341     if (this.state.leaveAdminTeamRes.state === "success") {
342       toast(I18NextService.i18n.t("left_admin_team"));
343       this.context.router.history.replace("/");
344     }
345   }
346
347   async handleEditEmoji(form: EditCustomEmoji) {
348     this.setState({ emojiLoading: true });
349
350     const res = await HttpService.client.editCustomEmoji(form);
351     if (res.state === "success") {
352       updateEmojiDataModel(res.data.custom_emoji);
353     }
354
355     this.setState({ emojiLoading: false });
356   }
357
358   async handleDeleteEmoji(form: DeleteCustomEmoji) {
359     this.setState({ emojiLoading: true });
360
361     const res = await HttpService.client.deleteCustomEmoji(form);
362     if (res.state === "success") {
363       removeFromEmojiDataModel(res.data.id);
364     }
365
366     this.setState({ emojiLoading: false });
367   }
368
369   async handleCreateEmoji(form: CreateCustomEmoji) {
370     this.setState({ emojiLoading: true });
371
372     const res = await HttpService.client.createCustomEmoji(form);
373     if (res.state === "success") {
374       updateEmojiDataModel(res.data.custom_emoji);
375     }
376
377     this.setState({ emojiLoading: false });
378   }
379 }