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