13 } from "lemmy-js-client";
14 import { i18n } from "../../i18next";
16 capitalizeFirstLetter,
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 import NavigationPrompt from "../common/navigation-prompt";
27 interface SiteFormProps {
28 blockedInstances?: Instance[];
29 allowedInstances?: Instance[];
32 onSaveSite(form: EditSite): void;
33 siteRes: GetSiteResponse;
37 interface SiteFormState {
40 allowed_instances: string;
41 blocked_instances: string;
46 type InstanceKey = "allowed_instances" | "blocked_instances";
48 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
49 state: SiteFormState = {
50 siteForm: this.initSiteForm(),
52 allowed_instances: "",
53 blocked_instances: "",
58 initSiteForm(): EditSite {
59 const site = this.props.siteRes.site_view.site;
60 const ls = this.props.siteRes.site_view.local_site;
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,
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 federation_worker_count: ls.federation_worker_count,
85 captcha_enabled: ls.captcha_enabled,
86 captcha_difficulty: ls.captcha_difficulty,
87 allowed_instances: this.props.allowedInstances?.map(i => i.domain),
88 blocked_instances: this.props.blockedInstances?.map(i => i.domain),
93 constructor(props: any, context: any) {
94 super(props, context);
96 this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
97 this.handleSiteLegalInfoChange = this.handleSiteLegalInfoChange.bind(this);
98 this.handleSiteApplicationQuestionChange =
99 this.handleSiteApplicationQuestionChange.bind(this);
101 this.handleIconUpload = this.handleIconUpload.bind(this);
102 this.handleIconRemove = this.handleIconRemove.bind(this);
104 this.handleBannerUpload = this.handleBannerUpload.bind(this);
105 this.handleBannerRemove = this.handleBannerRemove.bind(this);
107 this.handleDefaultPostListingTypeChange =
108 this.handleDefaultPostListingTypeChange.bind(this);
110 this.handleDiscussionLanguageChange =
111 this.handleDiscussionLanguageChange.bind(this);
113 this.handleAddInstance = this.handleAddInstance.bind(this);
114 this.handleRemoveInstance = this.handleRemoveInstance.bind(this);
116 this.handleInstanceEnterPress = this.handleInstanceEnterPress.bind(this);
117 this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
121 const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
124 className="site-form"
125 onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}
129 !this.props.loading &&
132 this.state.siteForm.name ||
133 this.state.siteForm.sidebar ||
134 this.state.siteForm.application_question ||
135 this.state.siteForm.description
137 !this.state.submitted
142 ? capitalizeFirstLetter(i18n.t("edit"))
143 : capitalizeFirstLetter(i18n.t("setup"))
144 } ${i18n.t("your_site")}`}</h5>
145 <div className="mb-3 row">
146 <label className="col-12 col-form-label" htmlFor="create-site-name">
149 <div className="col-12">
152 id="create-site-name"
153 className="form-control"
154 value={this.state.siteForm.name}
155 onInput={linkEvent(this, this.handleSiteNameChange)}
162 <div className="input-group mb-3">
163 <label className="me-2 col-form-label">{i18n.t("icon")}</label>
165 uploadTitle={i18n.t("upload_icon")}
166 imageSrc={this.state.siteForm.icon}
167 onUpload={this.handleIconUpload}
168 onRemove={this.handleIconRemove}
172 <div className="input-group mb-3">
173 <label className="me-2 col-form-label">{i18n.t("banner")}</label>
175 uploadTitle={i18n.t("upload_banner")}
176 imageSrc={this.state.siteForm.banner}
177 onUpload={this.handleBannerUpload}
178 onRemove={this.handleBannerRemove}
181 <div className="mb-3 row">
182 <label className="col-12 col-form-label" htmlFor="site-desc">
183 {i18n.t("description")}
185 <div className="col-12">
188 className="form-control"
190 value={this.state.siteForm.description}
191 onInput={linkEvent(this, this.handleSiteDescChange)}
196 <div className="mb-3 row">
197 <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
198 <div className="col-12">
200 initialContent={this.state.siteForm.sidebar}
201 onContentChange={this.handleSiteSidebarChange}
202 hideNavigationWarnings
208 <div className="mb-3 row">
209 <label className="col-12 col-form-label">
210 {i18n.t("legal_information")}
212 <div className="col-12">
214 initialContent={this.state.siteForm.legal_information}
215 onContentChange={this.handleSiteLegalInfoChange}
216 hideNavigationWarnings
222 <div className="mb-3 row">
223 <div className="col-12">
224 <div className="form-check">
226 className="form-check-input"
227 id="create-site-downvotes"
229 checked={this.state.siteForm.enable_downvotes}
230 onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
233 className="form-check-label"
234 htmlFor="create-site-downvotes"
236 {i18n.t("enable_downvotes")}
241 <div className="mb-3 row">
242 <div className="col-12">
243 <div className="form-check">
245 className="form-check-input"
246 id="create-site-enable-nsfw"
248 checked={this.state.siteForm.enable_nsfw}
249 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
252 className="form-check-label"
253 htmlFor="create-site-enable-nsfw"
255 {i18n.t("enable_nsfw")}
260 <div className="mb-3 row">
261 <div className="col-12">
263 className="form-check-label me-2"
264 htmlFor="create-site-registration-mode"
266 {i18n.t("registration_mode")}
269 id="create-site-registration-mode"
270 value={this.state.siteForm.registration_mode}
271 onChange={linkEvent(this, this.handleSiteRegistrationModeChange)}
272 className="form-select d-inline-block w-auto"
274 <option value={"RequireApplication"}>
275 {i18n.t("require_registration_application")}
277 <option value={"Open"}>{i18n.t("open_registration")}</option>
278 <option value={"Closed"}>{i18n.t("close_registration")}</option>
282 {this.state.siteForm.registration_mode == "RequireApplication" && (
283 <div className="mb-3 row">
284 <label className="col-12 col-form-label">
285 {i18n.t("application_questionnaire")}
287 <div className="col-12">
289 initialContent={this.state.siteForm.application_question}
290 onContentChange={this.handleSiteApplicationQuestionChange}
291 hideNavigationWarnings
298 <div className="mb-3 row">
299 <div className="col-12">
300 <div className="form-check">
302 className="form-check-input"
303 id="create-site-community-creation-admin-only"
305 checked={this.state.siteForm.community_creation_admin_only}
308 this.handleSiteCommunityCreationAdminOnly
312 className="form-check-label"
313 htmlFor="create-site-community-creation-admin-only"
315 {i18n.t("community_creation_admin_only")}
320 <div className="mb-3 row">
321 <div className="col-12">
322 <div className="form-check">
324 className="form-check-input"
325 id="create-site-require-email-verification"
327 checked={this.state.siteForm.require_email_verification}
330 this.handleSiteRequireEmailVerification
334 className="form-check-label"
335 htmlFor="create-site-require-email-verification"
337 {i18n.t("require_email_verification")}
342 <div className="mb-3 row">
343 <div className="col-12">
344 <div className="form-check">
346 className="form-check-input"
347 id="create-site-application-email-admins"
349 checked={this.state.siteForm.application_email_admins}
352 this.handleSiteApplicationEmailAdmins
356 className="form-check-label"
357 htmlFor="create-site-email-admins"
359 {i18n.t("application_email_admins")}
364 <div className="mb-3 row">
365 <div className="col-12">
366 <div className="form-check">
368 className="form-check-input"
369 id="create-site-reports-email-admins"
371 checked={this.state.siteForm.reports_email_admins}
372 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
375 className="form-check-label"
376 htmlFor="create-site-reports-email-admins"
378 {i18n.t("reports_email_admins")}
383 <div className="mb-3 row">
384 <div className="col-12">
386 className="form-check-label me-2"
387 htmlFor="create-site-default-theme"
392 id="create-site-default-theme"
393 value={this.state.siteForm.default_theme}
394 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
395 className="form-select d-inline-block w-auto"
397 <option value="browser">{i18n.t("browser_default")}</option>
398 {this.props.themeList?.map(theme => (
399 <option key={theme} value={theme}>
406 {this.props.showLocal && (
407 <form className="mb-3 row">
408 <label className="col-sm-3 col-form-label">
409 {i18n.t("listing_type")}
411 <div className="col-sm-9">
413 type_={this.state.siteForm.default_post_listing_type ?? "Local"}
415 showSubscribed={false}
416 onChange={this.handleDefaultPostListingTypeChange}
421 <div className="mb-3 row">
422 <div className="col-12">
423 <div className="form-check">
425 className="form-check-input"
426 id="create-site-private-instance"
428 checked={this.state.siteForm.private_instance}
429 onChange={linkEvent(this, this.handleSitePrivateInstance)}
432 className="form-check-label"
433 htmlFor="create-site-private-instance"
435 {i18n.t("private_instance")}
440 <div className="mb-3 row">
441 <div className="col-12">
442 <div className="form-check">
444 className="form-check-input"
445 id="create-site-hide-modlog-mod-names"
447 checked={this.state.siteForm.hide_modlog_mod_names}
448 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
451 className="form-check-label"
452 htmlFor="create-site-hide-modlog-mod-names"
454 {i18n.t("hide_modlog_mod_names")}
459 <div className="mb-3 row">
461 className="col-12 col-form-label"
462 htmlFor="create-site-slur-filter-regex"
464 {i18n.t("slur_filter_regex")}
466 <div className="col-12">
469 id="create-site-slur-filter-regex"
470 placeholder="(word1|word2)"
471 className="form-control"
472 value={this.state.siteForm.slur_filter_regex}
473 onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
479 allLanguages={this.props.siteRes.all_languages}
480 siteLanguages={this.props.siteRes.discussion_languages}
481 selectedLanguageIds={this.state.siteForm.discussion_languages}
483 onChange={this.handleDiscussionLanguageChange}
486 <div className="mb-3 row">
488 className="col-12 col-form-label"
489 htmlFor="create-site-actor-name"
491 {i18n.t("actor_name_max_length")}
493 <div className="col-12">
496 id="create-site-actor-name"
497 className="form-control"
499 value={this.state.siteForm.actor_name_max_length}
500 onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
504 <div className="mb-3 row">
505 <div className="col-12">
506 <div className="form-check">
508 className="form-check-input"
509 id="create-site-federation-enabled"
511 checked={this.state.siteForm.federation_enabled}
512 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
515 className="form-check-label"
516 htmlFor="create-site-federation-enabled"
518 {i18n.t("federation_enabled")}
523 {this.state.siteForm.federation_enabled && (
525 <div className="mb-3 row">
526 {this.federatedInstanceSelect("allowed_instances")}
527 {this.federatedInstanceSelect("blocked_instances")}
529 <div className="mb-3 row">
530 <div className="col-12">
531 <div className="form-check">
533 className="form-check-input"
534 id="create-site-federation-debug"
536 checked={this.state.siteForm.federation_debug}
537 onChange={linkEvent(this, this.handleSiteFederationDebug)}
540 className="form-check-label"
541 htmlFor="create-site-federation-debug"
543 {i18n.t("federation_debug")}
548 <div className="mb-3 row">
550 className="col-12 col-form-label"
551 htmlFor="create-site-federation-worker-count"
553 {i18n.t("federation_worker_count")}
555 <div className="col-12">
558 id="create-site-federation-worker-count"
559 className="form-control"
561 value={this.state.siteForm.federation_worker_count}
564 this.handleSiteFederationWorkerCount
571 <div className="mb-3 row">
572 <div className="col-12">
573 <div className="form-check">
575 className="form-check-input"
576 id="create-site-captcha-enabled"
578 checked={this.state.siteForm.captcha_enabled}
579 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
582 className="form-check-label"
583 htmlFor="create-site-captcha-enabled"
585 {i18n.t("captcha_enabled")}
590 {this.state.siteForm.captcha_enabled && (
591 <div className="mb-3 row">
592 <div className="col-12">
594 className="form-check-label me-2"
595 htmlFor="create-site-captcha-difficulty"
597 {i18n.t("captcha_difficulty")}
600 id="create-site-captcha-difficulty"
601 value={this.state.siteForm.captcha_difficulty}
602 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
603 className="form-select d-inline-block w-auto"
605 <option value="easy">{i18n.t("easy")}</option>
606 <option value="medium">{i18n.t("medium")}</option>
607 <option value="hard">{i18n.t("hard")}</option>
612 <div className="mb-3 row">
613 <div className="col-12">
616 className="btn btn-secondary me-2"
617 disabled={this.props.loading}
619 {this.props.loading ? (
622 capitalizeFirstLetter(i18n.t("save"))
624 capitalizeFirstLetter(i18n.t("create"))
633 federatedInstanceSelect(key: InstanceKey) {
634 const id = `create_site_${key}`;
635 const value = this.state.instance_select[key];
636 const selectedInstances = this.state.siteForm[key];
638 <div className="col-12 col-md-6">
639 <label className="col-form-label" htmlFor={id}>
642 <div className="d-flex justify-content-between align-items-center">
645 placeholder="instance.tld"
647 className="form-control"
649 onInput={linkEvent(key, this.handleInstanceTextChange)}
650 onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
654 className="btn btn-sm bg-success ms-2"
655 onClick={linkEvent(key, this.handleAddInstance)}
656 style={"width: 2rem; height: 2rem;"}
658 -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
663 classes="icon-inline text-light m-auto d-block position-static"
667 {selectedInstances && selectedInstances.length > 0 && (
668 <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
669 {selectedInstances.map(instance => (
672 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
674 <label className="d-block m-0 w-100 " htmlFor={instance}>
675 <strong>{instance}</strong>
680 style={"width: 2rem; height: 2rem;"}
681 className="btn btn-sm bg-danger"
684 this.handleRemoveInstance
689 classes="icon-inline text-light m-auto d-block position-static"
700 handleInstanceTextChange(type: InstanceKey, event: any) {
701 this.setState(s => ({
704 ...s.instance_select,
705 [type]: event.target.value,
710 handleInstanceEnterPress(
712 event: InfernoKeyboardEvent<HTMLInputElement>
714 if (event.code.toLowerCase() === "enter") {
715 event.preventDefault();
717 this.handleAddInstance(key);
721 handleSaveSiteSubmit(i: SiteForm, event: any) {
722 event.preventDefault();
723 const auth = myAuthRequired();
724 i.setState(s => ((s.siteForm.auth = auth), s));
725 i.setState({ submitted: true });
727 const stateSiteForm = i.state.siteForm;
729 let form: EditSite | CreateSite;
731 if (i.props.siteRes.site_view.local_site.site_setup) {
732 form = stateSiteForm;
735 name: stateSiteForm.name ?? "My site",
736 sidebar: stateSiteForm.sidebar,
737 description: stateSiteForm.description,
738 icon: stateSiteForm.icon,
739 banner: stateSiteForm.banner,
740 community_creation_admin_only:
741 stateSiteForm.community_creation_admin_only,
742 enable_nsfw: stateSiteForm.enable_nsfw,
743 enable_downvotes: stateSiteForm.enable_downvotes,
744 application_question: stateSiteForm.application_question,
745 registration_mode: stateSiteForm.registration_mode,
746 require_email_verification: stateSiteForm.require_email_verification,
747 private_instance: stateSiteForm.private_instance,
748 default_theme: stateSiteForm.default_theme,
749 default_post_listing_type: stateSiteForm.default_post_listing_type,
750 application_email_admins: stateSiteForm.application_email_admins,
751 hide_modlog_mod_names: stateSiteForm.hide_modlog_mod_names,
752 legal_information: stateSiteForm.legal_information,
753 slur_filter_regex: stateSiteForm.slur_filter_regex,
754 actor_name_max_length: stateSiteForm.actor_name_max_length,
755 rate_limit_message: stateSiteForm.rate_limit_message,
756 rate_limit_message_per_second:
757 stateSiteForm.rate_limit_message_per_second,
758 rate_limit_comment: stateSiteForm.rate_limit_comment,
759 rate_limit_comment_per_second:
760 stateSiteForm.rate_limit_comment_per_second,
761 rate_limit_image: stateSiteForm.rate_limit_image,
762 rate_limit_image_per_second: stateSiteForm.rate_limit_image_per_second,
763 rate_limit_post: stateSiteForm.rate_limit_post,
764 rate_limit_post_per_second: stateSiteForm.rate_limit_post_per_second,
765 rate_limit_register: stateSiteForm.rate_limit_register,
766 rate_limit_register_per_second:
767 stateSiteForm.rate_limit_register_per_second,
768 rate_limit_search: stateSiteForm.rate_limit_search,
769 rate_limit_search_per_second:
770 stateSiteForm.rate_limit_search_per_second,
771 federation_enabled: stateSiteForm.federation_enabled,
772 federation_debug: stateSiteForm.federation_debug,
773 federation_worker_count: stateSiteForm.federation_worker_count,
774 captcha_enabled: stateSiteForm.captcha_enabled,
775 captcha_difficulty: stateSiteForm.captcha_difficulty,
776 allowed_instances: stateSiteForm.allowed_instances,
777 blocked_instances: stateSiteForm.blocked_instances,
778 discussion_languages: stateSiteForm.discussion_languages,
783 i.props.onSaveSite(form);
786 handleAddInstance(key: InstanceKey) {
787 const instance = this.state.instance_select[key].trim();
789 if (!validInstanceTLD(instance)) {
793 if (!this.state.siteForm[key]?.includes(instance)) {
794 this.setState(s => ({
798 [key]: [...(s.siteForm[key] ?? []), instance],
801 ...s.instance_select,
806 const oppositeKey: InstanceKey =
807 key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
808 if (this.state.siteForm[oppositeKey]?.includes(instance)) {
809 this.handleRemoveInstance({ key: oppositeKey, instance });
814 handleRemoveInstance({
821 this.setState(s => ({
825 [key]: s.siteForm[key]?.filter(i => i !== instance),
830 handleSiteNameChange(i: SiteForm, event: any) {
831 i.state.siteForm.name = event.target.value;
835 handleSiteSidebarChange(val: string) {
836 this.setState(s => ((s.siteForm.sidebar = val), s));
839 handleSiteLegalInfoChange(val: string) {
840 this.setState(s => ((s.siteForm.legal_information = val), s));
843 handleTaglineChange(i: SiteForm, index: number, val: string) {
844 const taglines = i.state.siteForm.taglines;
846 taglines[index] = val;
851 handleDeleteTaglineClick(
854 event: InfernoMouseEvent<HTMLButtonElement>
856 event.preventDefault();
857 const taglines = i.state.siteForm.taglines;
859 taglines.splice(index, 1);
860 i.state.siteForm.taglines = undefined;
862 i.state.siteForm.taglines = taglines;
867 handleAddTaglineClick(
869 event: InfernoMouseEvent<HTMLButtonElement>
871 event.preventDefault();
872 if (!i.state.siteForm.taglines) {
873 i.state.siteForm.taglines = [];
875 i.state.siteForm.taglines.push("");
879 handleSiteApplicationQuestionChange(val: string) {
880 this.setState(s => ((s.siteForm.application_question = val), s));
883 handleSiteDescChange(i: SiteForm, event: any) {
884 i.state.siteForm.description = event.target.value;
888 handleSiteEnableNsfwChange(i: SiteForm, event: any) {
889 i.state.siteForm.enable_nsfw = event.target.checked;
893 handleSiteRegistrationModeChange(i: SiteForm, event: any) {
894 i.state.siteForm.registration_mode = event.target.value;
898 handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
899 i.state.siteForm.community_creation_admin_only = event.target.checked;
903 handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
904 i.state.siteForm.enable_downvotes = event.target.checked;
908 handleSiteRequireEmailVerification(i: SiteForm, event: any) {
909 i.state.siteForm.require_email_verification = event.target.checked;
913 handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
914 i.state.siteForm.application_email_admins = event.target.checked;
918 handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
919 i.state.siteForm.reports_email_admins = event.target.checked;
923 handleSitePrivateInstance(i: SiteForm, event: any) {
924 i.state.siteForm.private_instance = event.target.checked;
928 handleSiteHideModlogModNames(i: SiteForm, event: any) {
929 i.state.siteForm.hide_modlog_mod_names = event.target.checked;
933 handleSiteDefaultTheme(i: SiteForm, event: any) {
934 i.state.siteForm.default_theme = event.target.value;
938 handleIconUpload(url: string) {
939 this.setState(s => ((s.siteForm.icon = url), s));
943 this.setState(s => ((s.siteForm.icon = ""), s));
946 handleBannerUpload(url: string) {
947 this.setState(s => ((s.siteForm.banner = url), s));
950 handleBannerRemove() {
951 this.setState(s => ((s.siteForm.banner = ""), s));
954 handleSiteSlurFilterRegex(i: SiteForm, event: any) {
955 i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
958 handleSiteActorNameMaxLength(i: SiteForm, event: any) {
960 s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
964 handleSiteFederationEnabled(i: SiteForm, event: any) {
965 i.state.siteForm.federation_enabled = event.target.checked;
969 handleSiteFederationDebug(i: SiteForm, event: any) {
970 i.state.siteForm.federation_debug = event.target.checked;
974 handleSiteFederationWorkerCount(i: SiteForm, event: any) {
977 (s.siteForm.federation_worker_count = Number(event.target.value)), s
982 handleSiteCaptchaEnabled(i: SiteForm, event: any) {
983 i.state.siteForm.captcha_enabled = event.target.checked;
987 handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
988 i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
991 handleDiscussionLanguageChange(val: number[]) {
992 this.setState(s => ((s.siteForm.discussion_languages = val), s));
995 handleDefaultPostListingTypeChange(val: ListingType) {
996 this.setState(s => ((s.siteForm.default_post_listing_type = val), s));