]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/community-form.tsx
805e49638c457f0adf73759d85a3a6d82e711504
[lemmy-ui.git] / src / shared / components / community / community-form.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import { Prompt } from "inferno-router";
4 import {
5   CommunityResponse,
6   CommunityView,
7   CreateCommunity,
8   EditCommunity,
9   Language,
10   toUndefined,
11   UserOperation,
12   wsJsonToRes,
13   wsUserOp,
14 } from "lemmy-js-client";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { UserService, WebSocketService } from "../../services";
18 import {
19   auth,
20   capitalizeFirstLetter,
21   randomStr,
22   wsClient,
23   wsSubscribe,
24 } from "../../utils";
25 import { Icon, Spinner } from "../common/icon";
26 import { ImageUploadForm } from "../common/image-upload-form";
27 import { LanguageSelect } from "../common/language-select";
28 import { MarkdownTextArea } from "../common/markdown-textarea";
29
30 interface CommunityFormProps {
31   community_view: Option<CommunityView>; // If a community is given, that means this is an edit
32   allLanguages: Language[];
33   siteLanguages: number[];
34   communityLanguages: Option<number[]>;
35   onCancel?(): any;
36   onCreate?(community: CommunityView): any;
37   onEdit?(community: CommunityView): any;
38   enableNsfw?: boolean;
39 }
40
41 interface CommunityFormState {
42   communityForm: CreateCommunity;
43   loading: boolean;
44 }
45
46 export class CommunityForm extends Component<
47   CommunityFormProps,
48   CommunityFormState
49 > {
50   private id = `community-form-${randomStr()}`;
51   private subscription: Subscription;
52
53   private emptyState: CommunityFormState = {
54     communityForm: new CreateCommunity({
55       name: undefined,
56       title: undefined,
57       description: None,
58       discussion_languages: this.props.communityLanguages,
59       nsfw: None,
60       icon: None,
61       banner: None,
62       posting_restricted_to_mods: None,
63       auth: undefined,
64     }),
65     loading: false,
66   };
67
68   constructor(props: any, context: any) {
69     super(props, context);
70
71     this.state = this.emptyState;
72
73     this.handleCommunityDescriptionChange =
74       this.handleCommunityDescriptionChange.bind(this);
75
76     this.handleIconUpload = this.handleIconUpload.bind(this);
77     this.handleIconRemove = this.handleIconRemove.bind(this);
78
79     this.handleBannerUpload = this.handleBannerUpload.bind(this);
80     this.handleBannerRemove = this.handleBannerRemove.bind(this);
81
82     this.handleDiscussionLanguageChange =
83       this.handleDiscussionLanguageChange.bind(this);
84
85     this.parseMessage = this.parseMessage.bind(this);
86     this.subscription = wsSubscribe(this.parseMessage);
87
88     if (this.props.community_view.isSome()) {
89       let cv = this.props.community_view.unwrap();
90       this.state = {
91         ...this.state,
92         communityForm: new CreateCommunity({
93           name: cv.community.name,
94           title: cv.community.title,
95           description: cv.community.description,
96           nsfw: Some(cv.community.nsfw),
97           icon: cv.community.icon,
98           banner: cv.community.banner,
99           posting_restricted_to_mods: Some(
100             cv.community.posting_restricted_to_mods
101           ),
102           discussion_languages: this.props.communityLanguages,
103           auth: undefined,
104         }),
105       };
106     }
107   }
108
109   componentDidUpdate() {
110     if (
111       !this.state.loading &&
112       (this.state.communityForm.name ||
113         this.state.communityForm.title ||
114         this.state.communityForm.description.isSome())
115     ) {
116       window.onbeforeunload = () => true;
117     } else {
118       window.onbeforeunload = undefined;
119     }
120   }
121
122   componentWillUnmount() {
123     this.subscription.unsubscribe();
124     window.onbeforeunload = null;
125   }
126
127   render() {
128     return (
129       <>
130         <Prompt
131           when={
132             !this.state.loading &&
133             (this.state.communityForm.name ||
134               this.state.communityForm.title ||
135               this.state.communityForm.description.isSome())
136           }
137           message={i18n.t("block_leaving")}
138         />
139         <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
140           {this.props.community_view.isNone() && (
141             <div className="form-group row">
142               <label
143                 className="col-12 col-sm-2 col-form-label"
144                 htmlFor="community-name"
145               >
146                 {i18n.t("name")}
147                 <span
148                   className="position-absolute pointer unselectable ml-2 text-muted"
149                   data-tippy-content={i18n.t("name_explain")}
150                 >
151                   <Icon icon="help-circle" classes="icon-inline" />
152                 </span>
153               </label>
154               <div className="col-12 col-sm-10">
155                 <input
156                   type="text"
157                   id="community-name"
158                   className="form-control"
159                   value={this.state.communityForm.name}
160                   onInput={linkEvent(this, this.handleCommunityNameChange)}
161                   required
162                   minLength={3}
163                   pattern="[a-z0-9_]+"
164                   title={i18n.t("community_reqs")}
165                 />
166               </div>
167             </div>
168           )}
169           <div className="form-group row">
170             <label
171               className="col-12 col-sm-2 col-form-label"
172               htmlFor="community-title"
173             >
174               {i18n.t("display_name")}
175               <span
176                 className="position-absolute pointer unselectable ml-2 text-muted"
177                 data-tippy-content={i18n.t("display_name_explain")}
178               >
179                 <Icon icon="help-circle" classes="icon-inline" />
180               </span>
181             </label>
182             <div className="col-12 col-sm-10">
183               <input
184                 type="text"
185                 id="community-title"
186                 value={this.state.communityForm.title}
187                 onInput={linkEvent(this, this.handleCommunityTitleChange)}
188                 className="form-control"
189                 required
190                 minLength={3}
191                 maxLength={100}
192               />
193             </div>
194           </div>
195           <div className="form-group row">
196             <label className="col-12 col-sm-2">{i18n.t("icon")}</label>
197             <div className="col-12 col-sm-10">
198               <ImageUploadForm
199                 uploadTitle={i18n.t("upload_icon")}
200                 imageSrc={this.state.communityForm.icon}
201                 onUpload={this.handleIconUpload}
202                 onRemove={this.handleIconRemove}
203                 rounded
204               />
205             </div>
206           </div>
207           <div className="form-group row">
208             <label className="col-12 col-sm-2">{i18n.t("banner")}</label>
209             <div className="col-12 col-sm-10">
210               <ImageUploadForm
211                 uploadTitle={i18n.t("upload_banner")}
212                 imageSrc={this.state.communityForm.banner}
213                 onUpload={this.handleBannerUpload}
214                 onRemove={this.handleBannerRemove}
215               />
216             </div>
217           </div>
218           <div className="form-group row">
219             <label className="col-12 col-sm-2 col-form-label" htmlFor={this.id}>
220               {i18n.t("sidebar")}
221             </label>
222             <div className="col-12 col-sm-10">
223               <MarkdownTextArea
224                 initialContent={this.state.communityForm.description}
225                 initialLanguageId={None}
226                 placeholder={Some("description")}
227                 buttonTitle={None}
228                 maxLength={None}
229                 onContentChange={this.handleCommunityDescriptionChange}
230                 allLanguages={[]}
231                 siteLanguages={[]}
232               />
233             </div>
234           </div>
235
236           {this.props.enableNsfw && (
237             <div className="form-group row">
238               <legend className="col-form-label col-sm-2 pt-0">
239                 {i18n.t("nsfw")}
240               </legend>
241               <div className="col-10">
242                 <div className="form-check">
243                   <input
244                     className="form-check-input position-static"
245                     id="community-nsfw"
246                     type="checkbox"
247                     checked={toUndefined(this.state.communityForm.nsfw)}
248                     onChange={linkEvent(this, this.handleCommunityNsfwChange)}
249                   />
250                 </div>
251               </div>
252             </div>
253           )}
254           <div className="form-group row">
255             <legend className="col-form-label col-6 pt-0">
256               {i18n.t("only_mods_can_post_in_community")}
257             </legend>
258             <div className="col-6">
259               <div className="form-check">
260                 <input
261                   className="form-check-input position-static"
262                   id="community-only-mods-can-post"
263                   type="checkbox"
264                   checked={toUndefined(
265                     this.state.communityForm.posting_restricted_to_mods
266                   )}
267                   onChange={linkEvent(
268                     this,
269                     this.handleCommunityPostingRestrictedToMods
270                   )}
271                 />
272               </div>
273             </div>
274           </div>
275           <LanguageSelect
276             allLanguages={this.props.allLanguages}
277             siteLanguages={this.props.siteLanguages}
278             showSite
279             selectedLanguageIds={this.state.communityForm.discussion_languages}
280             multiple={true}
281             onChange={this.handleDiscussionLanguageChange}
282           />
283           <div className="form-group row">
284             <div className="col-12">
285               <button
286                 type="submit"
287                 className="btn btn-secondary mr-2"
288                 disabled={this.state.loading}
289               >
290                 {this.state.loading ? (
291                   <Spinner />
292                 ) : this.props.community_view.isSome() ? (
293                   capitalizeFirstLetter(i18n.t("save"))
294                 ) : (
295                   capitalizeFirstLetter(i18n.t("create"))
296                 )}
297               </button>
298               {this.props.community_view.isSome() && (
299                 <button
300                   type="button"
301                   className="btn btn-secondary"
302                   onClick={linkEvent(this, this.handleCancel)}
303                 >
304                   {i18n.t("cancel")}
305                 </button>
306               )}
307             </div>
308           </div>
309         </form>
310       </>
311     );
312   }
313
314   handleCreateCommunitySubmit(i: CommunityForm, event: any) {
315     event.preventDefault();
316     i.setState({ loading: true });
317     let cForm = i.state.communityForm;
318     cForm.auth = auth().unwrap();
319
320     i.props.community_view.match({
321       some: cv => {
322         let form = new EditCommunity({
323           community_id: cv.community.id,
324           title: Some(cForm.title),
325           description: cForm.description,
326           icon: cForm.icon,
327           banner: cForm.banner,
328           nsfw: cForm.nsfw,
329           posting_restricted_to_mods: cForm.posting_restricted_to_mods,
330           discussion_languages: cForm.discussion_languages,
331           auth: cForm.auth,
332         });
333
334         WebSocketService.Instance.send(wsClient.editCommunity(form));
335       },
336       none: () => {
337         WebSocketService.Instance.send(
338           wsClient.createCommunity(i.state.communityForm)
339         );
340       },
341     });
342     i.setState(i.state);
343   }
344
345   handleCommunityNameChange(i: CommunityForm, event: any) {
346     i.state.communityForm.name = event.target.value;
347     i.setState(i.state);
348   }
349
350   handleCommunityTitleChange(i: CommunityForm, event: any) {
351     i.state.communityForm.title = event.target.value;
352     i.setState(i.state);
353   }
354
355   handleCommunityDescriptionChange(val: string) {
356     this.setState(s => ((s.communityForm.description = Some(val)), s));
357   }
358
359   handleCommunityNsfwChange(i: CommunityForm, event: any) {
360     i.state.communityForm.nsfw = Some(event.target.checked);
361     i.setState(i.state);
362   }
363
364   handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
365     i.state.communityForm.posting_restricted_to_mods = Some(
366       event.target.checked
367     );
368     i.setState(i.state);
369   }
370
371   handleCancel(i: CommunityForm) {
372     i.props.onCancel();
373   }
374
375   handleIconUpload(url: string) {
376     this.setState(s => ((s.communityForm.icon = Some(url)), s));
377   }
378
379   handleIconRemove() {
380     this.setState(s => ((s.communityForm.icon = Some("")), s));
381   }
382
383   handleBannerUpload(url: string) {
384     this.setState(s => ((s.communityForm.banner = Some(url)), s));
385   }
386
387   handleBannerRemove() {
388     this.setState(s => ((s.communityForm.banner = Some("")), s));
389   }
390
391   handleDiscussionLanguageChange(val: number[]) {
392     this.setState(s => ((s.communityForm.discussion_languages = Some(val)), s));
393   }
394
395   parseMessage(msg: any) {
396     let op = wsUserOp(msg);
397     console.log(msg);
398     if (msg.error) {
399       // Errors handled by top level pages
400       // toast(i18n.t(msg.error), "danger");
401       this.setState({ loading: false });
402       return;
403     } else if (op == UserOperation.CreateCommunity) {
404       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
405       this.props.onCreate(data.community_view);
406
407       // Update myUserInfo
408       let community = data.community_view.community;
409
410       UserService.Instance.myUserInfo.match({
411         some: mui => {
412           let person = mui.local_user_view.person;
413           mui.follows.push({
414             community,
415             follower: person,
416           });
417           mui.moderates.push({
418             community,
419             moderator: person,
420           });
421         },
422         none: void 0,
423       });
424     } else if (op == UserOperation.EditCommunity) {
425       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
426       this.setState({ loading: false });
427       this.props.onEdit(data.community_view);
428       let community = data.community_view.community;
429
430       UserService.Instance.myUserInfo.match({
431         some: mui => {
432           let followFound = mui.follows.findIndex(
433             f => f.community.id == community.id
434           );
435           if (followFound) {
436             mui.follows[followFound].community = community;
437           }
438
439           let moderatesFound = mui.moderates.findIndex(
440             f => f.community.id == community.id
441           );
442           if (moderatesFound) {
443             mui.moderates[moderatesFound].community = community;
444           }
445         },
446         none: void 0,
447       });
448     }
449   }
450 }