]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/site-form.tsx
fix(a11y): Change the look and behavior of some file upload fields
[lemmy-ui.git] / src / shared / components / home / site-form.tsx
1 import { myAuthRequired } from "@utils/app";
2 import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers";
3 import {
4   Component,
5   InfernoKeyboardEvent,
6   InfernoMouseEvent,
7   linkEvent,
8 } from "inferno";
9 import {
10   CreateSite,
11   EditSite,
12   GetSiteResponse,
13   Instance,
14   ListingType,
15 } from "lemmy-js-client";
16 import { I18NextService } from "../../services";
17 import { Icon, Spinner } from "../common/icon";
18 import { ImageUploadForm } from "../common/image-upload-form";
19 import { LanguageSelect } from "../common/language-select";
20 import { ListingTypeSelect } from "../common/listing-type-select";
21 import { MarkdownTextArea } from "../common/markdown-textarea";
22 import NavigationPrompt from "../common/navigation-prompt";
23
24 interface SiteFormProps {
25   blockedInstances?: Instance[];
26   allowedInstances?: Instance[];
27   showLocal?: boolean;
28   themeList?: string[];
29   onSaveSite(form: EditSite): void;
30   siteRes: GetSiteResponse;
31   loading: boolean;
32 }
33
34 interface SiteFormState {
35   siteForm: EditSite;
36   instance_select: {
37     allowed_instances: string;
38     blocked_instances: string;
39   };
40   submitted: boolean;
41 }
42
43 type InstanceKey = "allowed_instances" | "blocked_instances";
44
45 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
46   state: SiteFormState = {
47     siteForm: this.initSiteForm(),
48     instance_select: {
49       allowed_instances: "",
50       blocked_instances: "",
51     },
52     submitted: false,
53   };
54
55   initSiteForm(): EditSite {
56     const site = this.props.siteRes.site_view.site;
57     const ls = this.props.siteRes.site_view.local_site;
58     return {
59       name: site.name,
60       sidebar: site.sidebar,
61       description: site.description,
62       enable_downvotes: ls.enable_downvotes,
63       registration_mode: ls.registration_mode,
64       enable_nsfw: ls.enable_nsfw,
65       community_creation_admin_only: ls.community_creation_admin_only,
66       icon: site.icon,
67       banner: site.banner,
68       require_email_verification: ls.require_email_verification,
69       application_question: ls.application_question,
70       private_instance: ls.private_instance,
71       default_theme: ls.default_theme,
72       default_post_listing_type: ls.default_post_listing_type,
73       legal_information: ls.legal_information,
74       application_email_admins: ls.application_email_admins,
75       reports_email_admins: ls.reports_email_admins,
76       hide_modlog_mod_names: ls.hide_modlog_mod_names,
77       discussion_languages: this.props.siteRes.discussion_languages,
78       slur_filter_regex: ls.slur_filter_regex,
79       actor_name_max_length: ls.actor_name_max_length,
80       federation_enabled: ls.federation_enabled,
81       captcha_enabled: ls.captcha_enabled,
82       captcha_difficulty: ls.captcha_difficulty,
83       allowed_instances: this.props.allowedInstances?.map(i => i.domain),
84       blocked_instances: this.props.blockedInstances?.map(i => i.domain),
85       auth: "TODO",
86     };
87   }
88
89   constructor(props: any, context: any) {
90     super(props, context);
91
92     this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
93     this.handleSiteLegalInfoChange = this.handleSiteLegalInfoChange.bind(this);
94     this.handleSiteApplicationQuestionChange =
95       this.handleSiteApplicationQuestionChange.bind(this);
96
97     this.handleIconUpload = this.handleIconUpload.bind(this);
98     this.handleIconRemove = this.handleIconRemove.bind(this);
99
100     this.handleBannerUpload = this.handleBannerUpload.bind(this);
101     this.handleBannerRemove = this.handleBannerRemove.bind(this);
102
103     this.handleDefaultPostListingTypeChange =
104       this.handleDefaultPostListingTypeChange.bind(this);
105
106     this.handleDiscussionLanguageChange =
107       this.handleDiscussionLanguageChange.bind(this);
108
109     this.handleAddInstance = this.handleAddInstance.bind(this);
110     this.handleRemoveInstance = this.handleRemoveInstance.bind(this);
111
112     this.handleInstanceEnterPress = this.handleInstanceEnterPress.bind(this);
113     this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
114   }
115
116   render() {
117     const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
118     return (
119       <form
120         className="site-form"
121         onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}
122       >
123         <NavigationPrompt
124           when={
125             !this.props.loading &&
126             !siteSetup &&
127             !!(
128               this.state.siteForm.name ||
129               this.state.siteForm.sidebar ||
130               this.state.siteForm.application_question ||
131               this.state.siteForm.description
132             ) &&
133             !this.state.submitted
134           }
135         />
136         <h5>{`${
137           siteSetup
138             ? capitalizeFirstLetter(I18NextService.i18n.t("edit"))
139             : capitalizeFirstLetter(I18NextService.i18n.t("setup"))
140         } ${I18NextService.i18n.t("your_site")}`}</h5>
141         <div className="mb-3 row">
142           <label className="col-12 col-form-label" htmlFor="create-site-name">
143             {I18NextService.i18n.t("name")}
144           </label>
145           <div className="col-12">
146             <input
147               type="text"
148               id="create-site-name"
149               className="form-control"
150               value={this.state.siteForm.name}
151               onInput={linkEvent(this, this.handleSiteNameChange)}
152               required
153               minLength={3}
154               maxLength={20}
155             />
156           </div>
157         </div>
158         <div className="row mb-3">
159           <label className="col-sm-2 col-form-label">
160             {I18NextService.i18n.t("icon")}
161           </label>
162           <div className="col-sm-10">
163             <ImageUploadForm
164               uploadTitle={I18NextService.i18n.t("upload_icon")}
165               imageSrc={this.state.siteForm.icon}
166               onUpload={this.handleIconUpload}
167               onRemove={this.handleIconRemove}
168               rounded
169             />
170           </div>
171         </div>
172         <div className="row mb-3">
173           <label className="col-sm-2 col-form-label">
174             {I18NextService.i18n.t("banner")}
175           </label>
176           <div className="col-sm-10">
177             <ImageUploadForm
178               uploadTitle={I18NextService.i18n.t("upload_banner")}
179               imageSrc={this.state.siteForm.banner}
180               onUpload={this.handleBannerUpload}
181               onRemove={this.handleBannerRemove}
182             />
183           </div>
184         </div>
185         <div className="mb-3 row">
186           <label className="col-12 col-form-label" htmlFor="site-desc">
187             {I18NextService.i18n.t("description")}
188           </label>
189           <div className="col-12">
190             <input
191               type="text"
192               className="form-control"
193               id="site-desc"
194               value={this.state.siteForm.description}
195               onInput={linkEvent(this, this.handleSiteDescChange)}
196               maxLength={150}
197             />
198           </div>
199         </div>
200         <div className="mb-3 row">
201           <label className="col-12 col-form-label">
202             {I18NextService.i18n.t("sidebar")}
203           </label>
204           <div className="col-12">
205             <MarkdownTextArea
206               initialContent={this.state.siteForm.sidebar}
207               onContentChange={this.handleSiteSidebarChange}
208               hideNavigationWarnings
209               allLanguages={[]}
210               siteLanguages={[]}
211             />
212           </div>
213         </div>
214         <div className="mb-3 row">
215           <label className="col-12 col-form-label">
216             {I18NextService.i18n.t("legal_information")}
217           </label>
218           <div className="col-12">
219             <MarkdownTextArea
220               initialContent={this.state.siteForm.legal_information}
221               onContentChange={this.handleSiteLegalInfoChange}
222               hideNavigationWarnings
223               allLanguages={[]}
224               siteLanguages={[]}
225             />
226           </div>
227         </div>
228         <div className="mb-3 row">
229           <div className="col-12">
230             <div className="form-check">
231               <input
232                 className="form-check-input"
233                 id="create-site-downvotes"
234                 type="checkbox"
235                 checked={this.state.siteForm.enable_downvotes}
236                 onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
237               />
238               <label
239                 className="form-check-label"
240                 htmlFor="create-site-downvotes"
241               >
242                 {I18NextService.i18n.t("enable_downvotes")}
243               </label>
244             </div>
245           </div>
246         </div>
247         <div className="mb-3 row">
248           <div className="col-12">
249             <div className="form-check">
250               <input
251                 className="form-check-input"
252                 id="create-site-enable-nsfw"
253                 type="checkbox"
254                 checked={this.state.siteForm.enable_nsfw}
255                 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
256               />
257               <label
258                 className="form-check-label"
259                 htmlFor="create-site-enable-nsfw"
260               >
261                 {I18NextService.i18n.t("enable_nsfw")}
262               </label>
263             </div>
264           </div>
265         </div>
266         <div className="mb-3 row">
267           <div className="col-12">
268             <label
269               className="form-check-label me-2"
270               htmlFor="create-site-registration-mode"
271             >
272               {I18NextService.i18n.t("registration_mode")}
273             </label>
274             <select
275               id="create-site-registration-mode"
276               value={this.state.siteForm.registration_mode}
277               onChange={linkEvent(this, this.handleSiteRegistrationModeChange)}
278               className="form-select d-inline-block w-auto"
279             >
280               <option value={"RequireApplication"}>
281                 {I18NextService.i18n.t("require_registration_application")}
282               </option>
283               <option value={"Open"}>
284                 {I18NextService.i18n.t("open_registration")}
285               </option>
286               <option value={"Closed"}>
287                 {I18NextService.i18n.t("close_registration")}
288               </option>
289             </select>
290           </div>
291         </div>
292         {this.state.siteForm.registration_mode == "RequireApplication" && (
293           <div className="mb-3 row">
294             <label className="col-12 col-form-label">
295               {I18NextService.i18n.t("application_questionnaire")}
296             </label>
297             <div className="col-12">
298               <MarkdownTextArea
299                 initialContent={this.state.siteForm.application_question}
300                 onContentChange={this.handleSiteApplicationQuestionChange}
301                 hideNavigationWarnings
302                 allLanguages={[]}
303                 siteLanguages={[]}
304               />
305             </div>
306           </div>
307         )}
308         <div className="mb-3 row">
309           <div className="col-12">
310             <div className="form-check">
311               <input
312                 className="form-check-input"
313                 id="create-site-community-creation-admin-only"
314                 type="checkbox"
315                 checked={this.state.siteForm.community_creation_admin_only}
316                 onChange={linkEvent(
317                   this,
318                   this.handleSiteCommunityCreationAdminOnly
319                 )}
320               />
321               <label
322                 className="form-check-label"
323                 htmlFor="create-site-community-creation-admin-only"
324               >
325                 {I18NextService.i18n.t("community_creation_admin_only")}
326               </label>
327             </div>
328           </div>
329         </div>
330         <div className="mb-3 row">
331           <div className="col-12">
332             <div className="form-check">
333               <input
334                 className="form-check-input"
335                 id="create-site-require-email-verification"
336                 type="checkbox"
337                 checked={this.state.siteForm.require_email_verification}
338                 onChange={linkEvent(
339                   this,
340                   this.handleSiteRequireEmailVerification
341                 )}
342               />
343               <label
344                 className="form-check-label"
345                 htmlFor="create-site-require-email-verification"
346               >
347                 {I18NextService.i18n.t("require_email_verification")}
348               </label>
349             </div>
350           </div>
351         </div>
352         <div className="mb-3 row">
353           <div className="col-12">
354             <div className="form-check">
355               <input
356                 className="form-check-input"
357                 id="create-site-application-email-admins"
358                 type="checkbox"
359                 checked={this.state.siteForm.application_email_admins}
360                 onChange={linkEvent(
361                   this,
362                   this.handleSiteApplicationEmailAdmins
363                 )}
364               />
365               <label
366                 className="form-check-label"
367                 htmlFor="create-site-email-admins"
368               >
369                 {I18NextService.i18n.t("application_email_admins")}
370               </label>
371             </div>
372           </div>
373         </div>
374         <div className="mb-3 row">
375           <div className="col-12">
376             <div className="form-check">
377               <input
378                 className="form-check-input"
379                 id="create-site-reports-email-admins"
380                 type="checkbox"
381                 checked={this.state.siteForm.reports_email_admins}
382                 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
383               />
384               <label
385                 className="form-check-label"
386                 htmlFor="create-site-reports-email-admins"
387               >
388                 {I18NextService.i18n.t("reports_email_admins")}
389               </label>
390             </div>
391           </div>
392         </div>
393         <div className="mb-3 row">
394           <div className="col-12">
395             <label
396               className="form-check-label me-2"
397               htmlFor="create-site-default-theme"
398             >
399               {I18NextService.i18n.t("theme")}
400             </label>
401             <select
402               id="create-site-default-theme"
403               value={this.state.siteForm.default_theme}
404               onChange={linkEvent(this, this.handleSiteDefaultTheme)}
405               className="form-select d-inline-block w-auto"
406             >
407               <option value="browser">
408                 {I18NextService.i18n.t("browser_default")}
409               </option>
410               {this.props.themeList?.map(theme => (
411                 <option key={theme} value={theme}>
412                   {theme}
413                 </option>
414               ))}
415             </select>
416           </div>
417         </div>
418         {this.props.showLocal && (
419           <form className="mb-3 row">
420             <label className="col-sm-3 col-form-label">
421               {I18NextService.i18n.t("listing_type")}
422             </label>
423             <div className="col-sm-9">
424               <ListingTypeSelect
425                 type_={this.state.siteForm.default_post_listing_type ?? "Local"}
426                 showLocal
427                 showSubscribed={false}
428                 onChange={this.handleDefaultPostListingTypeChange}
429               />
430             </div>
431           </form>
432         )}
433         <div className="mb-3 row">
434           <div className="col-12">
435             <div className="form-check">
436               <input
437                 className="form-check-input"
438                 id="create-site-private-instance"
439                 type="checkbox"
440                 checked={this.state.siteForm.private_instance}
441                 onChange={linkEvent(this, this.handleSitePrivateInstance)}
442               />
443               <label
444                 className="form-check-label"
445                 htmlFor="create-site-private-instance"
446               >
447                 {I18NextService.i18n.t("private_instance")}
448               </label>
449             </div>
450           </div>
451         </div>
452         <div className="mb-3 row">
453           <div className="col-12">
454             <div className="form-check">
455               <input
456                 className="form-check-input"
457                 id="create-site-hide-modlog-mod-names"
458                 type="checkbox"
459                 checked={this.state.siteForm.hide_modlog_mod_names}
460                 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
461               />
462               <label
463                 className="form-check-label"
464                 htmlFor="create-site-hide-modlog-mod-names"
465               >
466                 {I18NextService.i18n.t("hide_modlog_mod_names")}
467               </label>
468             </div>
469           </div>
470         </div>
471         <div className="mb-3 row">
472           <label
473             className="col-12 col-form-label"
474             htmlFor="create-site-slur-filter-regex"
475           >
476             {I18NextService.i18n.t("slur_filter_regex")}
477           </label>
478           <div className="col-12">
479             <input
480               type="text"
481               id="create-site-slur-filter-regex"
482               placeholder="(word1|word2)"
483               className="form-control"
484               value={this.state.siteForm.slur_filter_regex}
485               onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
486               minLength={3}
487             />
488           </div>
489         </div>
490         <LanguageSelect
491           allLanguages={this.props.siteRes.all_languages}
492           siteLanguages={this.props.siteRes.discussion_languages}
493           selectedLanguageIds={this.state.siteForm.discussion_languages}
494           multiple={true}
495           onChange={this.handleDiscussionLanguageChange}
496           showAll
497         />
498         <div className="mb-3 row">
499           <label
500             className="col-12 col-form-label"
501             htmlFor="create-site-actor-name"
502           >
503             {I18NextService.i18n.t("actor_name_max_length")}
504           </label>
505           <div className="col-12">
506             <input
507               type="number"
508               id="create-site-actor-name"
509               className="form-control"
510               min={5}
511               value={this.state.siteForm.actor_name_max_length}
512               onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
513             />
514           </div>
515         </div>
516         <div className="mb-3 row">
517           <div className="col-12">
518             <div className="form-check">
519               <input
520                 className="form-check-input"
521                 id="create-site-federation-enabled"
522                 type="checkbox"
523                 checked={this.state.siteForm.federation_enabled}
524                 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
525               />
526               <label
527                 className="form-check-label"
528                 htmlFor="create-site-federation-enabled"
529               >
530                 {I18NextService.i18n.t("federation_enabled")}
531               </label>
532             </div>
533           </div>
534         </div>
535         {this.state.siteForm.federation_enabled && (
536           <>
537             <div className="mb-3 row">
538               {this.federatedInstanceSelect("allowed_instances")}
539               {this.federatedInstanceSelect("blocked_instances")}
540             </div>
541             <div className="mb-3 row">
542               <div className="col-12">
543                 <div className="form-check">
544                   <input
545                     className="form-check-input"
546                     id="create-site-federation-debug"
547                     type="checkbox"
548                     checked={this.state.siteForm.federation_debug}
549                     onChange={linkEvent(this, this.handleSiteFederationDebug)}
550                   />
551                   <label
552                     className="form-check-label"
553                     htmlFor="create-site-federation-debug"
554                   >
555                     {I18NextService.i18n.t("federation_debug")}
556                   </label>
557                 </div>
558               </div>
559             </div>
560           </>
561         )}
562         <div className="mb-3 row">
563           <div className="col-12">
564             <div className="form-check">
565               <input
566                 className="form-check-input"
567                 id="create-site-captcha-enabled"
568                 type="checkbox"
569                 checked={this.state.siteForm.captcha_enabled}
570                 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
571               />
572               <label
573                 className="form-check-label"
574                 htmlFor="create-site-captcha-enabled"
575               >
576                 {I18NextService.i18n.t("captcha_enabled")}
577               </label>
578             </div>
579           </div>
580         </div>
581         {this.state.siteForm.captcha_enabled && (
582           <div className="mb-3 row">
583             <div className="col-12">
584               <label
585                 className="form-check-label me-2"
586                 htmlFor="create-site-captcha-difficulty"
587               >
588                 {I18NextService.i18n.t("captcha_difficulty")}
589               </label>
590               <select
591                 id="create-site-captcha-difficulty"
592                 value={this.state.siteForm.captcha_difficulty}
593                 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
594                 className="form-select d-inline-block w-auto"
595               >
596                 <option value="easy">{I18NextService.i18n.t("easy")}</option>
597                 <option value="medium">
598                   {I18NextService.i18n.t("medium")}
599                 </option>
600                 <option value="hard">{I18NextService.i18n.t("hard")}</option>
601               </select>
602             </div>
603           </div>
604         )}
605         <div className="mb-3 row">
606           <div className="col-12">
607             <button
608               type="submit"
609               className="btn btn-secondary me-2"
610               disabled={this.props.loading}
611             >
612               {this.props.loading ? (
613                 <Spinner />
614               ) : siteSetup ? (
615                 capitalizeFirstLetter(I18NextService.i18n.t("save"))
616               ) : (
617                 capitalizeFirstLetter(I18NextService.i18n.t("create"))
618               )}
619             </button>
620           </div>
621         </div>
622       </form>
623     );
624   }
625
626   federatedInstanceSelect(key: InstanceKey) {
627     const id = `create_site_${key}`;
628     const value = this.state.instance_select[key];
629     const selectedInstances = this.state.siteForm[key];
630     return (
631       <div className="col-12 col-md-6">
632         <label className="col-form-label" htmlFor={id}>
633           {I18NextService.i18n.t(key)}
634         </label>
635         <div className="d-flex justify-content-between align-items-center">
636           <input
637             type="text"
638             placeholder="instance.tld"
639             id={id}
640             className="form-control"
641             value={value}
642             onInput={linkEvent(key, this.handleInstanceTextChange)}
643             onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
644           />
645           <button
646             type="button"
647             className="btn btn-sm bg-success ms-2"
648             onClick={linkEvent(key, this.handleAddInstance)}
649             style={"width: 2rem; height: 2rem;"}
650             tabIndex={
651               -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
652             }
653           >
654             <Icon
655               icon="add"
656               classes="icon-inline text-light m-auto d-block position-static"
657             />
658           </button>
659         </div>
660         {selectedInstances && selectedInstances.length > 0 && (
661           <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
662             {selectedInstances.map(instance => (
663               <li
664                 key={instance}
665                 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
666               >
667                 <label className="d-block m-0 w-100 " htmlFor={instance}>
668                   <strong>{instance}</strong>
669                 </label>
670                 <button
671                   id={instance}
672                   type="button"
673                   style={"width: 2rem; height: 2rem;"}
674                   className="btn btn-sm bg-danger"
675                   onClick={linkEvent(
676                     { key, instance },
677                     this.handleRemoveInstance
678                   )}
679                 >
680                   <Icon
681                     icon="x"
682                     classes="icon-inline text-light m-auto d-block position-static"
683                   />
684                 </button>
685               </li>
686             ))}
687           </ul>
688         )}
689       </div>
690     );
691   }
692
693   handleInstanceTextChange(type: InstanceKey, event: any) {
694     this.setState(s => ({
695       ...s,
696       instance_select: {
697         ...s.instance_select,
698         [type]: event.target.value,
699       },
700     }));
701   }
702
703   handleInstanceEnterPress(
704     key: InstanceKey,
705     event: InfernoKeyboardEvent<HTMLInputElement>
706   ) {
707     if (event.code.toLowerCase() === "enter") {
708       event.preventDefault();
709
710       this.handleAddInstance(key);
711     }
712   }
713
714   handleSaveSiteSubmit(i: SiteForm, event: any) {
715     event.preventDefault();
716     const auth = myAuthRequired();
717     i.setState(s => ((s.siteForm.auth = auth), s));
718     i.setState({ submitted: true });
719
720     const stateSiteForm = i.state.siteForm;
721
722     let form: EditSite | CreateSite;
723
724     if (i.props.siteRes.site_view.local_site.site_setup) {
725       form = stateSiteForm;
726     } else {
727       form = {
728         name: stateSiteForm.name ?? "My site",
729         sidebar: stateSiteForm.sidebar,
730         description: stateSiteForm.description,
731         icon: stateSiteForm.icon,
732         banner: stateSiteForm.banner,
733         community_creation_admin_only:
734           stateSiteForm.community_creation_admin_only,
735         enable_nsfw: stateSiteForm.enable_nsfw,
736         enable_downvotes: stateSiteForm.enable_downvotes,
737         application_question: stateSiteForm.application_question,
738         registration_mode: stateSiteForm.registration_mode,
739         require_email_verification: stateSiteForm.require_email_verification,
740         private_instance: stateSiteForm.private_instance,
741         default_theme: stateSiteForm.default_theme,
742         default_post_listing_type: stateSiteForm.default_post_listing_type,
743         application_email_admins: stateSiteForm.application_email_admins,
744         hide_modlog_mod_names: stateSiteForm.hide_modlog_mod_names,
745         legal_information: stateSiteForm.legal_information,
746         slur_filter_regex: stateSiteForm.slur_filter_regex,
747         actor_name_max_length: stateSiteForm.actor_name_max_length,
748         rate_limit_message: stateSiteForm.rate_limit_message,
749         rate_limit_message_per_second:
750           stateSiteForm.rate_limit_message_per_second,
751         rate_limit_comment: stateSiteForm.rate_limit_comment,
752         rate_limit_comment_per_second:
753           stateSiteForm.rate_limit_comment_per_second,
754         rate_limit_image: stateSiteForm.rate_limit_image,
755         rate_limit_image_per_second: stateSiteForm.rate_limit_image_per_second,
756         rate_limit_post: stateSiteForm.rate_limit_post,
757         rate_limit_post_per_second: stateSiteForm.rate_limit_post_per_second,
758         rate_limit_register: stateSiteForm.rate_limit_register,
759         rate_limit_register_per_second:
760           stateSiteForm.rate_limit_register_per_second,
761         rate_limit_search: stateSiteForm.rate_limit_search,
762         rate_limit_search_per_second:
763           stateSiteForm.rate_limit_search_per_second,
764         federation_enabled: stateSiteForm.federation_enabled,
765         federation_debug: stateSiteForm.federation_debug,
766         captcha_enabled: stateSiteForm.captcha_enabled,
767         captcha_difficulty: stateSiteForm.captcha_difficulty,
768         allowed_instances: stateSiteForm.allowed_instances,
769         blocked_instances: stateSiteForm.blocked_instances,
770         discussion_languages: stateSiteForm.discussion_languages,
771         auth,
772       };
773     }
774
775     i.props.onSaveSite(form);
776   }
777
778   handleAddInstance(key: InstanceKey) {
779     const instance = this.state.instance_select[key].trim();
780
781     if (!validInstanceTLD(instance)) {
782       return;
783     }
784
785     if (!this.state.siteForm[key]?.includes(instance)) {
786       this.setState(s => ({
787         ...s,
788         siteForm: {
789           ...s.siteForm,
790           [key]: [...(s.siteForm[key] ?? []), instance],
791         },
792         instance_select: {
793           ...s.instance_select,
794           [key]: "",
795         },
796       }));
797
798       const oppositeKey: InstanceKey =
799         key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
800       if (this.state.siteForm[oppositeKey]?.includes(instance)) {
801         this.handleRemoveInstance({ key: oppositeKey, instance });
802       }
803     }
804   }
805
806   handleRemoveInstance({
807     key,
808     instance,
809   }: {
810     key: InstanceKey;
811     instance: string;
812   }) {
813     this.setState(s => ({
814       ...s,
815       siteForm: {
816         ...s.siteForm,
817         [key]: s.siteForm[key]?.filter(i => i !== instance),
818       },
819     }));
820   }
821
822   handleSiteNameChange(i: SiteForm, event: any) {
823     i.state.siteForm.name = event.target.value;
824     i.setState(i.state);
825   }
826
827   handleSiteSidebarChange(val: string) {
828     this.setState(s => ((s.siteForm.sidebar = val), s));
829   }
830
831   handleSiteLegalInfoChange(val: string) {
832     this.setState(s => ((s.siteForm.legal_information = val), s));
833   }
834
835   handleTaglineChange(i: SiteForm, index: number, val: string) {
836     const taglines = i.state.siteForm.taglines;
837     if (taglines) {
838       taglines[index] = val;
839       i.setState(i.state);
840     }
841   }
842
843   handleDeleteTaglineClick(
844     i: SiteForm,
845     index: number,
846     event: InfernoMouseEvent<HTMLButtonElement>
847   ) {
848     event.preventDefault();
849     const taglines = i.state.siteForm.taglines;
850     if (taglines) {
851       taglines.splice(index, 1);
852       i.state.siteForm.taglines = undefined;
853       i.setState(i.state);
854       i.state.siteForm.taglines = taglines;
855       i.setState(i.state);
856     }
857   }
858
859   handleAddTaglineClick(
860     i: SiteForm,
861     event: InfernoMouseEvent<HTMLButtonElement>
862   ) {
863     event.preventDefault();
864     if (!i.state.siteForm.taglines) {
865       i.state.siteForm.taglines = [];
866     }
867     i.state.siteForm.taglines.push("");
868     i.setState(i.state);
869   }
870
871   handleSiteApplicationQuestionChange(val: string) {
872     this.setState(s => ((s.siteForm.application_question = val), s));
873   }
874
875   handleSiteDescChange(i: SiteForm, event: any) {
876     i.state.siteForm.description = event.target.value;
877     i.setState(i.state);
878   }
879
880   handleSiteEnableNsfwChange(i: SiteForm, event: any) {
881     i.state.siteForm.enable_nsfw = event.target.checked;
882     i.setState(i.state);
883   }
884
885   handleSiteRegistrationModeChange(i: SiteForm, event: any) {
886     i.state.siteForm.registration_mode = event.target.value;
887     i.setState(i.state);
888   }
889
890   handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
891     i.state.siteForm.community_creation_admin_only = event.target.checked;
892     i.setState(i.state);
893   }
894
895   handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
896     i.state.siteForm.enable_downvotes = event.target.checked;
897     i.setState(i.state);
898   }
899
900   handleSiteRequireEmailVerification(i: SiteForm, event: any) {
901     i.state.siteForm.require_email_verification = event.target.checked;
902     i.setState(i.state);
903   }
904
905   handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
906     i.state.siteForm.application_email_admins = event.target.checked;
907     i.setState(i.state);
908   }
909
910   handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
911     i.state.siteForm.reports_email_admins = event.target.checked;
912     i.setState(i.state);
913   }
914
915   handleSitePrivateInstance(i: SiteForm, event: any) {
916     i.state.siteForm.private_instance = event.target.checked;
917     i.setState(i.state);
918   }
919
920   handleSiteHideModlogModNames(i: SiteForm, event: any) {
921     i.state.siteForm.hide_modlog_mod_names = event.target.checked;
922     i.setState(i.state);
923   }
924
925   handleSiteDefaultTheme(i: SiteForm, event: any) {
926     i.state.siteForm.default_theme = event.target.value;
927     i.setState(i.state);
928   }
929
930   handleIconUpload(url: string) {
931     this.setState(s => ((s.siteForm.icon = url), s));
932   }
933
934   handleIconRemove() {
935     this.setState(s => ((s.siteForm.icon = ""), s));
936   }
937
938   handleBannerUpload(url: string) {
939     this.setState(s => ((s.siteForm.banner = url), s));
940   }
941
942   handleBannerRemove() {
943     this.setState(s => ((s.siteForm.banner = ""), s));
944   }
945
946   handleSiteSlurFilterRegex(i: SiteForm, event: any) {
947     i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
948   }
949
950   handleSiteActorNameMaxLength(i: SiteForm, event: any) {
951     i.setState(
952       s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
953     );
954   }
955
956   handleSiteFederationEnabled(i: SiteForm, event: any) {
957     i.state.siteForm.federation_enabled = event.target.checked;
958     i.setState(i.state);
959   }
960
961   handleSiteFederationDebug(i: SiteForm, event: any) {
962     i.state.siteForm.federation_debug = event.target.checked;
963     i.setState(i.state);
964   }
965
966   handleSiteCaptchaEnabled(i: SiteForm, event: any) {
967     i.state.siteForm.captcha_enabled = event.target.checked;
968     i.setState(i.state);
969   }
970
971   handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
972     i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
973   }
974
975   handleDiscussionLanguageChange(val: number[]) {
976     this.setState(s => ((s.siteForm.discussion_languages = val), s));
977   }
978
979   handleDefaultPostListingTypeChange(val: ListingType) {
980     this.setState(s => ((s.siteForm.default_post_listing_type = val), s));
981   }
982 }