]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/site-form.tsx
Add show/hide button to password fields (#1861)
[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               <option value="browser-compact">
415                 {I18NextService.i18n.t("browser_default_compact")}
416               </option>
417               {this.props.themeList?.map(theme => (
418                 <option key={theme} value={theme}>
419                   {theme}
420                 </option>
421               ))}
422             </select>
423           </div>
424         </div>
425         {this.props.showLocal && (
426           <form className="mb-3 row">
427             <label className="col-sm-3 col-form-label">
428               {I18NextService.i18n.t("listing_type")}
429             </label>
430             <div className="col-sm-9">
431               <ListingTypeSelect
432                 type_={this.state.siteForm.default_post_listing_type ?? "Local"}
433                 showLocal
434                 showSubscribed={false}
435                 onChange={this.handleDefaultPostListingTypeChange}
436               />
437             </div>
438           </form>
439         )}
440         <div className="mb-3 row">
441           <div className="col-12">
442             <div className="form-check">
443               <input
444                 className="form-check-input"
445                 id="create-site-private-instance"
446                 type="checkbox"
447                 checked={this.state.siteForm.private_instance}
448                 onChange={linkEvent(this, this.handleSitePrivateInstance)}
449               />
450               <label
451                 className="form-check-label"
452                 htmlFor="create-site-private-instance"
453               >
454                 {I18NextService.i18n.t("private_instance")}
455               </label>
456             </div>
457           </div>
458         </div>
459         <div className="mb-3 row">
460           <div className="col-12">
461             <div className="form-check">
462               <input
463                 className="form-check-input"
464                 id="create-site-hide-modlog-mod-names"
465                 type="checkbox"
466                 checked={this.state.siteForm.hide_modlog_mod_names}
467                 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
468               />
469               <label
470                 className="form-check-label"
471                 htmlFor="create-site-hide-modlog-mod-names"
472               >
473                 {I18NextService.i18n.t("hide_modlog_mod_names")}
474               </label>
475             </div>
476           </div>
477         </div>
478         <div className="mb-3 row">
479           <label
480             className="col-12 col-form-label"
481             htmlFor="create-site-slur-filter-regex"
482           >
483             {I18NextService.i18n.t("slur_filter_regex")}
484           </label>
485           <div className="col-12">
486             <input
487               type="text"
488               id="create-site-slur-filter-regex"
489               placeholder="(word1|word2)"
490               className="form-control"
491               value={this.state.siteForm.slur_filter_regex}
492               onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
493               minLength={3}
494             />
495           </div>
496         </div>
497         <LanguageSelect
498           allLanguages={this.props.siteRes.all_languages}
499           siteLanguages={this.props.siteRes.discussion_languages}
500           selectedLanguageIds={this.state.siteForm.discussion_languages}
501           multiple={true}
502           onChange={this.handleDiscussionLanguageChange}
503           showAll
504         />
505         <div className="mb-3 row">
506           <label
507             className="col-12 col-form-label"
508             htmlFor="create-site-actor-name"
509           >
510             {I18NextService.i18n.t("actor_name_max_length")}
511           </label>
512           <div className="col-12">
513             <input
514               type="number"
515               id="create-site-actor-name"
516               className="form-control"
517               min={5}
518               value={this.state.siteForm.actor_name_max_length}
519               onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
520             />
521           </div>
522         </div>
523         <div className="mb-3 row">
524           <div className="col-12">
525             <div className="form-check">
526               <input
527                 className="form-check-input"
528                 id="create-site-federation-enabled"
529                 type="checkbox"
530                 checked={this.state.siteForm.federation_enabled}
531                 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
532               />
533               <label
534                 className="form-check-label"
535                 htmlFor="create-site-federation-enabled"
536               >
537                 {I18NextService.i18n.t("federation_enabled")}
538               </label>
539             </div>
540           </div>
541         </div>
542         {this.state.siteForm.federation_enabled && (
543           <>
544             <div className="mb-3 row">
545               {this.federatedInstanceSelect("allowed_instances")}
546               {this.federatedInstanceSelect("blocked_instances")}
547             </div>
548             <div className="mb-3 row">
549               <div className="col-12">
550                 <div className="form-check">
551                   <input
552                     className="form-check-input"
553                     id="create-site-federation-debug"
554                     type="checkbox"
555                     checked={this.state.siteForm.federation_debug}
556                     onChange={linkEvent(this, this.handleSiteFederationDebug)}
557                   />
558                   <label
559                     className="form-check-label"
560                     htmlFor="create-site-federation-debug"
561                   >
562                     {I18NextService.i18n.t("federation_debug")}
563                   </label>
564                 </div>
565               </div>
566             </div>
567           </>
568         )}
569         <div className="mb-3 row">
570           <div className="col-12">
571             <div className="form-check">
572               <input
573                 className="form-check-input"
574                 id="create-site-captcha-enabled"
575                 type="checkbox"
576                 checked={this.state.siteForm.captcha_enabled}
577                 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
578               />
579               <label
580                 className="form-check-label"
581                 htmlFor="create-site-captcha-enabled"
582               >
583                 {I18NextService.i18n.t("captcha_enabled")}
584               </label>
585             </div>
586           </div>
587         </div>
588         {this.state.siteForm.captcha_enabled && (
589           <div className="mb-3 row">
590             <div className="col-12">
591               <label
592                 className="form-check-label me-2"
593                 htmlFor="create-site-captcha-difficulty"
594               >
595                 {I18NextService.i18n.t("captcha_difficulty")}
596               </label>
597               <select
598                 id="create-site-captcha-difficulty"
599                 value={this.state.siteForm.captcha_difficulty}
600                 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
601                 className="form-select d-inline-block w-auto"
602               >
603                 <option value="easy">{I18NextService.i18n.t("easy")}</option>
604                 <option value="medium">
605                   {I18NextService.i18n.t("medium")}
606                 </option>
607                 <option value="hard">{I18NextService.i18n.t("hard")}</option>
608               </select>
609             </div>
610           </div>
611         )}
612         <div className="mb-3 row">
613           <div className="col-12">
614             <button
615               type="submit"
616               className="btn btn-secondary me-2"
617               disabled={this.props.loading}
618             >
619               {this.props.loading ? (
620                 <Spinner />
621               ) : siteSetup ? (
622                 capitalizeFirstLetter(I18NextService.i18n.t("save"))
623               ) : (
624                 capitalizeFirstLetter(I18NextService.i18n.t("create"))
625               )}
626             </button>
627           </div>
628         </div>
629       </form>
630     );
631   }
632
633   componentDidUpdate(
634     prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps>
635   ) {
636     if (
637       !(
638         deepEqual(prevProps.allowedInstances, this.props.allowedInstances) ||
639         deepEqual(prevProps.blockedInstances, this.props.blockedInstances)
640       )
641     ) {
642       this.setState({ siteForm: this.initSiteForm() });
643     }
644   }
645
646   federatedInstanceSelect(key: InstanceKey) {
647     const id = `create_site_${key}`;
648     const value = this.state.instance_select[key];
649     const selectedInstances = this.state.siteForm[key];
650     return (
651       <div className="col-12 col-md-6">
652         <label className="col-form-label" htmlFor={id}>
653           {I18NextService.i18n.t(key)}
654         </label>
655         <div className="d-flex justify-content-between align-items-center">
656           <input
657             type="text"
658             placeholder="instance.tld"
659             id={id}
660             className="form-control"
661             value={value}
662             onInput={linkEvent(key, this.handleInstanceTextChange)}
663             onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
664           />
665           <button
666             type="button"
667             className="btn btn-sm bg-success ms-2"
668             onClick={linkEvent(key, this.handleAddInstance)}
669             style={"width: 2rem; height: 2rem;"}
670             tabIndex={
671               -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
672             }
673           >
674             <Icon
675               icon="add"
676               classes="icon-inline text-light m-auto d-block position-static"
677             />
678           </button>
679         </div>
680         {selectedInstances && selectedInstances.length > 0 && (
681           <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
682             {selectedInstances.map(instance => (
683               <li
684                 key={instance}
685                 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
686               >
687                 <label className="d-block m-0 w-100 " htmlFor={instance}>
688                   <strong>{instance}</strong>
689                 </label>
690                 <button
691                   id={instance}
692                   type="button"
693                   style={"width: 2rem; height: 2rem;"}
694                   className="btn btn-sm bg-danger"
695                   onClick={linkEvent(
696                     { key, instance },
697                     this.handleRemoveInstance
698                   )}
699                 >
700                   <Icon
701                     icon="x"
702                     classes="icon-inline text-light m-auto d-block position-static"
703                   />
704                 </button>
705               </li>
706             ))}
707           </ul>
708         )}
709       </div>
710     );
711   }
712
713   handleInstanceTextChange(type: InstanceKey, event: any) {
714     this.setState(s => ({
715       ...s,
716       instance_select: {
717         ...s.instance_select,
718         [type]: event.target.value,
719       },
720     }));
721   }
722
723   handleInstanceEnterPress(
724     key: InstanceKey,
725     event: InfernoKeyboardEvent<HTMLInputElement>
726   ) {
727     if (event.code.toLowerCase() === "enter") {
728       event.preventDefault();
729
730       this.handleAddInstance(key);
731     }
732   }
733
734   handleSaveSiteSubmit(i: SiteForm, event: any) {
735     event.preventDefault();
736     const auth = myAuthRequired();
737     i.setState(s => ((s.siteForm.auth = auth), s));
738     i.setState({ submitted: true });
739
740     const stateSiteForm = i.state.siteForm;
741
742     let form: EditSite | CreateSite;
743
744     if (i.props.siteRes.site_view.local_site.site_setup) {
745       form = stateSiteForm;
746     } else {
747       form = {
748         name: stateSiteForm.name ?? "My site",
749         sidebar: stateSiteForm.sidebar,
750         description: stateSiteForm.description,
751         icon: stateSiteForm.icon,
752         banner: stateSiteForm.banner,
753         community_creation_admin_only:
754           stateSiteForm.community_creation_admin_only,
755         enable_nsfw: stateSiteForm.enable_nsfw,
756         enable_downvotes: stateSiteForm.enable_downvotes,
757         application_question: stateSiteForm.application_question,
758         registration_mode: stateSiteForm.registration_mode,
759         require_email_verification: stateSiteForm.require_email_verification,
760         private_instance: stateSiteForm.private_instance,
761         default_theme: stateSiteForm.default_theme,
762         default_post_listing_type: stateSiteForm.default_post_listing_type,
763         application_email_admins: stateSiteForm.application_email_admins,
764         hide_modlog_mod_names: stateSiteForm.hide_modlog_mod_names,
765         legal_information: stateSiteForm.legal_information,
766         slur_filter_regex: stateSiteForm.slur_filter_regex,
767         actor_name_max_length: stateSiteForm.actor_name_max_length,
768         rate_limit_message: stateSiteForm.rate_limit_message,
769         rate_limit_message_per_second:
770           stateSiteForm.rate_limit_message_per_second,
771         rate_limit_comment: stateSiteForm.rate_limit_comment,
772         rate_limit_comment_per_second:
773           stateSiteForm.rate_limit_comment_per_second,
774         rate_limit_image: stateSiteForm.rate_limit_image,
775         rate_limit_image_per_second: stateSiteForm.rate_limit_image_per_second,
776         rate_limit_post: stateSiteForm.rate_limit_post,
777         rate_limit_post_per_second: stateSiteForm.rate_limit_post_per_second,
778         rate_limit_register: stateSiteForm.rate_limit_register,
779         rate_limit_register_per_second:
780           stateSiteForm.rate_limit_register_per_second,
781         rate_limit_search: stateSiteForm.rate_limit_search,
782         rate_limit_search_per_second:
783           stateSiteForm.rate_limit_search_per_second,
784         federation_enabled: stateSiteForm.federation_enabled,
785         federation_debug: stateSiteForm.federation_debug,
786         captcha_enabled: stateSiteForm.captcha_enabled,
787         captcha_difficulty: stateSiteForm.captcha_difficulty,
788         allowed_instances: stateSiteForm.allowed_instances,
789         blocked_instances: stateSiteForm.blocked_instances,
790         discussion_languages: stateSiteForm.discussion_languages,
791         auth,
792       };
793     }
794
795     i.props.onSaveSite(form);
796   }
797
798   handleAddInstance(key: InstanceKey) {
799     const instance = this.state.instance_select[key].trim();
800
801     if (!validInstanceTLD(instance)) {
802       return;
803     }
804
805     if (!this.state.siteForm[key]?.includes(instance)) {
806       this.setState(s => ({
807         ...s,
808         siteForm: {
809           ...s.siteForm,
810           [key]: [...(s.siteForm[key] ?? []), instance],
811         },
812         instance_select: {
813           ...s.instance_select,
814           [key]: "",
815         },
816       }));
817
818       const oppositeKey: InstanceKey =
819         key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
820       if (this.state.siteForm[oppositeKey]?.includes(instance)) {
821         this.handleRemoveInstance({ key: oppositeKey, instance });
822       }
823     }
824   }
825
826   handleRemoveInstance({
827     key,
828     instance,
829   }: {
830     key: InstanceKey;
831     instance: string;
832   }) {
833     this.setState(s => ({
834       ...s,
835       siteForm: {
836         ...s.siteForm,
837         [key]: s.siteForm[key]?.filter(i => i !== instance),
838       },
839     }));
840   }
841
842   handleSiteNameChange(i: SiteForm, event: any) {
843     i.state.siteForm.name = event.target.value;
844     i.setState(i.state);
845   }
846
847   handleSiteSidebarChange(val: string) {
848     this.setState(s => ((s.siteForm.sidebar = val), s));
849   }
850
851   handleSiteLegalInfoChange(val: string) {
852     this.setState(s => ((s.siteForm.legal_information = val), s));
853   }
854
855   handleTaglineChange(i: SiteForm, index: number, val: string) {
856     const taglines = i.state.siteForm.taglines;
857     if (taglines) {
858       taglines[index] = val;
859       i.setState(i.state);
860     }
861   }
862
863   handleDeleteTaglineClick(
864     i: SiteForm,
865     index: number,
866     event: InfernoMouseEvent<HTMLButtonElement>
867   ) {
868     event.preventDefault();
869     const taglines = i.state.siteForm.taglines;
870     if (taglines) {
871       taglines.splice(index, 1);
872       i.state.siteForm.taglines = undefined;
873       i.setState(i.state);
874       i.state.siteForm.taglines = taglines;
875       i.setState(i.state);
876     }
877   }
878
879   handleAddTaglineClick(
880     i: SiteForm,
881     event: InfernoMouseEvent<HTMLButtonElement>
882   ) {
883     event.preventDefault();
884     if (!i.state.siteForm.taglines) {
885       i.state.siteForm.taglines = [];
886     }
887     i.state.siteForm.taglines.push("");
888     i.setState(i.state);
889   }
890
891   handleSiteApplicationQuestionChange(val: string) {
892     this.setState(s => ((s.siteForm.application_question = val), s));
893   }
894
895   handleSiteDescChange(i: SiteForm, event: any) {
896     i.state.siteForm.description = event.target.value;
897     i.setState(i.state);
898   }
899
900   handleSiteEnableNsfwChange(i: SiteForm, event: any) {
901     i.state.siteForm.enable_nsfw = event.target.checked;
902     i.setState(i.state);
903   }
904
905   handleSiteRegistrationModeChange(i: SiteForm, event: any) {
906     i.state.siteForm.registration_mode = event.target.value;
907     i.setState(i.state);
908   }
909
910   handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
911     i.state.siteForm.community_creation_admin_only = event.target.checked;
912     i.setState(i.state);
913   }
914
915   handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
916     i.state.siteForm.enable_downvotes = event.target.checked;
917     i.setState(i.state);
918   }
919
920   handleSiteRequireEmailVerification(i: SiteForm, event: any) {
921     i.state.siteForm.require_email_verification = event.target.checked;
922     i.setState(i.state);
923   }
924
925   handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
926     i.state.siteForm.application_email_admins = event.target.checked;
927     i.setState(i.state);
928   }
929
930   handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
931     i.state.siteForm.reports_email_admins = event.target.checked;
932     i.setState(i.state);
933   }
934
935   handleSitePrivateInstance(i: SiteForm, event: any) {
936     i.state.siteForm.private_instance = event.target.checked;
937     i.setState(i.state);
938   }
939
940   handleSiteHideModlogModNames(i: SiteForm, event: any) {
941     i.state.siteForm.hide_modlog_mod_names = event.target.checked;
942     i.setState(i.state);
943   }
944
945   handleSiteDefaultTheme(i: SiteForm, event: any) {
946     i.state.siteForm.default_theme = event.target.value;
947     i.setState(i.state);
948   }
949
950   handleIconUpload(url: string) {
951     this.setState(s => ((s.siteForm.icon = url), s));
952   }
953
954   handleIconRemove() {
955     this.setState(s => ((s.siteForm.icon = ""), s));
956   }
957
958   handleBannerUpload(url: string) {
959     this.setState(s => ((s.siteForm.banner = url), s));
960   }
961
962   handleBannerRemove() {
963     this.setState(s => ((s.siteForm.banner = ""), s));
964   }
965
966   handleSiteSlurFilterRegex(i: SiteForm, event: any) {
967     i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
968   }
969
970   handleSiteActorNameMaxLength(i: SiteForm, event: any) {
971     i.setState(
972       s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
973     );
974   }
975
976   handleSiteFederationEnabled(i: SiteForm, event: any) {
977     i.state.siteForm.federation_enabled = event.target.checked;
978     i.setState(i.state);
979   }
980
981   handleSiteFederationDebug(i: SiteForm, event: any) {
982     i.state.siteForm.federation_debug = event.target.checked;
983     i.setState(i.state);
984   }
985
986   handleSiteCaptchaEnabled(i: SiteForm, event: any) {
987     i.state.siteForm.captcha_enabled = event.target.checked;
988     i.setState(i.state);
989   }
990
991   handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
992     i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
993   }
994
995   handleDiscussionLanguageChange(val: number[]) {
996     this.setState(s => ((s.siteForm.discussion_languages = val), s));
997   }
998
999   handleDefaultPostListingTypeChange(val: ListingType) {
1000     this.setState(s => ((s.siteForm.default_post_listing_type = val), s));
1001   }
1002 }