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