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