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