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