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