]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/signup.tsx
Adding Community Language fixes. #783 (#868)
[lemmy-ui.git] / src / shared / components / home / signup.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Options, passwordStrength } from "check-password-strength";
3 import { I18nKeys } from "i18next";
4 import { Component, linkEvent } from "inferno";
5 import { T } from "inferno-i18next-dess";
6 import {
7   CaptchaResponse,
8   GetCaptchaResponse,
9   GetSiteResponse,
10   LoginResponse,
11   Register,
12   SiteView,
13   toUndefined,
14   UserOperation,
15   wsJsonToRes,
16   wsUserOp,
17 } from "lemmy-js-client";
18 import { Subscription } from "rxjs";
19 import { i18n } from "../../i18next";
20 import { UserService, WebSocketService } from "../../services";
21 import {
22   isBrowser,
23   joinLemmyUrl,
24   mdToHtml,
25   setIsoData,
26   toast,
27   validEmail,
28   wsClient,
29   wsSubscribe,
30 } from "../../utils";
31 import { HtmlTags } from "../common/html-tags";
32 import { Icon, Spinner } from "../common/icon";
33 import { MarkdownTextArea } from "../common/markdown-textarea";
34
35 const passwordStrengthOptions: Options<string> = [
36   {
37     id: 0,
38     value: "very_weak",
39     minDiversity: 0,
40     minLength: 0,
41   },
42   {
43     id: 1,
44     value: "weak",
45     minDiversity: 2,
46     minLength: 10,
47   },
48   {
49     id: 2,
50     value: "medium",
51     minDiversity: 3,
52     minLength: 12,
53   },
54   {
55     id: 3,
56     value: "strong",
57     minDiversity: 4,
58     minLength: 14,
59   },
60 ];
61
62 interface State {
63   registerForm: Register;
64   registerLoading: boolean;
65   captcha: Option<GetCaptchaResponse>;
66   captchaPlaying: boolean;
67   siteRes: GetSiteResponse;
68 }
69
70 export class Signup extends Component<any, State> {
71   private isoData = setIsoData(this.context);
72   private subscription: Subscription;
73   private audio: HTMLAudioElement;
74
75   emptyState: State = {
76     registerForm: new Register({
77       username: undefined,
78       password: undefined,
79       password_verify: undefined,
80       show_nsfw: false,
81       captcha_uuid: None,
82       captcha_answer: None,
83       honeypot: None,
84       answer: None,
85       email: None,
86     }),
87     registerLoading: false,
88     captcha: None,
89     captchaPlaying: false,
90     siteRes: this.isoData.site_res,
91   };
92
93   constructor(props: any, context: any) {
94     super(props, context);
95
96     this.state = this.emptyState;
97     this.handleAnswerChange = this.handleAnswerChange.bind(this);
98
99     this.parseMessage = this.parseMessage.bind(this);
100     this.subscription = wsSubscribe(this.parseMessage);
101
102     if (isBrowser()) {
103       WebSocketService.Instance.send(wsClient.getCaptcha());
104     }
105   }
106
107   componentWillUnmount() {
108     if (isBrowser()) {
109       this.subscription.unsubscribe();
110     }
111   }
112
113   get documentTitle(): string {
114     let siteView = this.state.siteRes.site_view;
115     return `${this.titleName(siteView)} - ${siteView.site.name}`;
116   }
117
118   titleName(siteView: SiteView): string {
119     return i18n.t(
120       siteView.local_site.private_instance ? "apply_to_join" : "sign_up"
121     );
122   }
123
124   get isLemmyMl(): boolean {
125     return isBrowser() && window.location.hostname == "lemmy.ml";
126   }
127
128   render() {
129     return (
130       <div className="container-lg">
131         <HtmlTags
132           title={this.documentTitle}
133           path={this.context.router.route.match.url}
134           description={None}
135           image={None}
136         />
137         <div className="row">
138           <div className="col-12 col-lg-6 offset-lg-3">
139             {this.registerForm()}
140           </div>
141         </div>
142       </div>
143     );
144   }
145
146   registerForm() {
147     let siteView = this.state.siteRes.site_view;
148     return (
149       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
150         <h5>{this.titleName(siteView)}</h5>
151
152         {this.isLemmyMl && (
153           <div className="form-group row">
154             <div className="mt-2 mb-0 alert alert-warning" role="alert">
155               <T i18nKey="lemmy_ml_registration_message">
156                 #<a href={joinLemmyUrl}>#</a>
157               </T>
158             </div>
159           </div>
160         )}
161
162         <div className="form-group row">
163           <label
164             className="col-sm-2 col-form-label"
165             htmlFor="register-username"
166           >
167             {i18n.t("username")}
168           </label>
169
170           <div className="col-sm-10">
171             <input
172               type="text"
173               id="register-username"
174               className="form-control"
175               value={this.state.registerForm.username}
176               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
177               required
178               minLength={3}
179               pattern="[a-zA-Z0-9_]+"
180               title={i18n.t("community_reqs")}
181             />
182           </div>
183         </div>
184
185         <div className="form-group row">
186           <label className="col-sm-2 col-form-label" htmlFor="register-email">
187             {i18n.t("email")}
188           </label>
189           <div className="col-sm-10">
190             <input
191               type="email"
192               id="register-email"
193               className="form-control"
194               placeholder={
195                 siteView.local_site.require_email_verification
196                   ? i18n.t("required")
197                   : i18n.t("optional")
198               }
199               value={toUndefined(this.state.registerForm.email)}
200               autoComplete="email"
201               onInput={linkEvent(this, this.handleRegisterEmailChange)}
202               required={siteView.local_site.require_email_verification}
203               minLength={3}
204             />
205             {!siteView.local_site.require_email_verification &&
206               !this.state.registerForm.email.map(validEmail).unwrapOr(true) && (
207                 <div className="mt-2 mb-0 alert alert-warning" role="alert">
208                   <Icon icon="alert-triangle" classes="icon-inline mr-2" />
209                   {i18n.t("no_password_reset")}
210                 </div>
211               )}
212           </div>
213         </div>
214
215         <div className="form-group row">
216           <label
217             className="col-sm-2 col-form-label"
218             htmlFor="register-password"
219           >
220             {i18n.t("password")}
221           </label>
222           <div className="col-sm-10">
223             <input
224               type="password"
225               id="register-password"
226               value={this.state.registerForm.password}
227               autoComplete="new-password"
228               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
229               minLength={10}
230               maxLength={60}
231               className="form-control"
232               required
233             />
234             {this.state.registerForm.password && (
235               <div className={this.passwordColorClass}>
236                 {i18n.t(this.passwordStrength as I18nKeys)}
237               </div>
238             )}
239           </div>
240         </div>
241
242         <div className="form-group row">
243           <label
244             className="col-sm-2 col-form-label"
245             htmlFor="register-verify-password"
246           >
247             {i18n.t("verify_password")}
248           </label>
249           <div className="col-sm-10">
250             <input
251               type="password"
252               id="register-verify-password"
253               value={this.state.registerForm.password_verify}
254               autoComplete="new-password"
255               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
256               maxLength={60}
257               className="form-control"
258               required
259             />
260           </div>
261         </div>
262
263         {siteView.local_site.require_application && (
264           <>
265             <div className="form-group row">
266               <div className="offset-sm-2 col-sm-10">
267                 <div className="mt-2 alert alert-warning" role="alert">
268                   <Icon icon="alert-triangle" classes="icon-inline mr-2" />
269                   {i18n.t("fill_out_application")}
270                 </div>
271                 {siteView.local_site.application_question.match({
272                   some: question => (
273                     <div
274                       className="md-div"
275                       dangerouslySetInnerHTML={mdToHtml(question)}
276                     />
277                   ),
278                   none: <></>,
279                 })}
280               </div>
281             </div>
282
283             <div className="form-group row">
284               <label
285                 className="col-sm-2 col-form-label"
286                 htmlFor="application_answer"
287               >
288                 {i18n.t("answer")}
289               </label>
290               <div className="col-sm-10">
291                 <MarkdownTextArea
292                   initialContent={None}
293                   initialLanguageId={None}
294                   placeholder={None}
295                   buttonTitle={None}
296                   maxLength={None}
297                   onContentChange={this.handleAnswerChange}
298                   hideNavigationWarnings
299                   allLanguages={[]}
300                   siteLanguages={[]}
301                 />
302               </div>
303             </div>
304           </>
305         )}
306
307         {this.state.captcha.isSome() && (
308           <div className="form-group row">
309             <label className="col-sm-2" htmlFor="register-captcha">
310               <span className="mr-2">{i18n.t("enter_code")}</span>
311               <button
312                 type="button"
313                 className="btn btn-secondary"
314                 onClick={linkEvent(this, this.handleRegenCaptcha)}
315                 aria-label={i18n.t("captcha")}
316               >
317                 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
318               </button>
319             </label>
320             {this.showCaptcha()}
321             <div className="col-sm-6">
322               <input
323                 type="text"
324                 className="form-control"
325                 id="register-captcha"
326                 value={toUndefined(this.state.registerForm.captcha_answer)}
327                 onInput={linkEvent(
328                   this,
329                   this.handleRegisterCaptchaAnswerChange
330                 )}
331                 required
332               />
333             </div>
334           </div>
335         )}
336         {siteView.local_site.enable_nsfw && (
337           <div className="form-group row">
338             <div className="col-sm-10">
339               <div className="form-check">
340                 <input
341                   className="form-check-input"
342                   id="register-show-nsfw"
343                   type="checkbox"
344                   checked={this.state.registerForm.show_nsfw}
345                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
346                 />
347                 <label
348                   className="form-check-label"
349                   htmlFor="register-show-nsfw"
350                 >
351                   {i18n.t("show_nsfw")}
352                 </label>
353               </div>
354             </div>
355           </div>
356         )}
357         <input
358           tabIndex={-1}
359           autoComplete="false"
360           name="a_password"
361           type="text"
362           className="form-control honeypot"
363           id="register-honey"
364           value={toUndefined(this.state.registerForm.honeypot)}
365           onInput={linkEvent(this, this.handleHoneyPotChange)}
366         />
367         <div className="form-group row">
368           <div className="col-sm-10">
369             <button type="submit" className="btn btn-secondary">
370               {this.state.registerLoading ? (
371                 <Spinner />
372               ) : (
373                 this.titleName(siteView)
374               )}
375             </button>
376           </div>
377         </div>
378       </form>
379     );
380   }
381
382   showCaptcha() {
383     return this.state.captcha.match({
384       some: captcha => (
385         <div className="col-sm-4">
386           {captcha.ok.match({
387             some: res => (
388               <>
389                 <img
390                   className="rounded-top img-fluid"
391                   src={this.captchaPngSrc(res)}
392                   style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
393                   alt={i18n.t("captcha")}
394                 />
395                 {res.wav.isSome() && (
396                   <button
397                     className="rounded-bottom btn btn-sm btn-secondary btn-block"
398                     style="border-top-right-radius: 0; border-top-left-radius: 0;"
399                     title={i18n.t("play_captcha_audio")}
400                     onClick={linkEvent(this, this.handleCaptchaPlay)}
401                     type="button"
402                     disabled={this.state.captchaPlaying}
403                   >
404                     <Icon icon="play" classes="icon-play" />
405                   </button>
406                 )}
407               </>
408             ),
409             none: <></>,
410           })}
411         </div>
412       ),
413       none: <></>,
414     });
415   }
416
417   get passwordStrength() {
418     return passwordStrength(
419       this.state.registerForm.password,
420       passwordStrengthOptions
421     ).value;
422   }
423
424   get passwordColorClass(): string {
425     let strength = this.passwordStrength;
426
427     if (["weak", "medium"].includes(strength)) {
428       return "text-warning";
429     } else if (strength == "strong") {
430       return "text-success";
431     } else {
432       return "text-danger";
433     }
434   }
435
436   handleRegisterSubmit(i: Signup, event: any) {
437     event.preventDefault();
438     i.setState({ registerLoading: true });
439     WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
440   }
441
442   handleRegisterUsernameChange(i: Signup, event: any) {
443     i.state.registerForm.username = event.target.value;
444     i.setState(i.state);
445   }
446
447   handleRegisterEmailChange(i: Signup, event: any) {
448     i.state.registerForm.email = Some(event.target.value);
449     if (i.state.registerForm.email.unwrap() == "") {
450       i.state.registerForm.email = None;
451     }
452     i.setState(i.state);
453   }
454
455   handleRegisterPasswordChange(i: Signup, event: any) {
456     i.state.registerForm.password = event.target.value;
457     i.setState(i.state);
458   }
459
460   handleRegisterPasswordVerifyChange(i: Signup, event: any) {
461     i.state.registerForm.password_verify = event.target.value;
462     i.setState(i.state);
463   }
464
465   handleRegisterShowNsfwChange(i: Signup, event: any) {
466     i.state.registerForm.show_nsfw = event.target.checked;
467     i.setState(i.state);
468   }
469
470   handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
471     i.state.registerForm.captcha_answer = Some(event.target.value);
472     i.setState(i.state);
473   }
474
475   handleAnswerChange(val: string) {
476     this.setState(s => ((s.registerForm.answer = Some(val)), s));
477   }
478
479   handleHoneyPotChange(i: Signup, event: any) {
480     i.state.registerForm.honeypot = Some(event.target.value);
481     i.setState(i.state);
482   }
483
484   handleRegenCaptcha(i: Signup) {
485     i.audio = null;
486     i.setState({ captchaPlaying: false });
487     WebSocketService.Instance.send(wsClient.getCaptcha());
488   }
489
490   handleCaptchaPlay(i: Signup) {
491     // This was a bad bug, it should only build the new audio on a new file.
492     // Replays would stop prematurely if this was rebuilt every time.
493     i.state.captcha.match({
494       some: captcha =>
495         captcha.ok.match({
496           some: res => {
497             if (i.audio == null) {
498               let base64 = `data:audio/wav;base64,${res.wav}`;
499               i.audio = new Audio(base64);
500             }
501
502             i.audio.play();
503
504             i.setState({ captchaPlaying: true });
505
506             i.audio.addEventListener("ended", () => {
507               i.audio.currentTime = 0;
508               i.setState({ captchaPlaying: false });
509             });
510           },
511           none: void 0,
512         }),
513       none: void 0,
514     });
515   }
516
517   captchaPngSrc(captcha: CaptchaResponse) {
518     return `data:image/png;base64,${captcha.png}`;
519   }
520
521   parseMessage(msg: any) {
522     let op = wsUserOp(msg);
523     console.log(msg);
524     if (msg.error) {
525       toast(i18n.t(msg.error), "danger");
526       this.setState(this.emptyState);
527       this.setState(s => ((s.registerForm.captcha_answer = undefined), s));
528       // Refetch another captcha
529       // WebSocketService.Instance.send(wsClient.getCaptcha());
530       return;
531     } else {
532       if (op == UserOperation.Register) {
533         let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
534         this.setState(this.emptyState);
535         // Only log them in if a jwt was set
536         if (data.jwt.isSome()) {
537           UserService.Instance.login(data);
538           this.props.history.push("/communities");
539           location.reload();
540         } else {
541           if (data.verify_email_sent) {
542             toast(i18n.t("verify_email_sent"));
543           }
544           if (data.registration_created) {
545             toast(i18n.t("registration_application_sent"));
546           }
547           this.props.history.push("/");
548         }
549       } else if (op == UserOperation.GetCaptcha) {
550         let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse);
551         data.ok.match({
552           some: res => {
553             this.setState({ captcha: Some(data) });
554             this.setState(
555               s => ((s.registerForm.captcha_uuid = Some(res.uuid)), s)
556             );
557           },
558           none: void 0,
559         });
560       } else if (op == UserOperation.PasswordReset) {
561         toast(i18n.t("reset_password_mail_sent"));
562       } else if (op == UserOperation.GetSite) {
563         let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
564         this.setState({ siteRes: data });
565       }
566     }
567   }
568 }