1 import { myAuthRequired } from "@utils/app";
2 import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers";
15 } from "lemmy-js-client";
16 import { i18n } from "../../i18next";
17 import { Icon, Spinner } from "../common/icon";
18 import { ImageUploadForm } from "../common/image-upload-form";
19 import { LanguageSelect } from "../common/language-select";
20 import { ListingTypeSelect } from "../common/listing-type-select";
21 import { MarkdownTextArea } from "../common/markdown-textarea";
22 import NavigationPrompt from "../common/navigation-prompt";
24 interface SiteFormProps {
25 blockedInstances?: Instance[];
26 allowedInstances?: Instance[];
29 onSaveSite(form: EditSite): void;
30 siteRes: GetSiteResponse;
34 interface SiteFormState {
37 allowed_instances: string;
38 blocked_instances: string;
43 type InstanceKey = "allowed_instances" | "blocked_instances";
45 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
46 state: SiteFormState = {
47 siteForm: this.initSiteForm(),
49 allowed_instances: "",
50 blocked_instances: "",
55 initSiteForm(): EditSite {
56 const site = this.props.siteRes.site_view.site;
57 const ls = this.props.siteRes.site_view.local_site;
60 sidebar: site.sidebar,
61 description: site.description,
62 enable_downvotes: ls.enable_downvotes,
63 registration_mode: ls.registration_mode,
64 enable_nsfw: ls.enable_nsfw,
65 community_creation_admin_only: ls.community_creation_admin_only,
68 require_email_verification: ls.require_email_verification,
69 application_question: ls.application_question,
70 private_instance: ls.private_instance,
71 default_theme: ls.default_theme,
72 default_post_listing_type: ls.default_post_listing_type,
73 legal_information: ls.legal_information,
74 application_email_admins: ls.application_email_admins,
75 reports_email_admins: ls.reports_email_admins,
76 hide_modlog_mod_names: ls.hide_modlog_mod_names,
77 discussion_languages: this.props.siteRes.discussion_languages,
78 slur_filter_regex: ls.slur_filter_regex,
79 actor_name_max_length: ls.actor_name_max_length,
80 federation_enabled: ls.federation_enabled,
81 federation_worker_count: ls.federation_worker_count,
82 captcha_enabled: ls.captcha_enabled,
83 captcha_difficulty: ls.captcha_difficulty,
84 allowed_instances: this.props.allowedInstances?.map(i => i.domain),
85 blocked_instances: this.props.blockedInstances?.map(i => i.domain),
90 constructor(props: any, context: any) {
91 super(props, context);
93 this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
94 this.handleSiteLegalInfoChange = this.handleSiteLegalInfoChange.bind(this);
95 this.handleSiteApplicationQuestionChange =
96 this.handleSiteApplicationQuestionChange.bind(this);
98 this.handleIconUpload = this.handleIconUpload.bind(this);
99 this.handleIconRemove = this.handleIconRemove.bind(this);
101 this.handleBannerUpload = this.handleBannerUpload.bind(this);
102 this.handleBannerRemove = this.handleBannerRemove.bind(this);
104 this.handleDefaultPostListingTypeChange =
105 this.handleDefaultPostListingTypeChange.bind(this);
107 this.handleDiscussionLanguageChange =
108 this.handleDiscussionLanguageChange.bind(this);
110 this.handleAddInstance = this.handleAddInstance.bind(this);
111 this.handleRemoveInstance = this.handleRemoveInstance.bind(this);
113 this.handleInstanceEnterPress = this.handleInstanceEnterPress.bind(this);
114 this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
118 const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
121 className="site-form"
122 onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}
126 !this.props.loading &&
129 this.state.siteForm.name ||
130 this.state.siteForm.sidebar ||
131 this.state.siteForm.application_question ||
132 this.state.siteForm.description
134 !this.state.submitted
139 ? capitalizeFirstLetter(i18n.t("edit"))
140 : capitalizeFirstLetter(i18n.t("setup"))
141 } ${i18n.t("your_site")}`}</h5>
142 <div className="mb-3 row">
143 <label className="col-12 col-form-label" htmlFor="create-site-name">
146 <div className="col-12">
149 id="create-site-name"
150 className="form-control"
151 value={this.state.siteForm.name}
152 onInput={linkEvent(this, this.handleSiteNameChange)}
159 <div className="input-group mb-3">
160 <label className="me-2 col-form-label">{i18n.t("icon")}</label>
162 uploadTitle={i18n.t("upload_icon")}
163 imageSrc={this.state.siteForm.icon}
164 onUpload={this.handleIconUpload}
165 onRemove={this.handleIconRemove}
169 <div className="input-group mb-3">
170 <label className="me-2 col-form-label">{i18n.t("banner")}</label>
172 uploadTitle={i18n.t("upload_banner")}
173 imageSrc={this.state.siteForm.banner}
174 onUpload={this.handleBannerUpload}
175 onRemove={this.handleBannerRemove}
178 <div className="mb-3 row">
179 <label className="col-12 col-form-label" htmlFor="site-desc">
180 {i18n.t("description")}
182 <div className="col-12">
185 className="form-control"
187 value={this.state.siteForm.description}
188 onInput={linkEvent(this, this.handleSiteDescChange)}
193 <div className="mb-3 row">
194 <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
195 <div className="col-12">
197 initialContent={this.state.siteForm.sidebar}
198 onContentChange={this.handleSiteSidebarChange}
199 hideNavigationWarnings
205 <div className="mb-3 row">
206 <label className="col-12 col-form-label">
207 {i18n.t("legal_information")}
209 <div className="col-12">
211 initialContent={this.state.siteForm.legal_information}
212 onContentChange={this.handleSiteLegalInfoChange}
213 hideNavigationWarnings
219 <div className="mb-3 row">
220 <div className="col-12">
221 <div className="form-check">
223 className="form-check-input"
224 id="create-site-downvotes"
226 checked={this.state.siteForm.enable_downvotes}
227 onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
230 className="form-check-label"
231 htmlFor="create-site-downvotes"
233 {i18n.t("enable_downvotes")}
238 <div className="mb-3 row">
239 <div className="col-12">
240 <div className="form-check">
242 className="form-check-input"
243 id="create-site-enable-nsfw"
245 checked={this.state.siteForm.enable_nsfw}
246 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
249 className="form-check-label"
250 htmlFor="create-site-enable-nsfw"
252 {i18n.t("enable_nsfw")}
257 <div className="mb-3 row">
258 <div className="col-12">
260 className="form-check-label me-2"
261 htmlFor="create-site-registration-mode"
263 {i18n.t("registration_mode")}
266 id="create-site-registration-mode"
267 value={this.state.siteForm.registration_mode}
268 onChange={linkEvent(this, this.handleSiteRegistrationModeChange)}
269 className="form-select d-inline-block w-auto"
271 <option value={"RequireApplication"}>
272 {i18n.t("require_registration_application")}
274 <option value={"Open"}>{i18n.t("open_registration")}</option>
275 <option value={"Closed"}>{i18n.t("close_registration")}</option>
279 {this.state.siteForm.registration_mode == "RequireApplication" && (
280 <div className="mb-3 row">
281 <label className="col-12 col-form-label">
282 {i18n.t("application_questionnaire")}
284 <div className="col-12">
286 initialContent={this.state.siteForm.application_question}
287 onContentChange={this.handleSiteApplicationQuestionChange}
288 hideNavigationWarnings
295 <div className="mb-3 row">
296 <div className="col-12">
297 <div className="form-check">
299 className="form-check-input"
300 id="create-site-community-creation-admin-only"
302 checked={this.state.siteForm.community_creation_admin_only}
305 this.handleSiteCommunityCreationAdminOnly
309 className="form-check-label"
310 htmlFor="create-site-community-creation-admin-only"
312 {i18n.t("community_creation_admin_only")}
317 <div className="mb-3 row">
318 <div className="col-12">
319 <div className="form-check">
321 className="form-check-input"
322 id="create-site-require-email-verification"
324 checked={this.state.siteForm.require_email_verification}
327 this.handleSiteRequireEmailVerification
331 className="form-check-label"
332 htmlFor="create-site-require-email-verification"
334 {i18n.t("require_email_verification")}
339 <div className="mb-3 row">
340 <div className="col-12">
341 <div className="form-check">
343 className="form-check-input"
344 id="create-site-application-email-admins"
346 checked={this.state.siteForm.application_email_admins}
349 this.handleSiteApplicationEmailAdmins
353 className="form-check-label"
354 htmlFor="create-site-email-admins"
356 {i18n.t("application_email_admins")}
361 <div className="mb-3 row">
362 <div className="col-12">
363 <div className="form-check">
365 className="form-check-input"
366 id="create-site-reports-email-admins"
368 checked={this.state.siteForm.reports_email_admins}
369 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
372 className="form-check-label"
373 htmlFor="create-site-reports-email-admins"
375 {i18n.t("reports_email_admins")}
380 <div className="mb-3 row">
381 <div className="col-12">
383 className="form-check-label me-2"
384 htmlFor="create-site-default-theme"
389 id="create-site-default-theme"
390 value={this.state.siteForm.default_theme}
391 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
392 className="form-select d-inline-block w-auto"
394 <option value="browser">{i18n.t("browser_default")}</option>
395 {this.props.themeList?.map(theme => (
396 <option key={theme} value={theme}>
403 {this.props.showLocal && (
404 <form className="mb-3 row">
405 <label className="col-sm-3 col-form-label">
406 {i18n.t("listing_type")}
408 <div className="col-sm-9">
410 type_={this.state.siteForm.default_post_listing_type ?? "Local"}
412 showSubscribed={false}
413 onChange={this.handleDefaultPostListingTypeChange}
418 <div className="mb-3 row">
419 <div className="col-12">
420 <div className="form-check">
422 className="form-check-input"
423 id="create-site-private-instance"
425 checked={this.state.siteForm.private_instance}
426 onChange={linkEvent(this, this.handleSitePrivateInstance)}
429 className="form-check-label"
430 htmlFor="create-site-private-instance"
432 {i18n.t("private_instance")}
437 <div className="mb-3 row">
438 <div className="col-12">
439 <div className="form-check">
441 className="form-check-input"
442 id="create-site-hide-modlog-mod-names"
444 checked={this.state.siteForm.hide_modlog_mod_names}
445 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
448 className="form-check-label"
449 htmlFor="create-site-hide-modlog-mod-names"
451 {i18n.t("hide_modlog_mod_names")}
456 <div className="mb-3 row">
458 className="col-12 col-form-label"
459 htmlFor="create-site-slur-filter-regex"
461 {i18n.t("slur_filter_regex")}
463 <div className="col-12">
466 id="create-site-slur-filter-regex"
467 placeholder="(word1|word2)"
468 className="form-control"
469 value={this.state.siteForm.slur_filter_regex}
470 onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
476 allLanguages={this.props.siteRes.all_languages}
477 siteLanguages={this.props.siteRes.discussion_languages}
478 selectedLanguageIds={this.state.siteForm.discussion_languages}
480 onChange={this.handleDiscussionLanguageChange}
483 <div className="mb-3 row">
485 className="col-12 col-form-label"
486 htmlFor="create-site-actor-name"
488 {i18n.t("actor_name_max_length")}
490 <div className="col-12">
493 id="create-site-actor-name"
494 className="form-control"
496 value={this.state.siteForm.actor_name_max_length}
497 onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
501 <div className="mb-3 row">
502 <div className="col-12">
503 <div className="form-check">
505 className="form-check-input"
506 id="create-site-federation-enabled"
508 checked={this.state.siteForm.federation_enabled}
509 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
512 className="form-check-label"
513 htmlFor="create-site-federation-enabled"
515 {i18n.t("federation_enabled")}
520 {this.state.siteForm.federation_enabled && (
522 <div className="mb-3 row">
523 {this.federatedInstanceSelect("allowed_instances")}
524 {this.federatedInstanceSelect("blocked_instances")}
526 <div className="mb-3 row">
527 <div className="col-12">
528 <div className="form-check">
530 className="form-check-input"
531 id="create-site-federation-debug"
533 checked={this.state.siteForm.federation_debug}
534 onChange={linkEvent(this, this.handleSiteFederationDebug)}
537 className="form-check-label"
538 htmlFor="create-site-federation-debug"
540 {i18n.t("federation_debug")}
545 <div className="mb-3 row">
547 className="col-12 col-form-label"
548 htmlFor="create-site-federation-worker-count"
550 {i18n.t("federation_worker_count")}
552 <div className="col-12">
555 id="create-site-federation-worker-count"
556 className="form-control"
558 value={this.state.siteForm.federation_worker_count}
561 this.handleSiteFederationWorkerCount
568 <div className="mb-3 row">
569 <div className="col-12">
570 <div className="form-check">
572 className="form-check-input"
573 id="create-site-captcha-enabled"
575 checked={this.state.siteForm.captcha_enabled}
576 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
579 className="form-check-label"
580 htmlFor="create-site-captcha-enabled"
582 {i18n.t("captcha_enabled")}
587 {this.state.siteForm.captcha_enabled && (
588 <div className="mb-3 row">
589 <div className="col-12">
591 className="form-check-label me-2"
592 htmlFor="create-site-captcha-difficulty"
594 {i18n.t("captcha_difficulty")}
597 id="create-site-captcha-difficulty"
598 value={this.state.siteForm.captcha_difficulty}
599 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
600 className="form-select d-inline-block w-auto"
602 <option value="easy">{i18n.t("easy")}</option>
603 <option value="medium">{i18n.t("medium")}</option>
604 <option value="hard">{i18n.t("hard")}</option>
609 <div className="mb-3 row">
610 <div className="col-12">
613 className="btn btn-secondary me-2"
614 disabled={this.props.loading}
616 {this.props.loading ? (
619 capitalizeFirstLetter(i18n.t("save"))
621 capitalizeFirstLetter(i18n.t("create"))
630 federatedInstanceSelect(key: InstanceKey) {
631 const id = `create_site_${key}`;
632 const value = this.state.instance_select[key];
633 const selectedInstances = this.state.siteForm[key];
635 <div className="col-12 col-md-6">
636 <label className="col-form-label" htmlFor={id}>
639 <div className="d-flex justify-content-between align-items-center">
642 placeholder="instance.tld"
644 className="form-control"
646 onInput={linkEvent(key, this.handleInstanceTextChange)}
647 onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
651 className="btn btn-sm bg-success ms-2"
652 onClick={linkEvent(key, this.handleAddInstance)}
653 style={"width: 2rem; height: 2rem;"}
655 -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
660 classes="icon-inline text-light m-auto d-block position-static"
664 {selectedInstances && selectedInstances.length > 0 && (
665 <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
666 {selectedInstances.map(instance => (
669 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
671 <label className="d-block m-0 w-100 " htmlFor={instance}>
672 <strong>{instance}</strong>
677 style={"width: 2rem; height: 2rem;"}
678 className="btn btn-sm bg-danger"
681 this.handleRemoveInstance
686 classes="icon-inline text-light m-auto d-block position-static"
697 handleInstanceTextChange(type: InstanceKey, event: any) {
698 this.setState(s => ({
701 ...s.instance_select,
702 [type]: event.target.value,
707 handleInstanceEnterPress(
709 event: InfernoKeyboardEvent<HTMLInputElement>
711 if (event.code.toLowerCase() === "enter") {
712 event.preventDefault();
714 this.handleAddInstance(key);
718 handleSaveSiteSubmit(i: SiteForm, event: any) {
719 event.preventDefault();
720 const auth = myAuthRequired();
721 i.setState(s => ((s.siteForm.auth = auth), s));
722 i.setState({ submitted: true });
724 const stateSiteForm = i.state.siteForm;
726 let form: EditSite | CreateSite;
728 if (i.props.siteRes.site_view.local_site.site_setup) {
729 form = stateSiteForm;
732 name: stateSiteForm.name ?? "My site",
733 sidebar: stateSiteForm.sidebar,
734 description: stateSiteForm.description,
735 icon: stateSiteForm.icon,
736 banner: stateSiteForm.banner,
737 community_creation_admin_only:
738 stateSiteForm.community_creation_admin_only,
739 enable_nsfw: stateSiteForm.enable_nsfw,
740 enable_downvotes: stateSiteForm.enable_downvotes,
741 application_question: stateSiteForm.application_question,
742 registration_mode: stateSiteForm.registration_mode,
743 require_email_verification: stateSiteForm.require_email_verification,
744 private_instance: stateSiteForm.private_instance,
745 default_theme: stateSiteForm.default_theme,
746 default_post_listing_type: stateSiteForm.default_post_listing_type,
747 application_email_admins: stateSiteForm.application_email_admins,
748 hide_modlog_mod_names: stateSiteForm.hide_modlog_mod_names,
749 legal_information: stateSiteForm.legal_information,
750 slur_filter_regex: stateSiteForm.slur_filter_regex,
751 actor_name_max_length: stateSiteForm.actor_name_max_length,
752 rate_limit_message: stateSiteForm.rate_limit_message,
753 rate_limit_message_per_second:
754 stateSiteForm.rate_limit_message_per_second,
755 rate_limit_comment: stateSiteForm.rate_limit_comment,
756 rate_limit_comment_per_second:
757 stateSiteForm.rate_limit_comment_per_second,
758 rate_limit_image: stateSiteForm.rate_limit_image,
759 rate_limit_image_per_second: stateSiteForm.rate_limit_image_per_second,
760 rate_limit_post: stateSiteForm.rate_limit_post,
761 rate_limit_post_per_second: stateSiteForm.rate_limit_post_per_second,
762 rate_limit_register: stateSiteForm.rate_limit_register,
763 rate_limit_register_per_second:
764 stateSiteForm.rate_limit_register_per_second,
765 rate_limit_search: stateSiteForm.rate_limit_search,
766 rate_limit_search_per_second:
767 stateSiteForm.rate_limit_search_per_second,
768 federation_enabled: stateSiteForm.federation_enabled,
769 federation_debug: stateSiteForm.federation_debug,
770 federation_worker_count: stateSiteForm.federation_worker_count,
771 captcha_enabled: stateSiteForm.captcha_enabled,
772 captcha_difficulty: stateSiteForm.captcha_difficulty,
773 allowed_instances: stateSiteForm.allowed_instances,
774 blocked_instances: stateSiteForm.blocked_instances,
775 discussion_languages: stateSiteForm.discussion_languages,
780 i.props.onSaveSite(form);
783 handleAddInstance(key: InstanceKey) {
784 const instance = this.state.instance_select[key].trim();
786 if (!validInstanceTLD(instance)) {
790 if (!this.state.siteForm[key]?.includes(instance)) {
791 this.setState(s => ({
795 [key]: [...(s.siteForm[key] ?? []), instance],
798 ...s.instance_select,
803 const oppositeKey: InstanceKey =
804 key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
805 if (this.state.siteForm[oppositeKey]?.includes(instance)) {
806 this.handleRemoveInstance({ key: oppositeKey, instance });
811 handleRemoveInstance({
818 this.setState(s => ({
822 [key]: s.siteForm[key]?.filter(i => i !== instance),
827 handleSiteNameChange(i: SiteForm, event: any) {
828 i.state.siteForm.name = event.target.value;
832 handleSiteSidebarChange(val: string) {
833 this.setState(s => ((s.siteForm.sidebar = val), s));
836 handleSiteLegalInfoChange(val: string) {
837 this.setState(s => ((s.siteForm.legal_information = val), s));
840 handleTaglineChange(i: SiteForm, index: number, val: string) {
841 const taglines = i.state.siteForm.taglines;
843 taglines[index] = val;
848 handleDeleteTaglineClick(
851 event: InfernoMouseEvent<HTMLButtonElement>
853 event.preventDefault();
854 const taglines = i.state.siteForm.taglines;
856 taglines.splice(index, 1);
857 i.state.siteForm.taglines = undefined;
859 i.state.siteForm.taglines = taglines;
864 handleAddTaglineClick(
866 event: InfernoMouseEvent<HTMLButtonElement>
868 event.preventDefault();
869 if (!i.state.siteForm.taglines) {
870 i.state.siteForm.taglines = [];
872 i.state.siteForm.taglines.push("");
876 handleSiteApplicationQuestionChange(val: string) {
877 this.setState(s => ((s.siteForm.application_question = val), s));
880 handleSiteDescChange(i: SiteForm, event: any) {
881 i.state.siteForm.description = event.target.value;
885 handleSiteEnableNsfwChange(i: SiteForm, event: any) {
886 i.state.siteForm.enable_nsfw = event.target.checked;
890 handleSiteRegistrationModeChange(i: SiteForm, event: any) {
891 i.state.siteForm.registration_mode = event.target.value;
895 handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
896 i.state.siteForm.community_creation_admin_only = event.target.checked;
900 handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
901 i.state.siteForm.enable_downvotes = event.target.checked;
905 handleSiteRequireEmailVerification(i: SiteForm, event: any) {
906 i.state.siteForm.require_email_verification = event.target.checked;
910 handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
911 i.state.siteForm.application_email_admins = event.target.checked;
915 handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
916 i.state.siteForm.reports_email_admins = event.target.checked;
920 handleSitePrivateInstance(i: SiteForm, event: any) {
921 i.state.siteForm.private_instance = event.target.checked;
925 handleSiteHideModlogModNames(i: SiteForm, event: any) {
926 i.state.siteForm.hide_modlog_mod_names = event.target.checked;
930 handleSiteDefaultTheme(i: SiteForm, event: any) {
931 i.state.siteForm.default_theme = event.target.value;
935 handleIconUpload(url: string) {
936 this.setState(s => ((s.siteForm.icon = url), s));
940 this.setState(s => ((s.siteForm.icon = ""), s));
943 handleBannerUpload(url: string) {
944 this.setState(s => ((s.siteForm.banner = url), s));
947 handleBannerRemove() {
948 this.setState(s => ((s.siteForm.banner = ""), s));
951 handleSiteSlurFilterRegex(i: SiteForm, event: any) {
952 i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
955 handleSiteActorNameMaxLength(i: SiteForm, event: any) {
957 s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
961 handleSiteFederationEnabled(i: SiteForm, event: any) {
962 i.state.siteForm.federation_enabled = event.target.checked;
966 handleSiteFederationDebug(i: SiteForm, event: any) {
967 i.state.siteForm.federation_debug = event.target.checked;
971 handleSiteFederationWorkerCount(i: SiteForm, event: any) {
974 (s.siteForm.federation_worker_count = Number(event.target.value)), s
979 handleSiteCaptchaEnabled(i: SiteForm, event: any) {
980 i.state.siteForm.captcha_enabled = event.target.checked;
984 handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
985 i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
988 handleDiscussionLanguageChange(val: number[]) {
989 this.setState(s => ((s.siteForm.discussion_languages = val), s));
992 handleDefaultPostListingTypeChange(val: ListingType) {
993 this.setState(s => ((s.siteForm.default_post_listing_type = val), s));