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