7 import { Prompt } from "inferno-router";
11 GetFederatedInstancesResponse,
14 } from "lemmy-js-client";
15 import { i18n } from "../../i18next";
16 import { WebSocketService } from "../../services";
18 capitalizeFirstLetter,
23 import { Icon, Spinner } from "../common/icon";
24 import { ImageUploadForm } from "../common/image-upload-form";
25 import { LanguageSelect } from "../common/language-select";
26 import { ListingTypeSelect } from "../common/listing-type-select";
27 import { MarkdownTextArea } from "../common/markdown-textarea";
29 interface SiteFormProps {
30 siteRes: GetSiteResponse;
31 instancesRes?: GetFederatedInstancesResponse;
35 interface SiteFormState {
40 allowed_instances: string;
41 blocked_instances: string;
45 type InstanceKey = "allowed_instances" | "blocked_instances";
47 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
48 state: SiteFormState = {
54 allowed_instances: "",
55 blocked_instances: "",
59 constructor(props: any, context: any) {
60 super(props, context);
62 this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
63 this.handleSiteLegalInfoChange = this.handleSiteLegalInfoChange.bind(this);
64 this.handleSiteApplicationQuestionChange =
65 this.handleSiteApplicationQuestionChange.bind(this);
67 this.handleIconUpload = this.handleIconUpload.bind(this);
68 this.handleIconRemove = this.handleIconRemove.bind(this);
70 this.handleBannerUpload = this.handleBannerUpload.bind(this);
71 this.handleBannerRemove = this.handleBannerRemove.bind(this);
73 this.handleDefaultPostListingTypeChange =
74 this.handleDefaultPostListingTypeChange.bind(this);
76 this.handleDiscussionLanguageChange =
77 this.handleDiscussionLanguageChange.bind(this);
79 const site = this.props.siteRes.site_view.site;
80 const ls = this.props.siteRes.site_view.local_site;
85 sidebar: site.sidebar,
86 description: site.description,
87 enable_downvotes: ls.enable_downvotes,
88 registration_mode: ls.registration_mode,
89 enable_nsfw: ls.enable_nsfw,
90 community_creation_admin_only: ls.community_creation_admin_only,
93 require_email_verification: ls.require_email_verification,
94 application_question: ls.application_question,
95 private_instance: ls.private_instance,
96 default_theme: ls.default_theme,
97 default_post_listing_type: ls.default_post_listing_type,
98 legal_information: ls.legal_information,
99 application_email_admins: ls.application_email_admins,
100 reports_email_admins: ls.reports_email_admins,
101 hide_modlog_mod_names: ls.hide_modlog_mod_names,
102 discussion_languages: this.props.siteRes.discussion_languages,
103 slur_filter_regex: ls.slur_filter_regex,
104 actor_name_max_length: ls.actor_name_max_length,
105 federation_enabled: ls.federation_enabled,
106 federation_debug: ls.federation_debug,
107 federation_worker_count: ls.federation_worker_count,
108 captcha_enabled: ls.captcha_enabled,
109 captcha_difficulty: ls.captcha_difficulty,
111 this.props.instancesRes?.federated_instances?.allowed.map(
115 this.props.instancesRes?.federated_instances?.blocked.map(
123 async componentDidMount() {
124 this.setState({ themeList: await fetchThemeList() });
127 // Necessary to stop the loading
128 componentWillReceiveProps() {
129 this.setState({ loading: false });
132 componentDidUpdate() {
134 !this.state.loading &&
135 !this.props.siteRes.site_view.local_site.site_setup &&
136 (this.state.siteForm.name ||
137 this.state.siteForm.sidebar ||
138 this.state.siteForm.application_question ||
139 this.state.siteForm.description)
141 window.onbeforeunload = () => true;
143 window.onbeforeunload = null;
147 componentWillUnmount() {
148 window.onbeforeunload = null;
152 const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
157 !this.state.loading &&
159 (this.state.siteForm.name ||
160 this.state.siteForm.sidebar ||
161 this.state.siteForm.application_question ||
162 this.state.siteForm.description)
164 message={i18n.t("block_leaving")}
166 <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
169 ? capitalizeFirstLetter(i18n.t("save"))
170 : capitalizeFirstLetter(i18n.t("name"))
171 } ${i18n.t("your_site")}`}</h5>
172 <div className="form-group row">
173 <label className="col-12 col-form-label" htmlFor="create-site-name">
176 <div className="col-12">
179 id="create-site-name"
180 className="form-control"
181 value={this.state.siteForm.name}
182 onInput={linkEvent(this, this.handleSiteNameChange)}
189 <div className="form-group">
190 <label>{i18n.t("icon")}</label>
192 uploadTitle={i18n.t("upload_icon")}
193 imageSrc={this.state.siteForm.icon}
194 onUpload={this.handleIconUpload}
195 onRemove={this.handleIconRemove}
199 <div className="form-group">
200 <label>{i18n.t("banner")}</label>
202 uploadTitle={i18n.t("upload_banner")}
203 imageSrc={this.state.siteForm.banner}
204 onUpload={this.handleBannerUpload}
205 onRemove={this.handleBannerRemove}
208 <div className="form-group row">
209 <label className="col-12 col-form-label" htmlFor="site-desc">
210 {i18n.t("description")}
212 <div className="col-12">
215 className="form-control"
217 value={this.state.siteForm.description}
218 onInput={linkEvent(this, this.handleSiteDescChange)}
223 <div className="form-group row">
224 <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
225 <div className="col-12">
227 initialContent={this.state.siteForm.sidebar}
228 onContentChange={this.handleSiteSidebarChange}
229 hideNavigationWarnings
235 <div className="form-group row">
236 <label className="col-12 col-form-label">
237 {i18n.t("legal_information")}
239 <div className="col-12">
241 initialContent={this.state.siteForm.legal_information}
242 onContentChange={this.handleSiteLegalInfoChange}
243 hideNavigationWarnings
249 <div className="form-group row">
250 <div className="col-12">
251 <div className="form-check">
253 className="form-check-input"
254 id="create-site-downvotes"
256 checked={this.state.siteForm.enable_downvotes}
259 this.handleSiteEnableDownvotesChange
263 className="form-check-label"
264 htmlFor="create-site-downvotes"
266 {i18n.t("enable_downvotes")}
271 <div className="form-group row">
272 <div className="col-12">
273 <div className="form-check">
275 className="form-check-input"
276 id="create-site-enable-nsfw"
278 checked={this.state.siteForm.enable_nsfw}
279 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
282 className="form-check-label"
283 htmlFor="create-site-enable-nsfw"
285 {i18n.t("enable_nsfw")}
290 <div className="form-group row">
291 <div className="col-12">
293 className="form-check-label mr-2"
294 htmlFor="create-site-registration-mode"
296 {i18n.t("registration_mode")}
299 id="create-site-registration-mode"
300 value={this.state.siteForm.registration_mode}
303 this.handleSiteRegistrationModeChange
305 className="custom-select w-auto"
307 <option value={"RequireApplication"}>
308 {i18n.t("require_registration_application")}
310 <option value={"Open"}>{i18n.t("open_registration")}</option>
311 <option value={"Closed"}>{i18n.t("close_registration")}</option>
315 {this.state.siteForm.registration_mode == "RequireApplication" && (
316 <div className="form-group row">
317 <label className="col-12 col-form-label">
318 {i18n.t("application_questionnaire")}
320 <div className="col-12">
322 initialContent={this.state.siteForm.application_question}
323 onContentChange={this.handleSiteApplicationQuestionChange}
324 hideNavigationWarnings
331 <div className="form-group row">
332 <div className="col-12">
333 <div className="form-check">
335 className="form-check-input"
336 id="create-site-community-creation-admin-only"
338 checked={this.state.siteForm.community_creation_admin_only}
341 this.handleSiteCommunityCreationAdminOnly
345 className="form-check-label"
346 htmlFor="create-site-community-creation-admin-only"
348 {i18n.t("community_creation_admin_only")}
353 <div className="form-group row">
354 <div className="col-12">
355 <div className="form-check">
357 className="form-check-input"
358 id="create-site-require-email-verification"
360 checked={this.state.siteForm.require_email_verification}
363 this.handleSiteRequireEmailVerification
367 className="form-check-label"
368 htmlFor="create-site-require-email-verification"
370 {i18n.t("require_email_verification")}
375 <div className="form-group row">
376 <div className="col-12">
377 <div className="form-check">
379 className="form-check-input"
380 id="create-site-application-email-admins"
382 checked={this.state.siteForm.application_email_admins}
385 this.handleSiteApplicationEmailAdmins
389 className="form-check-label"
390 htmlFor="create-site-email-admins"
392 {i18n.t("application_email_admins")}
397 <div className="form-group row">
398 <div className="col-12">
399 <div className="form-check">
401 className="form-check-input"
402 id="create-site-reports-email-admins"
404 checked={this.state.siteForm.reports_email_admins}
405 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
408 className="form-check-label"
409 htmlFor="create-site-reports-email-admins"
411 {i18n.t("reports_email_admins")}
416 <div className="form-group row">
417 <div className="col-12">
419 className="form-check-label mr-2"
420 htmlFor="create-site-default-theme"
425 id="create-site-default-theme"
426 value={this.state.siteForm.default_theme}
427 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
428 className="custom-select w-auto"
430 <option value="browser">{i18n.t("browser_default")}</option>
431 {this.state.themeList?.map(theme => (
432 <option key={theme} value={theme}>
439 {this.props.showLocal && (
440 <form className="form-group row">
441 <label className="col-sm-3">{i18n.t("listing_type")}</label>
442 <div className="col-sm-9">
445 this.state.siteForm.default_post_listing_type ?? "Local"
448 showSubscribed={false}
449 onChange={this.handleDefaultPostListingTypeChange}
454 <div className="form-group row">
455 <div className="col-12">
456 <div className="form-check">
458 className="form-check-input"
459 id="create-site-private-instance"
461 checked={this.state.siteForm.private_instance}
462 onChange={linkEvent(this, this.handleSitePrivateInstance)}
465 className="form-check-label"
466 htmlFor="create-site-private-instance"
468 {i18n.t("private_instance")}
473 <div className="form-group row">
474 <div className="col-12">
475 <div className="form-check">
477 className="form-check-input"
478 id="create-site-hide-modlog-mod-names"
480 checked={this.state.siteForm.hide_modlog_mod_names}
481 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
484 className="form-check-label"
485 htmlFor="create-site-hide-modlog-mod-names"
487 {i18n.t("hide_modlog_mod_names")}
492 <div className="form-group row">
494 className="col-12 col-form-label"
495 htmlFor="create-site-slur-filter-regex"
497 {i18n.t("slur_filter_regex")}
499 <div className="col-12">
502 id="create-site-slur-filter-regex"
503 placeholder="(word1|word2)"
504 className="form-control"
505 value={this.state.siteForm.slur_filter_regex}
506 onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
512 allLanguages={this.props.siteRes.all_languages}
513 siteLanguages={this.props.siteRes.discussion_languages}
514 selectedLanguageIds={this.state.siteForm.discussion_languages}
516 onChange={this.handleDiscussionLanguageChange}
519 <div className="form-group row">
521 className="col-12 col-form-label"
522 htmlFor="create-site-actor-name"
524 {i18n.t("actor_name_max_length")}
526 <div className="col-12">
529 id="create-site-actor-name"
530 className="form-control"
532 value={this.state.siteForm.actor_name_max_length}
533 onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
537 <div className="form-group row">
538 <div className="col-12">
539 <div className="form-check">
541 className="form-check-input"
542 id="create-site-federation-enabled"
544 checked={this.state.siteForm.federation_enabled}
545 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
548 className="form-check-label"
549 htmlFor="create-site-federation-enabled"
551 {i18n.t("federation_enabled")}
556 {this.state.siteForm.federation_enabled && (
558 <div className="form-group row">
559 {this.federatedInstanceSelect("allowed_instances")}
560 {this.federatedInstanceSelect("blocked_instances")}
562 <div className="form-group row">
563 <div className="col-12">
564 <div className="form-check">
566 className="form-check-input"
567 id="create-site-federation-debug"
569 checked={this.state.siteForm.federation_debug}
570 onChange={linkEvent(this, this.handleSiteFederationDebug)}
573 className="form-check-label"
574 htmlFor="create-site-federation-debug"
576 {i18n.t("federation_debug")}
581 <div className="form-group row">
583 className="col-12 col-form-label"
584 htmlFor="create-site-federation-worker-count"
586 {i18n.t("federation_worker_count")}
588 <div className="col-12">
591 id="create-site-federation-worker-count"
592 className="form-control"
594 value={this.state.siteForm.federation_worker_count}
597 this.handleSiteFederationWorkerCount
604 <div className="form-group row">
605 <div className="col-12">
606 <div className="form-check">
608 className="form-check-input"
609 id="create-site-captcha-enabled"
611 checked={this.state.siteForm.captcha_enabled}
612 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
615 className="form-check-label"
616 htmlFor="create-site-captcha-enabled"
618 {i18n.t("captcha_enabled")}
623 {this.state.siteForm.captcha_enabled && (
624 <div className="form-group row">
625 <div className="col-12">
627 className="form-check-label mr-2"
628 htmlFor="create-site-captcha-difficulty"
630 {i18n.t("captcha_difficulty")}
633 id="create-site-captcha-difficulty"
634 value={this.state.siteForm.captcha_difficulty}
635 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
636 className="custom-select w-auto"
638 <option value="easy">{i18n.t("easy")}</option>
639 <option value="medium">{i18n.t("medium")}</option>
640 <option value="hard">{i18n.t("hard")}</option>
645 <div className="form-group row">
646 <div className="col-12">
649 className="btn btn-secondary mr-2"
650 disabled={this.state.loading}
652 {this.state.loading ? (
655 capitalizeFirstLetter(i18n.t("save"))
657 capitalizeFirstLetter(i18n.t("create"))
667 federatedInstanceSelect(key: InstanceKey) {
668 const id = `create_site_${key}`;
669 const value = this.state.instance_select[key];
670 const selectedInstances = this.state.siteForm[key];
672 <div className="col-12 col-md-6">
673 <label className="col-form-label" htmlFor={id}>
676 <div className="d-flex justify-content-between align-items-center">
679 placeholder="instance.tld"
681 className="form-control"
683 onInput={linkEvent(key, this.handleInstanceTextChange)}
684 onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
688 className="btn btn-sm bg-success ml-2"
689 onClick={linkEvent(key, this.handleAddInstance)}
691 -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
694 <Icon icon="add" classes="icon-inline text-light m-auto" />
697 {selectedInstances && selectedInstances.length > 0 && (
698 <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
699 {selectedInstances.map(instance => (
702 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
704 <label className="d-block m-0 w-100 " htmlFor={instance}>
705 <strong>{instance}</strong>
710 className="btn btn-sm bg-danger"
713 this.handleRemoveInstance
716 <Icon icon="x" classes="icon-inline text-light m-auto" />
726 handleInstanceTextChange(type: InstanceKey, event: any) {
727 this.setState(s => ({
730 ...s.instance_select,
731 [type]: event.target.value,
736 handleInstanceEnterPress(
738 event: InfernoKeyboardEvent<HTMLInputElement>
740 if (event.code.toLowerCase() === "enter") {
741 event.preventDefault();
743 this.handleAddInstance(key);
747 handleCreateSiteSubmit(i: SiteForm, event: any) {
748 event.preventDefault();
749 i.setState({ loading: true });
750 const auth = myAuth() ?? "TODO";
751 i.setState(s => ((s.siteForm.auth = auth), s));
752 if (i.props.siteRes.site_view.local_site.site_setup) {
753 WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
755 const sForm = i.state.siteForm;
756 const form: CreateSite = {
757 name: sForm.name ?? "My site",
758 sidebar: sForm.sidebar,
759 description: sForm.description,
761 banner: sForm.banner,
762 community_creation_admin_only: sForm.community_creation_admin_only,
763 enable_nsfw: sForm.enable_nsfw,
764 enable_downvotes: sForm.enable_downvotes,
765 application_question: sForm.application_question,
766 registration_mode: sForm.registration_mode,
767 require_email_verification: sForm.require_email_verification,
768 private_instance: sForm.private_instance,
769 default_theme: sForm.default_theme,
770 default_post_listing_type: sForm.default_post_listing_type,
771 application_email_admins: sForm.application_email_admins,
772 hide_modlog_mod_names: sForm.hide_modlog_mod_names,
773 legal_information: sForm.legal_information,
774 slur_filter_regex: sForm.slur_filter_regex,
775 actor_name_max_length: sForm.actor_name_max_length,
776 federation_enabled: sForm.federation_enabled,
777 federation_debug: sForm.federation_debug,
778 federation_worker_count: sForm.federation_worker_count,
779 captcha_enabled: sForm.captcha_enabled,
780 captcha_difficulty: sForm.captcha_difficulty,
781 allowed_instances: sForm.allowed_instances,
782 blocked_instances: sForm.blocked_instances,
783 discussion_languages: sForm.discussion_languages,
786 WebSocketService.Instance.send(wsClient.createSite(form));
791 handleAddInstance(key: InstanceKey) {
792 const instance = this.state.instance_select[key].trim();
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));