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