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_debug: ls.federation_debug,
85 federation_worker_count: ls.federation_worker_count,
86 captcha_enabled: ls.captcha_enabled,
87 captcha_difficulty: ls.captcha_difficulty,
88 allowed_instances: this.props.allowedInstances?.map(i => i.domain),
89 blocked_instances: this.props.blockedInstances?.map(i => i.domain),
94 constructor(props: any, context: any) {
95 super(props, context);
97 this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
98 this.handleSiteLegalInfoChange = this.handleSiteLegalInfoChange.bind(this);
99 this.handleSiteApplicationQuestionChange =
100 this.handleSiteApplicationQuestionChange.bind(this);
102 this.handleIconUpload = this.handleIconUpload.bind(this);
103 this.handleIconRemove = this.handleIconRemove.bind(this);
105 this.handleBannerUpload = this.handleBannerUpload.bind(this);
106 this.handleBannerRemove = this.handleBannerRemove.bind(this);
108 this.handleDefaultPostListingTypeChange =
109 this.handleDefaultPostListingTypeChange.bind(this);
111 this.handleDiscussionLanguageChange =
112 this.handleDiscussionLanguageChange.bind(this);
114 this.handleAddInstance = this.handleAddInstance.bind(this);
115 this.handleRemoveInstance = this.handleRemoveInstance.bind(this);
117 this.handleInstanceEnterPress = this.handleInstanceEnterPress.bind(this);
118 this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
122 const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
124 <form onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}>
127 !this.props.loading &&
130 this.state.siteForm.name ||
131 this.state.siteForm.sidebar ||
132 this.state.siteForm.application_question ||
133 this.state.siteForm.description
135 !this.state.submitted
140 ? capitalizeFirstLetter(i18n.t("edit"))
141 : capitalizeFirstLetter(i18n.t("setup"))
142 } ${i18n.t("your_site")}`}</h5>
143 <div className="form-group row">
144 <label className="col-12 col-form-label" htmlFor="create-site-name">
147 <div className="col-12">
150 id="create-site-name"
151 className="form-control"
152 value={this.state.siteForm.name}
153 onInput={linkEvent(this, this.handleSiteNameChange)}
160 <div className="form-group">
161 <label className="mr-2">{i18n.t("icon")}</label>
163 uploadTitle={i18n.t("upload_icon")}
164 imageSrc={this.state.siteForm.icon}
165 onUpload={this.handleIconUpload}
166 onRemove={this.handleIconRemove}
170 <div className="form-group">
171 <label className="mr-2">{i18n.t("banner")}</label>
173 uploadTitle={i18n.t("upload_banner")}
174 imageSrc={this.state.siteForm.banner}
175 onUpload={this.handleBannerUpload}
176 onRemove={this.handleBannerRemove}
179 <div className="form-group row">
180 <label className="col-12 col-form-label" htmlFor="site-desc">
181 {i18n.t("description")}
183 <div className="col-12">
186 className="form-control"
188 value={this.state.siteForm.description}
189 onInput={linkEvent(this, this.handleSiteDescChange)}
194 <div className="form-group row">
195 <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
196 <div className="col-12">
198 initialContent={this.state.siteForm.sidebar}
199 onContentChange={this.handleSiteSidebarChange}
200 hideNavigationWarnings
206 <div className="form-group row">
207 <label className="col-12 col-form-label">
208 {i18n.t("legal_information")}
210 <div className="col-12">
212 initialContent={this.state.siteForm.legal_information}
213 onContentChange={this.handleSiteLegalInfoChange}
214 hideNavigationWarnings
220 <div className="form-group row">
221 <div className="col-12">
222 <div className="form-check">
224 className="form-check-input"
225 id="create-site-downvotes"
227 checked={this.state.siteForm.enable_downvotes}
228 onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
231 className="form-check-label"
232 htmlFor="create-site-downvotes"
234 {i18n.t("enable_downvotes")}
239 <div className="form-group row">
240 <div className="col-12">
241 <div className="form-check">
243 className="form-check-input"
244 id="create-site-enable-nsfw"
246 checked={this.state.siteForm.enable_nsfw}
247 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
250 className="form-check-label"
251 htmlFor="create-site-enable-nsfw"
253 {i18n.t("enable_nsfw")}
258 <div className="form-group row">
259 <div className="col-12">
261 className="form-check-label mr-2"
262 htmlFor="create-site-registration-mode"
264 {i18n.t("registration_mode")}
267 id="create-site-registration-mode"
268 value={this.state.siteForm.registration_mode}
269 onChange={linkEvent(this, this.handleSiteRegistrationModeChange)}
270 className="custom-select w-auto"
272 <option value={"RequireApplication"}>
273 {i18n.t("require_registration_application")}
275 <option value={"Open"}>{i18n.t("open_registration")}</option>
276 <option value={"Closed"}>{i18n.t("close_registration")}</option>
280 {this.state.siteForm.registration_mode == "RequireApplication" && (
281 <div className="form-group row">
282 <label className="col-12 col-form-label">
283 {i18n.t("application_questionnaire")}
285 <div className="col-12">
287 initialContent={this.state.siteForm.application_question}
288 onContentChange={this.handleSiteApplicationQuestionChange}
289 hideNavigationWarnings
296 <div className="form-group row">
297 <div className="col-12">
298 <div className="form-check">
300 className="form-check-input"
301 id="create-site-community-creation-admin-only"
303 checked={this.state.siteForm.community_creation_admin_only}
306 this.handleSiteCommunityCreationAdminOnly
310 className="form-check-label"
311 htmlFor="create-site-community-creation-admin-only"
313 {i18n.t("community_creation_admin_only")}
318 <div className="form-group row">
319 <div className="col-12">
320 <div className="form-check">
322 className="form-check-input"
323 id="create-site-require-email-verification"
325 checked={this.state.siteForm.require_email_verification}
328 this.handleSiteRequireEmailVerification
332 className="form-check-label"
333 htmlFor="create-site-require-email-verification"
335 {i18n.t("require_email_verification")}
340 <div className="form-group row">
341 <div className="col-12">
342 <div className="form-check">
344 className="form-check-input"
345 id="create-site-application-email-admins"
347 checked={this.state.siteForm.application_email_admins}
350 this.handleSiteApplicationEmailAdmins
354 className="form-check-label"
355 htmlFor="create-site-email-admins"
357 {i18n.t("application_email_admins")}
362 <div className="form-group row">
363 <div className="col-12">
364 <div className="form-check">
366 className="form-check-input"
367 id="create-site-reports-email-admins"
369 checked={this.state.siteForm.reports_email_admins}
370 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
373 className="form-check-label"
374 htmlFor="create-site-reports-email-admins"
376 {i18n.t("reports_email_admins")}
381 <div className="form-group row">
382 <div className="col-12">
384 className="form-check-label mr-2"
385 htmlFor="create-site-default-theme"
390 id="create-site-default-theme"
391 value={this.state.siteForm.default_theme}
392 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
393 className="custom-select w-auto"
395 <option value="browser">{i18n.t("browser_default")}</option>
396 {this.props.themeList?.map(theme => (
397 <option key={theme} value={theme}>
404 {this.props.showLocal && (
405 <form className="form-group row">
406 <label className="col-sm-3">{i18n.t("listing_type")}</label>
407 <div className="col-sm-9">
409 type_={this.state.siteForm.default_post_listing_type ?? "Local"}
411 showSubscribed={false}
412 onChange={this.handleDefaultPostListingTypeChange}
417 <div className="form-group row">
418 <div className="col-12">
419 <div className="form-check">
421 className="form-check-input"
422 id="create-site-private-instance"
424 checked={this.state.siteForm.private_instance}
425 onChange={linkEvent(this, this.handleSitePrivateInstance)}
428 className="form-check-label"
429 htmlFor="create-site-private-instance"
431 {i18n.t("private_instance")}
436 <div className="form-group row">
437 <div className="col-12">
438 <div className="form-check">
440 className="form-check-input"
441 id="create-site-hide-modlog-mod-names"
443 checked={this.state.siteForm.hide_modlog_mod_names}
444 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
447 className="form-check-label"
448 htmlFor="create-site-hide-modlog-mod-names"
450 {i18n.t("hide_modlog_mod_names")}
455 <div className="form-group row">
457 className="col-12 col-form-label"
458 htmlFor="create-site-slur-filter-regex"
460 {i18n.t("slur_filter_regex")}
462 <div className="col-12">
465 id="create-site-slur-filter-regex"
466 placeholder="(word1|word2)"
467 className="form-control"
468 value={this.state.siteForm.slur_filter_regex}
469 onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
475 allLanguages={this.props.siteRes.all_languages}
476 siteLanguages={this.props.siteRes.discussion_languages}
477 selectedLanguageIds={this.state.siteForm.discussion_languages}
479 onChange={this.handleDiscussionLanguageChange}
482 <div className="form-group row">
484 className="col-12 col-form-label"
485 htmlFor="create-site-actor-name"
487 {i18n.t("actor_name_max_length")}
489 <div className="col-12">
492 id="create-site-actor-name"
493 className="form-control"
495 value={this.state.siteForm.actor_name_max_length}
496 onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
500 <div className="form-group row">
501 <div className="col-12">
502 <div className="form-check">
504 className="form-check-input"
505 id="create-site-federation-enabled"
507 checked={this.state.siteForm.federation_enabled}
508 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
511 className="form-check-label"
512 htmlFor="create-site-federation-enabled"
514 {i18n.t("federation_enabled")}
519 {this.state.siteForm.federation_enabled && (
521 <div className="form-group row">
522 {this.federatedInstanceSelect("allowed_instances")}
523 {this.federatedInstanceSelect("blocked_instances")}
525 <div className="form-group row">
526 <div className="col-12">
527 <div className="form-check">
529 className="form-check-input"
530 id="create-site-federation-debug"
532 checked={this.state.siteForm.federation_debug}
533 onChange={linkEvent(this, this.handleSiteFederationDebug)}
536 className="form-check-label"
537 htmlFor="create-site-federation-debug"
539 {i18n.t("federation_debug")}
544 <div className="form-group row">
546 className="col-12 col-form-label"
547 htmlFor="create-site-federation-worker-count"
549 {i18n.t("federation_worker_count")}
551 <div className="col-12">
554 id="create-site-federation-worker-count"
555 className="form-control"
557 value={this.state.siteForm.federation_worker_count}
560 this.handleSiteFederationWorkerCount
567 <div className="form-group row">
568 <div className="col-12">
569 <div className="form-check">
571 className="form-check-input"
572 id="create-site-captcha-enabled"
574 checked={this.state.siteForm.captcha_enabled}
575 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
578 className="form-check-label"
579 htmlFor="create-site-captcha-enabled"
581 {i18n.t("captcha_enabled")}
586 {this.state.siteForm.captcha_enabled && (
587 <div className="form-group row">
588 <div className="col-12">
590 className="form-check-label mr-2"
591 htmlFor="create-site-captcha-difficulty"
593 {i18n.t("captcha_difficulty")}
596 id="create-site-captcha-difficulty"
597 value={this.state.siteForm.captcha_difficulty}
598 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
599 className="custom-select w-auto"
601 <option value="easy">{i18n.t("easy")}</option>
602 <option value="medium">{i18n.t("medium")}</option>
603 <option value="hard">{i18n.t("hard")}</option>
608 <div className="form-group row">
609 <div className="col-12">
612 className="btn btn-secondary mr-2"
613 disabled={this.props.loading}
615 {this.props.loading ? (
618 capitalizeFirstLetter(i18n.t("save"))
620 capitalizeFirstLetter(i18n.t("create"))
629 federatedInstanceSelect(key: InstanceKey) {
630 const id = `create_site_${key}`;
631 const value = this.state.instance_select[key];
632 const selectedInstances = this.state.siteForm[key];
634 <div className="col-12 col-md-6">
635 <label className="col-form-label" htmlFor={id}>
638 <div className="d-flex justify-content-between align-items-center">
641 placeholder="instance.tld"
643 className="form-control"
645 onInput={linkEvent(key, this.handleInstanceTextChange)}
646 onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
650 className="btn btn-sm bg-success ml-2"
651 onClick={linkEvent(key, this.handleAddInstance)}
652 style={"width: 2rem; height: 2rem;"}
654 -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
659 classes="icon-inline text-light m-auto d-block position-static"
663 {selectedInstances && selectedInstances.length > 0 && (
664 <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
665 {selectedInstances.map(instance => (
668 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
670 <label className="d-block m-0 w-100 " htmlFor={instance}>
671 <strong>{instance}</strong>
676 style={"width: 2rem; height: 2rem;"}
677 className="btn btn-sm bg-danger"
680 this.handleRemoveInstance
685 classes="icon-inline text-light m-auto d-block position-static"
696 handleInstanceTextChange(type: InstanceKey, event: any) {
697 this.setState(s => ({
700 ...s.instance_select,
701 [type]: event.target.value,
706 handleInstanceEnterPress(
708 event: InfernoKeyboardEvent<HTMLInputElement>
710 if (event.code.toLowerCase() === "enter") {
711 event.preventDefault();
713 this.handleAddInstance(key);
717 handleSaveSiteSubmit(i: SiteForm, event: any) {
718 event.preventDefault();
719 const auth = myAuthRequired();
720 i.setState(s => ((s.siteForm.auth = auth), s));
721 i.setState({ submitted: true });
723 const stateSiteForm = i.state.siteForm;
725 let form: EditSite | CreateSite;
727 if (i.props.siteRes.site_view.local_site.site_setup) {
728 form = stateSiteForm;
731 name: stateSiteForm.name ?? "My site",
732 sidebar: stateSiteForm.sidebar,
733 description: stateSiteForm.description,
734 icon: stateSiteForm.icon,
735 banner: stateSiteForm.banner,
736 community_creation_admin_only:
737 stateSiteForm.community_creation_admin_only,
738 enable_nsfw: stateSiteForm.enable_nsfw,
739 enable_downvotes: stateSiteForm.enable_downvotes,
740 application_question: stateSiteForm.application_question,
741 registration_mode: stateSiteForm.registration_mode,
742 require_email_verification: stateSiteForm.require_email_verification,
743 private_instance: stateSiteForm.private_instance,
744 default_theme: stateSiteForm.default_theme,
745 default_post_listing_type: stateSiteForm.default_post_listing_type,
746 application_email_admins: stateSiteForm.application_email_admins,
747 hide_modlog_mod_names: stateSiteForm.hide_modlog_mod_names,
748 legal_information: stateSiteForm.legal_information,
749 slur_filter_regex: stateSiteForm.slur_filter_regex,
750 actor_name_max_length: stateSiteForm.actor_name_max_length,
751 rate_limit_message: stateSiteForm.rate_limit_message,
752 rate_limit_message_per_second:
753 stateSiteForm.rate_limit_message_per_second,
754 rate_limit_comment: stateSiteForm.rate_limit_comment,
755 rate_limit_comment_per_second:
756 stateSiteForm.rate_limit_comment_per_second,
757 rate_limit_image: stateSiteForm.rate_limit_image,
758 rate_limit_image_per_second: stateSiteForm.rate_limit_image_per_second,
759 rate_limit_post: stateSiteForm.rate_limit_post,
760 rate_limit_post_per_second: stateSiteForm.rate_limit_post_per_second,
761 rate_limit_register: stateSiteForm.rate_limit_register,
762 rate_limit_register_per_second:
763 stateSiteForm.rate_limit_register_per_second,
764 rate_limit_search: stateSiteForm.rate_limit_search,
765 rate_limit_search_per_second:
766 stateSiteForm.rate_limit_search_per_second,
767 federation_enabled: stateSiteForm.federation_enabled,
768 federation_debug: stateSiteForm.federation_debug,
769 federation_worker_count: stateSiteForm.federation_worker_count,
770 captcha_enabled: stateSiteForm.captcha_enabled,
771 captcha_difficulty: stateSiteForm.captcha_difficulty,
772 allowed_instances: stateSiteForm.allowed_instances,
773 blocked_instances: stateSiteForm.blocked_instances,
774 discussion_languages: stateSiteForm.discussion_languages,
779 i.props.onSaveSite(form);
782 handleAddInstance(key: InstanceKey) {
783 const instance = this.state.instance_select[key].trim();
785 if (!validInstanceTLD(instance)) {
789 if (!this.state.siteForm[key]?.includes(instance)) {
790 this.setState(s => ({
794 [key]: [...(s.siteForm[key] ?? []), instance],
797 ...s.instance_select,
802 const oppositeKey: InstanceKey =
803 key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
804 if (this.state.siteForm[oppositeKey]?.includes(instance)) {
805 this.handleRemoveInstance({ key: oppositeKey, instance });
810 handleRemoveInstance({
817 this.setState(s => ({
821 [key]: s.siteForm[key]?.filter(i => i !== instance),
826 handleSiteNameChange(i: SiteForm, event: any) {
827 i.state.siteForm.name = event.target.value;
831 handleSiteSidebarChange(val: string) {
832 this.setState(s => ((s.siteForm.sidebar = val), s));
835 handleSiteLegalInfoChange(val: string) {
836 this.setState(s => ((s.siteForm.legal_information = val), s));
839 handleTaglineChange(i: SiteForm, index: number, val: string) {
840 const taglines = i.state.siteForm.taglines;
842 taglines[index] = val;
847 handleDeleteTaglineClick(
850 event: InfernoMouseEvent<HTMLButtonElement>
852 event.preventDefault();
853 const taglines = i.state.siteForm.taglines;
855 taglines.splice(index, 1);
856 i.state.siteForm.taglines = undefined;
858 i.state.siteForm.taglines = taglines;
863 handleAddTaglineClick(
865 event: InfernoMouseEvent<HTMLButtonElement>
867 event.preventDefault();
868 if (!i.state.siteForm.taglines) {
869 i.state.siteForm.taglines = [];
871 i.state.siteForm.taglines.push("");
875 handleSiteApplicationQuestionChange(val: string) {
876 this.setState(s => ((s.siteForm.application_question = val), s));
879 handleSiteDescChange(i: SiteForm, event: any) {
880 i.state.siteForm.description = event.target.value;
884 handleSiteEnableNsfwChange(i: SiteForm, event: any) {
885 i.state.siteForm.enable_nsfw = event.target.checked;
889 handleSiteRegistrationModeChange(i: SiteForm, event: any) {
890 i.state.siteForm.registration_mode = event.target.value;
894 handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
895 i.state.siteForm.community_creation_admin_only = event.target.checked;
899 handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
900 i.state.siteForm.enable_downvotes = event.target.checked;
904 handleSiteRequireEmailVerification(i: SiteForm, event: any) {
905 i.state.siteForm.require_email_verification = event.target.checked;
909 handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
910 i.state.siteForm.application_email_admins = event.target.checked;
914 handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
915 i.state.siteForm.reports_email_admins = event.target.checked;
919 handleSitePrivateInstance(i: SiteForm, event: any) {
920 i.state.siteForm.private_instance = event.target.checked;
924 handleSiteHideModlogModNames(i: SiteForm, event: any) {
925 i.state.siteForm.hide_modlog_mod_names = event.target.checked;
929 handleSiteDefaultTheme(i: SiteForm, event: any) {
930 i.state.siteForm.default_theme = event.target.value;
934 handleIconUpload(url: string) {
935 this.setState(s => ((s.siteForm.icon = url), s));
939 this.setState(s => ((s.siteForm.icon = ""), s));
942 handleBannerUpload(url: string) {
943 this.setState(s => ((s.siteForm.banner = url), s));
946 handleBannerRemove() {
947 this.setState(s => ((s.siteForm.banner = ""), s));
950 handleSiteSlurFilterRegex(i: SiteForm, event: any) {
951 i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
954 handleSiteActorNameMaxLength(i: SiteForm, event: any) {
956 s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
960 handleSiteFederationEnabled(i: SiteForm, event: any) {
961 i.state.siteForm.federation_enabled = event.target.checked;
965 handleSiteFederationDebug(i: SiteForm, event: any) {
966 i.state.siteForm.federation_debug = event.target.checked;
970 handleSiteFederationWorkerCount(i: SiteForm, event: any) {
973 (s.siteForm.federation_worker_count = Number(event.target.value)), s
978 handleSiteCaptchaEnabled(i: SiteForm, event: any) {
979 i.state.siteForm.captcha_enabled = event.target.checked;
983 handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
984 i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
987 handleDiscussionLanguageChange(val: number[]) {
988 this.setState(s => ((s.siteForm.discussion_languages = val), s));
991 handleDefaultPostListingTypeChange(val: ListingType) {
992 this.setState(s => ((s.siteForm.default_post_listing_type = val), s));