10 GetFederatedInstancesResponse,
13 } from "lemmy-js-client";
14 import { i18n } from "../../i18next";
15 import { WebSocketService } from "../../services";
17 capitalizeFirstLetter,
22 import { Icon, Spinner } from "../common/icon";
23 import { ImageUploadForm } from "../common/image-upload-form";
24 import { LanguageSelect } from "../common/language-select";
25 import { ListingTypeSelect } from "../common/listing-type-select";
26 import { MarkdownTextArea } from "../common/markdown-textarea";
27 import NavigationPrompt from "../common/navigation-prompt";
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 &&
160 this.state.siteForm.name ||
161 this.state.siteForm.sidebar ||
162 this.state.siteForm.application_question ||
163 this.state.siteForm.description
167 <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
170 ? capitalizeFirstLetter(i18n.t("save"))
171 : capitalizeFirstLetter(i18n.t("name"))
172 } ${i18n.t("your_site")}`}</h5>
173 <div className="form-group row">
174 <label className="col-12 col-form-label" htmlFor="create-site-name">
177 <div className="col-12">
180 id="create-site-name"
181 className="form-control"
182 value={this.state.siteForm.name}
183 onInput={linkEvent(this, this.handleSiteNameChange)}
190 <div className="form-group">
191 <label>{i18n.t("icon")}</label>
193 uploadTitle={i18n.t("upload_icon")}
194 imageSrc={this.state.siteForm.icon}
195 onUpload={this.handleIconUpload}
196 onRemove={this.handleIconRemove}
200 <div className="form-group">
201 <label>{i18n.t("banner")}</label>
203 uploadTitle={i18n.t("upload_banner")}
204 imageSrc={this.state.siteForm.banner}
205 onUpload={this.handleBannerUpload}
206 onRemove={this.handleBannerRemove}
209 <div className="form-group row">
210 <label className="col-12 col-form-label" htmlFor="site-desc">
211 {i18n.t("description")}
213 <div className="col-12">
216 className="form-control"
218 value={this.state.siteForm.description}
219 onInput={linkEvent(this, this.handleSiteDescChange)}
224 <div className="form-group row">
225 <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
226 <div className="col-12">
228 initialContent={this.state.siteForm.sidebar}
229 onContentChange={this.handleSiteSidebarChange}
230 hideNavigationWarnings
236 <div className="form-group row">
237 <label className="col-12 col-form-label">
238 {i18n.t("legal_information")}
240 <div className="col-12">
242 initialContent={this.state.siteForm.legal_information}
243 onContentChange={this.handleSiteLegalInfoChange}
244 hideNavigationWarnings
250 <div className="form-group row">
251 <div className="col-12">
252 <div className="form-check">
254 className="form-check-input"
255 id="create-site-downvotes"
257 checked={this.state.siteForm.enable_downvotes}
260 this.handleSiteEnableDownvotesChange
264 className="form-check-label"
265 htmlFor="create-site-downvotes"
267 {i18n.t("enable_downvotes")}
272 <div className="form-group row">
273 <div className="col-12">
274 <div className="form-check">
276 className="form-check-input"
277 id="create-site-enable-nsfw"
279 checked={this.state.siteForm.enable_nsfw}
280 onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
283 className="form-check-label"
284 htmlFor="create-site-enable-nsfw"
286 {i18n.t("enable_nsfw")}
291 <div className="form-group row">
292 <div className="col-12">
294 className="form-check-label mr-2"
295 htmlFor="create-site-registration-mode"
297 {i18n.t("registration_mode")}
300 id="create-site-registration-mode"
301 value={this.state.siteForm.registration_mode}
304 this.handleSiteRegistrationModeChange
306 className="custom-select w-auto"
308 <option value={"RequireApplication"}>
309 {i18n.t("require_registration_application")}
311 <option value={"Open"}>{i18n.t("open_registration")}</option>
312 <option value={"Closed"}>{i18n.t("close_registration")}</option>
316 {this.state.siteForm.registration_mode == "RequireApplication" && (
317 <div className="form-group row">
318 <label className="col-12 col-form-label">
319 {i18n.t("application_questionnaire")}
321 <div className="col-12">
323 initialContent={this.state.siteForm.application_question}
324 onContentChange={this.handleSiteApplicationQuestionChange}
325 hideNavigationWarnings
332 <div className="form-group row">
333 <div className="col-12">
334 <div className="form-check">
336 className="form-check-input"
337 id="create-site-community-creation-admin-only"
339 checked={this.state.siteForm.community_creation_admin_only}
342 this.handleSiteCommunityCreationAdminOnly
346 className="form-check-label"
347 htmlFor="create-site-community-creation-admin-only"
349 {i18n.t("community_creation_admin_only")}
354 <div className="form-group row">
355 <div className="col-12">
356 <div className="form-check">
358 className="form-check-input"
359 id="create-site-require-email-verification"
361 checked={this.state.siteForm.require_email_verification}
364 this.handleSiteRequireEmailVerification
368 className="form-check-label"
369 htmlFor="create-site-require-email-verification"
371 {i18n.t("require_email_verification")}
376 <div className="form-group row">
377 <div className="col-12">
378 <div className="form-check">
380 className="form-check-input"
381 id="create-site-application-email-admins"
383 checked={this.state.siteForm.application_email_admins}
386 this.handleSiteApplicationEmailAdmins
390 className="form-check-label"
391 htmlFor="create-site-email-admins"
393 {i18n.t("application_email_admins")}
398 <div className="form-group row">
399 <div className="col-12">
400 <div className="form-check">
402 className="form-check-input"
403 id="create-site-reports-email-admins"
405 checked={this.state.siteForm.reports_email_admins}
406 onChange={linkEvent(this, this.handleSiteReportsEmailAdmins)}
409 className="form-check-label"
410 htmlFor="create-site-reports-email-admins"
412 {i18n.t("reports_email_admins")}
417 <div className="form-group row">
418 <div className="col-12">
420 className="form-check-label mr-2"
421 htmlFor="create-site-default-theme"
426 id="create-site-default-theme"
427 value={this.state.siteForm.default_theme}
428 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
429 className="custom-select w-auto"
431 <option value="browser">{i18n.t("browser_default")}</option>
432 {this.state.themeList?.map(theme => (
433 <option key={theme} value={theme}>
440 {this.props.showLocal && (
441 <form className="form-group row">
442 <label className="col-sm-3">{i18n.t("listing_type")}</label>
443 <div className="col-sm-9">
446 this.state.siteForm.default_post_listing_type ?? "Local"
449 showSubscribed={false}
450 onChange={this.handleDefaultPostListingTypeChange}
455 <div className="form-group row">
456 <div className="col-12">
457 <div className="form-check">
459 className="form-check-input"
460 id="create-site-private-instance"
462 checked={this.state.siteForm.private_instance}
463 onChange={linkEvent(this, this.handleSitePrivateInstance)}
466 className="form-check-label"
467 htmlFor="create-site-private-instance"
469 {i18n.t("private_instance")}
474 <div className="form-group row">
475 <div className="col-12">
476 <div className="form-check">
478 className="form-check-input"
479 id="create-site-hide-modlog-mod-names"
481 checked={this.state.siteForm.hide_modlog_mod_names}
482 onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
485 className="form-check-label"
486 htmlFor="create-site-hide-modlog-mod-names"
488 {i18n.t("hide_modlog_mod_names")}
493 <div className="form-group row">
495 className="col-12 col-form-label"
496 htmlFor="create-site-slur-filter-regex"
498 {i18n.t("slur_filter_regex")}
500 <div className="col-12">
503 id="create-site-slur-filter-regex"
504 placeholder="(word1|word2)"
505 className="form-control"
506 value={this.state.siteForm.slur_filter_regex}
507 onInput={linkEvent(this, this.handleSiteSlurFilterRegex)}
513 allLanguages={this.props.siteRes.all_languages}
514 siteLanguages={this.props.siteRes.discussion_languages}
515 selectedLanguageIds={this.state.siteForm.discussion_languages}
517 onChange={this.handleDiscussionLanguageChange}
520 <div className="form-group row">
522 className="col-12 col-form-label"
523 htmlFor="create-site-actor-name"
525 {i18n.t("actor_name_max_length")}
527 <div className="col-12">
530 id="create-site-actor-name"
531 className="form-control"
533 value={this.state.siteForm.actor_name_max_length}
534 onInput={linkEvent(this, this.handleSiteActorNameMaxLength)}
538 <div className="form-group row">
539 <div className="col-12">
540 <div className="form-check">
542 className="form-check-input"
543 id="create-site-federation-enabled"
545 checked={this.state.siteForm.federation_enabled}
546 onChange={linkEvent(this, this.handleSiteFederationEnabled)}
549 className="form-check-label"
550 htmlFor="create-site-federation-enabled"
552 {i18n.t("federation_enabled")}
557 {this.state.siteForm.federation_enabled && (
559 <div className="form-group row">
560 {this.federatedInstanceSelect("allowed_instances")}
561 {this.federatedInstanceSelect("blocked_instances")}
563 <div className="form-group row">
564 <div className="col-12">
565 <div className="form-check">
567 className="form-check-input"
568 id="create-site-federation-debug"
570 checked={this.state.siteForm.federation_debug}
571 onChange={linkEvent(this, this.handleSiteFederationDebug)}
574 className="form-check-label"
575 htmlFor="create-site-federation-debug"
577 {i18n.t("federation_debug")}
582 <div className="form-group row">
584 className="col-12 col-form-label"
585 htmlFor="create-site-federation-worker-count"
587 {i18n.t("federation_worker_count")}
589 <div className="col-12">
592 id="create-site-federation-worker-count"
593 className="form-control"
595 value={this.state.siteForm.federation_worker_count}
598 this.handleSiteFederationWorkerCount
605 <div className="form-group row">
606 <div className="col-12">
607 <div className="form-check">
609 className="form-check-input"
610 id="create-site-captcha-enabled"
612 checked={this.state.siteForm.captcha_enabled}
613 onChange={linkEvent(this, this.handleSiteCaptchaEnabled)}
616 className="form-check-label"
617 htmlFor="create-site-captcha-enabled"
619 {i18n.t("captcha_enabled")}
624 {this.state.siteForm.captcha_enabled && (
625 <div className="form-group row">
626 <div className="col-12">
628 className="form-check-label mr-2"
629 htmlFor="create-site-captcha-difficulty"
631 {i18n.t("captcha_difficulty")}
634 id="create-site-captcha-difficulty"
635 value={this.state.siteForm.captcha_difficulty}
636 onChange={linkEvent(this, this.handleSiteCaptchaDifficulty)}
637 className="custom-select w-auto"
639 <option value="easy">{i18n.t("easy")}</option>
640 <option value="medium">{i18n.t("medium")}</option>
641 <option value="hard">{i18n.t("hard")}</option>
646 <div className="form-group row">
647 <div className="col-12">
650 className="btn btn-secondary mr-2"
651 disabled={this.state.loading}
653 {this.state.loading ? (
656 capitalizeFirstLetter(i18n.t("save"))
658 capitalizeFirstLetter(i18n.t("create"))
668 federatedInstanceSelect(key: InstanceKey) {
669 const id = `create_site_${key}`;
670 const value = this.state.instance_select[key];
671 const selectedInstances = this.state.siteForm[key];
673 <div className="col-12 col-md-6">
674 <label className="col-form-label" htmlFor={id}>
677 <div className="d-flex justify-content-between align-items-center">
680 placeholder="instance.tld"
682 className="form-control"
684 onInput={linkEvent(key, this.handleInstanceTextChange)}
685 onKeyUp={linkEvent(key, this.handleInstanceEnterPress)}
689 className="btn btn-sm bg-success ml-2"
690 onClick={linkEvent(key, this.handleAddInstance)}
692 -1 /* Making this untabble because handling enter key in text input makes keyboard support for this button redundant */
695 <Icon icon="add" classes="icon-inline text-light m-auto" />
698 {selectedInstances && selectedInstances.length > 0 && (
699 <ul className="mt-3 list-unstyled w-100 d-flex flex-column justify-content-around align-items-center">
700 {selectedInstances.map(instance => (
703 className="my-1 w-100 w-md-75 d-flex align-items-center justify-content-between"
705 <label className="d-block m-0 w-100 " htmlFor={instance}>
706 <strong>{instance}</strong>
711 className="btn btn-sm bg-danger"
714 this.handleRemoveInstance
717 <Icon icon="x" classes="icon-inline text-light m-auto" />
727 handleInstanceTextChange(type: InstanceKey, event: any) {
728 this.setState(s => ({
731 ...s.instance_select,
732 [type]: event.target.value,
737 handleInstanceEnterPress(
739 event: InfernoKeyboardEvent<HTMLInputElement>
741 if (event.code.toLowerCase() === "enter") {
742 event.preventDefault();
744 this.handleAddInstance(key);
748 handleCreateSiteSubmit(i: SiteForm, event: any) {
749 event.preventDefault();
750 i.setState({ loading: true });
751 const auth = myAuth() ?? "TODO";
752 i.setState(s => ((s.siteForm.auth = auth), s));
753 if (i.props.siteRes.site_view.local_site.site_setup) {
754 WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
756 const sForm = i.state.siteForm;
757 const form: CreateSite = {
758 name: sForm.name ?? "My site",
759 sidebar: sForm.sidebar,
760 description: sForm.description,
762 banner: sForm.banner,
763 community_creation_admin_only: sForm.community_creation_admin_only,
764 enable_nsfw: sForm.enable_nsfw,
765 enable_downvotes: sForm.enable_downvotes,
766 application_question: sForm.application_question,
767 registration_mode: sForm.registration_mode,
768 require_email_verification: sForm.require_email_verification,
769 private_instance: sForm.private_instance,
770 default_theme: sForm.default_theme,
771 default_post_listing_type: sForm.default_post_listing_type,
772 application_email_admins: sForm.application_email_admins,
773 hide_modlog_mod_names: sForm.hide_modlog_mod_names,
774 legal_information: sForm.legal_information,
775 slur_filter_regex: sForm.slur_filter_regex,
776 actor_name_max_length: sForm.actor_name_max_length,
777 federation_enabled: sForm.federation_enabled,
778 federation_debug: sForm.federation_debug,
779 federation_worker_count: sForm.federation_worker_count,
780 captcha_enabled: sForm.captcha_enabled,
781 captcha_difficulty: sForm.captcha_difficulty,
782 allowed_instances: sForm.allowed_instances,
783 blocked_instances: sForm.blocked_instances,
784 discussion_languages: sForm.discussion_languages,
787 WebSocketService.Instance.send(wsClient.createSite(form));
792 handleAddInstance(key: InstanceKey) {
793 const instance = this.state.instance_select[key].trim();
794 if (!this.state.siteForm[key]?.includes(instance)) {
795 this.setState(s => ({
799 [key]: [...(s.siteForm[key] ?? []), instance],
802 ...s.instance_select,
807 const oppositeKey: InstanceKey =
808 key === "allowed_instances" ? "blocked_instances" : "allowed_instances";
809 if (this.state.siteForm[oppositeKey]?.includes(instance)) {
810 this.handleRemoveInstance({ key: oppositeKey, instance });
815 handleRemoveInstance({
822 this.setState(s => ({
826 [key]: s.siteForm[key]?.filter(i => i !== instance),
831 handleSiteNameChange(i: SiteForm, event: any) {
832 i.state.siteForm.name = event.target.value;
836 handleSiteSidebarChange(val: string) {
837 this.setState(s => ((s.siteForm.sidebar = val), s));
840 handleSiteLegalInfoChange(val: string) {
841 this.setState(s => ((s.siteForm.legal_information = val), s));
844 handleTaglineChange(i: SiteForm, index: number, val: string) {
845 const taglines = i.state.siteForm.taglines;
847 taglines[index] = val;
852 handleDeleteTaglineClick(
855 event: InfernoMouseEvent<HTMLButtonElement>
857 event.preventDefault();
858 const taglines = i.state.siteForm.taglines;
860 taglines.splice(index, 1);
861 i.state.siteForm.taglines = undefined;
863 i.state.siteForm.taglines = taglines;
868 handleAddTaglineClick(
870 event: InfernoMouseEvent<HTMLButtonElement>
872 event.preventDefault();
873 if (!i.state.siteForm.taglines) {
874 i.state.siteForm.taglines = [];
876 i.state.siteForm.taglines.push("");
880 handleSiteApplicationQuestionChange(val: string) {
881 this.setState(s => ((s.siteForm.application_question = val), s));
884 handleSiteDescChange(i: SiteForm, event: any) {
885 i.state.siteForm.description = event.target.value;
889 handleSiteEnableNsfwChange(i: SiteForm, event: any) {
890 i.state.siteForm.enable_nsfw = event.target.checked;
894 handleSiteRegistrationModeChange(i: SiteForm, event: any) {
895 i.state.siteForm.registration_mode = event.target.value;
899 handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
900 i.state.siteForm.community_creation_admin_only = event.target.checked;
904 handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
905 i.state.siteForm.enable_downvotes = event.target.checked;
909 handleSiteRequireEmailVerification(i: SiteForm, event: any) {
910 i.state.siteForm.require_email_verification = event.target.checked;
914 handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
915 i.state.siteForm.application_email_admins = event.target.checked;
919 handleSiteReportsEmailAdmins(i: SiteForm, event: any) {
920 i.state.siteForm.reports_email_admins = event.target.checked;
924 handleSitePrivateInstance(i: SiteForm, event: any) {
925 i.state.siteForm.private_instance = event.target.checked;
929 handleSiteHideModlogModNames(i: SiteForm, event: any) {
930 i.state.siteForm.hide_modlog_mod_names = event.target.checked;
934 handleSiteDefaultTheme(i: SiteForm, event: any) {
935 i.state.siteForm.default_theme = event.target.value;
939 handleIconUpload(url: string) {
940 this.setState(s => ((s.siteForm.icon = url), s));
944 this.setState(s => ((s.siteForm.icon = ""), s));
947 handleBannerUpload(url: string) {
948 this.setState(s => ((s.siteForm.banner = url), s));
951 handleBannerRemove() {
952 this.setState(s => ((s.siteForm.banner = ""), s));
955 handleSiteSlurFilterRegex(i: SiteForm, event: any) {
956 i.setState(s => ((s.siteForm.slur_filter_regex = event.target.value), s));
959 handleSiteActorNameMaxLength(i: SiteForm, event: any) {
961 s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s)
965 handleSiteFederationEnabled(i: SiteForm, event: any) {
966 i.state.siteForm.federation_enabled = event.target.checked;
970 handleSiteFederationDebug(i: SiteForm, event: any) {
971 i.state.siteForm.federation_debug = event.target.checked;
975 handleSiteFederationWorkerCount(i: SiteForm, event: any) {
978 (s.siteForm.federation_worker_count = Number(event.target.value)), s
983 handleSiteCaptchaEnabled(i: SiteForm, event: any) {
984 i.state.siteForm.captcha_enabled = event.target.checked;
988 handleSiteCaptchaDifficulty(i: SiteForm, event: any) {
989 i.setState(s => ((s.siteForm.captcha_difficulty = event.target.value), s));
992 handleDiscussionLanguageChange(val: number[]) {
993 this.setState(s => ((s.siteForm.discussion_languages = val), s));
996 handleDefaultPostListingTypeChange(val: ListingType) {
997 this.setState(s => ((s.siteForm.default_post_listing_type = val), s));