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