]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/admin-settings.tsx
Merge branch 'main' into remove_markdown_it_emoji
[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 { i18n } from "../../i18next";
22 import { InitialFetchRequest } from "../../interfaces";
23 import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
24 import { FirstLoadService } from "../../services/FirstLoadService";
25 import { HttpService, RequestState } from "../../services/HttpService";
26 import { toast } from "../../toast";
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="admin-settings 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: isSelected => (
134                 <div
135                   className={classNames("tab-pane show", {
136                     active: isSelected,
137                   })}
138                   role="tabpanel"
139                   id="site-tab-pane"
140                 >
141                   <div className="row">
142                     <div className="col-12 col-md-6">
143                       <SiteForm
144                         showLocal={showLocal(this.isoData)}
145                         allowedInstances={federationData?.allowed}
146                         blockedInstances={federationData?.blocked}
147                         onSaveSite={this.handleEditSite}
148                         siteRes={this.state.siteRes}
149                         themeList={this.state.themeList}
150                         loading={this.state.loading}
151                       />
152                     </div>
153                     <div className="col-12 col-md-6">
154                       {this.admins()}
155                       {this.bannedUsers()}
156                     </div>
157                   </div>
158                 </div>
159               ),
160             },
161             {
162               key: "rate_limiting",
163               label: "Rate Limiting",
164               getNode: isSelected => (
165                 <div
166                   className={classNames("tab-pane", {
167                     active: isSelected,
168                   })}
169                   role="tabpanel"
170                   id="rate_limiting-tab-pane"
171                 >
172                   <RateLimitForm
173                     rateLimits={
174                       this.state.siteRes.site_view.local_site_rate_limit
175                     }
176                     onSaveSite={this.handleEditSite}
177                     loading={this.state.loading}
178                   />
179                 </div>
180               ),
181             },
182             {
183               key: "taglines",
184               label: i18n.t("taglines"),
185               getNode: isSelected => (
186                 <div
187                   className={classNames("tab-pane", {
188                     active: isSelected,
189                   })}
190                   role="tabpanel"
191                   id="taglines-tab-pane"
192                 >
193                   <div className="row">
194                     <TaglineForm
195                       taglines={this.state.siteRes.taglines}
196                       onSaveSite={this.handleEditSite}
197                       loading={this.state.loading}
198                     />
199                   </div>
200                 </div>
201               ),
202             },
203             {
204               key: "emojis",
205               label: i18n.t("emojis"),
206               getNode: isSelected => (
207                 <div
208                   className={classNames("tab-pane", {
209                     active: isSelected,
210                   })}
211                   role="tabpanel"
212                   id="emojis-tab-pane"
213                 >
214                   <div className="row">
215                     <EmojiForm
216                       onCreate={this.handleCreateEmoji}
217                       onDelete={this.handleDeleteEmoji}
218                       onEdit={this.handleEditEmoji}
219                       loading={this.state.emojiLoading}
220                     />
221                   </div>
222                 </div>
223               ),
224             },
225           ]}
226         />
227       </div>
228     );
229   }
230
231   async fetchData() {
232     this.setState({
233       bannedRes: { state: "loading" },
234       instancesRes: { state: "loading" },
235       themeList: [],
236     });
237
238     const auth = myAuthRequired();
239
240     const [bannedRes, instancesRes, themeList] = await Promise.all([
241       HttpService.client.getBannedPersons({ auth }),
242       HttpService.client.getFederatedInstances({ auth }),
243       fetchThemeList(),
244     ]);
245
246     this.setState({
247       bannedRes,
248       instancesRes,
249       themeList,
250     });
251   }
252
253   admins() {
254     return (
255       <>
256         <h5>{capitalizeFirstLetter(i18n.t("admins"))}</h5>
257         <ul className="list-unstyled">
258           {this.state.siteRes.admins.map(admin => (
259             <li key={admin.person.id} className="list-inline-item">
260               <PersonListing person={admin.person} />
261             </li>
262           ))}
263         </ul>
264         {this.leaveAdmin()}
265       </>
266     );
267   }
268
269   leaveAdmin() {
270     return (
271       <button
272         onClick={linkEvent(this, this.handleLeaveAdminTeam)}
273         className="btn btn-danger mb-2"
274       >
275         {this.state.leaveAdminTeamRes.state == "loading" ? (
276           <Spinner />
277         ) : (
278           i18n.t("leave_admin_team")
279         )}
280       </button>
281     );
282   }
283
284   bannedUsers() {
285     switch (this.state.bannedRes.state) {
286       case "loading":
287         return (
288           <h5>
289             <Spinner large />
290           </h5>
291         );
292       case "success": {
293         const bans = this.state.bannedRes.data.banned;
294         return (
295           <>
296             <h5>{i18n.t("banned_users")}</h5>
297             <ul className="list-unstyled">
298               {bans.map(banned => (
299                 <li key={banned.person.id} className="list-inline-item">
300                   <PersonListing person={banned.person} />
301                 </li>
302               ))}
303             </ul>
304           </>
305         );
306       }
307     }
308   }
309
310   async handleEditSite(form: EditSite) {
311     this.setState({ loading: true });
312
313     const editRes = await HttpService.client.editSite(form);
314
315     if (editRes.state === "success") {
316       this.setState(s => {
317         s.siteRes.site_view = editRes.data.site_view;
318         // TODO: Where to get taglines from?
319         s.siteRes.taglines = editRes.data.taglines;
320         return s;
321       });
322       toast(i18n.t("site_saved"));
323     }
324
325     this.setState({ loading: false });
326
327     return editRes;
328   }
329
330   handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
331     i.ctx.setState({ currentTab: i.tab });
332   }
333
334   async handleLeaveAdminTeam(i: AdminSettings) {
335     i.setState({ leaveAdminTeamRes: { state: "loading" } });
336     this.setState({
337       leaveAdminTeamRes: await HttpService.client.leaveAdmin({
338         auth: myAuthRequired(),
339       }),
340     });
341
342     if (this.state.leaveAdminTeamRes.state === "success") {
343       toast(i18n.t("left_admin_team"));
344       this.context.router.history.replace("/");
345     }
346   }
347
348   async handleEditEmoji(form: EditCustomEmoji) {
349     this.setState({ emojiLoading: true });
350
351     const res = await HttpService.client.editCustomEmoji(form);
352     if (res.state === "success") {
353       updateEmojiDataModel(res.data.custom_emoji);
354     }
355
356     this.setState({ emojiLoading: false });
357   }
358
359   async handleDeleteEmoji(form: DeleteCustomEmoji) {
360     this.setState({ emojiLoading: true });
361
362     const res = await HttpService.client.deleteCustomEmoji(form);
363     if (res.state === "success") {
364       removeFromEmojiDataModel(res.data.id);
365     }
366
367     this.setState({ emojiLoading: false });
368   }
369
370   async handleCreateEmoji(form: CreateCustomEmoji) {
371     this.setState({ emojiLoading: true });
372
373     const res = await HttpService.client.createCustomEmoji(form);
374     if (res.state === "success") {
375       updateEmojiDataModel(res.data.custom_emoji);
376     }
377
378     this.setState({ emojiLoading: false });
379   }
380 }