]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/signup.tsx
Upgrade inferno v8.0.0 try2 (#790)
[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     return this.state.siteRes.site_view.match({
115       some: siteView => `${this.titleName(siteView)} - ${siteView.site.name}`,
116       none: "",
117     });
118   }
119
120   titleName(siteView: SiteView): string {
121     return i18n.t(siteView.site.private_instance ? "apply_to_join" : "sign_up");
122   }
123
124   get isLemmyMl(): boolean {
125     return isBrowser() && window.location.hostname == "lemmy.ml";
126   }
127
128   render() {
129     return (
130       <div className="container">
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     return this.state.siteRes.site_view.match({
148       some: siteView => (
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.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.site.require_email_verification}
203                 minLength={3}
204               />
205               {!siteView.site.require_email_verification &&
206                 !this.state.registerForm.email
207                   .map(validEmail)
208                   .unwrapOr(true) && (
209                   <div className="mt-2 mb-0 alert alert-warning" role="alert">
210                     <Icon icon="alert-triangle" classes="icon-inline mr-2" />
211                     {i18n.t("no_password_reset")}
212                   </div>
213                 )}
214             </div>
215           </div>
216
217           <div className="form-group row">
218             <label
219               className="col-sm-2 col-form-label"
220               htmlFor="register-password"
221             >
222               {i18n.t("password")}
223             </label>
224             <div className="col-sm-10">
225               <input
226                 type="password"
227                 id="register-password"
228                 value={this.state.registerForm.password}
229                 autoComplete="new-password"
230                 onInput={linkEvent(this, this.handleRegisterPasswordChange)}
231                 minLength={10}
232                 maxLength={60}
233                 className="form-control"
234                 required
235               />
236               {this.state.registerForm.password && (
237                 <div className={this.passwordColorClass}>
238                   {i18n.t(this.passwordStrength as I18nKeys)}
239                 </div>
240               )}
241             </div>
242           </div>
243
244           <div className="form-group row">
245             <label
246               className="col-sm-2 col-form-label"
247               htmlFor="register-verify-password"
248             >
249               {i18n.t("verify_password")}
250             </label>
251             <div className="col-sm-10">
252               <input
253                 type="password"
254                 id="register-verify-password"
255                 value={this.state.registerForm.password_verify}
256                 autoComplete="new-password"
257                 onInput={linkEvent(
258                   this,
259                   this.handleRegisterPasswordVerifyChange
260                 )}
261                 maxLength={60}
262                 className="form-control"
263                 required
264               />
265             </div>
266           </div>
267
268           {siteView.site.require_application && (
269             <>
270               <div className="form-group row">
271                 <div className="offset-sm-2 col-sm-10">
272                   <div className="mt-2 alert alert-warning" role="alert">
273                     <Icon icon="alert-triangle" classes="icon-inline mr-2" />
274                     {i18n.t("fill_out_application")}
275                   </div>
276                   {siteView.site.application_question.match({
277                     some: question => (
278                       <div
279                         className="md-div"
280                         dangerouslySetInnerHTML={mdToHtml(question)}
281                       />
282                     ),
283                     none: <></>,
284                   })}
285                 </div>
286               </div>
287
288               <div className="form-group row">
289                 <label
290                   className="col-sm-2 col-form-label"
291                   htmlFor="application_answer"
292                 >
293                   {i18n.t("answer")}
294                 </label>
295                 <div className="col-sm-10">
296                   <MarkdownTextArea
297                     initialContent={None}
298                     placeholder={None}
299                     buttonTitle={None}
300                     maxLength={None}
301                     onContentChange={this.handleAnswerChange}
302                     hideNavigationWarnings
303                   />
304                 </div>
305               </div>
306             </>
307           )}
308
309           {this.state.captcha.isSome() && (
310             <div className="form-group row">
311               <label className="col-sm-2" htmlFor="register-captcha">
312                 <span className="mr-2">{i18n.t("enter_code")}</span>
313                 <button
314                   type="button"
315                   className="btn btn-secondary"
316                   onClick={linkEvent(this, this.handleRegenCaptcha)}
317                   aria-label={i18n.t("captcha")}
318                 >
319                   <Icon icon="refresh-cw" classes="icon-refresh-cw" />
320                 </button>
321               </label>
322               {this.showCaptcha()}
323               <div className="col-sm-6">
324                 <input
325                   type="text"
326                   className="form-control"
327                   id="register-captcha"
328                   value={toUndefined(this.state.registerForm.captcha_answer)}
329                   onInput={linkEvent(
330                     this,
331                     this.handleRegisterCaptchaAnswerChange
332                   )}
333                   required
334                 />
335               </div>
336             </div>
337           )}
338           {siteView.site.enable_nsfw && (
339             <div className="form-group row">
340               <div className="col-sm-10">
341                 <div className="form-check">
342                   <input
343                     className="form-check-input"
344                     id="register-show-nsfw"
345                     type="checkbox"
346                     checked={this.state.registerForm.show_nsfw}
347                     onChange={linkEvent(
348                       this,
349                       this.handleRegisterShowNsfwChange
350                     )}
351                   />
352                   <label
353                     className="form-check-label"
354                     htmlFor="register-show-nsfw"
355                   >
356                     {i18n.t("show_nsfw")}
357                   </label>
358                 </div>
359               </div>
360             </div>
361           )}
362           <input
363             tabIndex={-1}
364             autoComplete="false"
365             name="a_password"
366             type="text"
367             className="form-control honeypot"
368             id="register-honey"
369             value={toUndefined(this.state.registerForm.honeypot)}
370             onInput={linkEvent(this, this.handleHoneyPotChange)}
371           />
372           <div className="form-group row">
373             <div className="col-sm-10">
374               <button type="submit" className="btn btn-secondary">
375                 {this.state.registerLoading ? (
376                   <Spinner />
377                 ) : (
378                   this.titleName(siteView)
379                 )}
380               </button>
381             </div>
382           </div>
383         </form>
384       ),
385       none: <></>,
386     });
387   }
388
389   showCaptcha() {
390     return this.state.captcha.match({
391       some: captcha => (
392         <div className="col-sm-4">
393           {captcha.ok.match({
394             some: res => (
395               <>
396                 <img
397                   className="rounded-top img-fluid"
398                   src={this.captchaPngSrc(res)}
399                   style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
400                   alt={i18n.t("captcha")}
401                 />
402                 {res.wav.isSome() && (
403                   <button
404                     className="rounded-bottom btn btn-sm btn-secondary btn-block"
405                     style="border-top-right-radius: 0; border-top-left-radius: 0;"
406                     title={i18n.t("play_captcha_audio")}
407                     onClick={linkEvent(this, this.handleCaptchaPlay)}
408                     type="button"
409                     disabled={this.state.captchaPlaying}
410                   >
411                     <Icon icon="play" classes="icon-play" />
412                   </button>
413                 )}
414               </>
415             ),
416             none: <></>,
417           })}
418         </div>
419       ),
420       none: <></>,
421     });
422   }
423
424   get passwordStrength() {
425     return passwordStrength(
426       this.state.registerForm.password,
427       passwordStrengthOptions
428     ).value;
429   }
430
431   get passwordColorClass(): string {
432     let strength = this.passwordStrength;
433
434     if (["weak", "medium"].includes(strength)) {
435       return "text-warning";
436     } else if (strength == "strong") {
437       return "text-success";
438     } else {
439       return "text-danger";
440     }
441   }
442
443   handleRegisterSubmit(i: Signup, event: any) {
444     event.preventDefault();
445     i.setState({ registerLoading: true });
446     WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
447   }
448
449   handleRegisterUsernameChange(i: Signup, event: any) {
450     i.state.registerForm.username = event.target.value;
451     i.setState(i.state);
452   }
453
454   handleRegisterEmailChange(i: Signup, event: any) {
455     i.state.registerForm.email = Some(event.target.value);
456     if (i.state.registerForm.email.unwrap() == "") {
457       i.state.registerForm.email = None;
458     }
459     i.setState(i.state);
460   }
461
462   handleRegisterPasswordChange(i: Signup, event: any) {
463     i.state.registerForm.password = event.target.value;
464     i.setState(i.state);
465   }
466
467   handleRegisterPasswordVerifyChange(i: Signup, event: any) {
468     i.state.registerForm.password_verify = event.target.value;
469     i.setState(i.state);
470   }
471
472   handleRegisterShowNsfwChange(i: Signup, event: any) {
473     i.state.registerForm.show_nsfw = event.target.checked;
474     i.setState(i.state);
475   }
476
477   handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
478     i.state.registerForm.captcha_answer = Some(event.target.value);
479     i.setState(i.state);
480   }
481
482   handleAnswerChange(val: string) {
483     this.setState(s => ((s.registerForm.answer = Some(val)), s));
484   }
485
486   handleHoneyPotChange(i: Signup, event: any) {
487     i.state.registerForm.honeypot = Some(event.target.value);
488     i.setState(i.state);
489   }
490
491   handleRegenCaptcha(i: Signup) {
492     i.audio = null;
493     i.setState({ captchaPlaying: false });
494     WebSocketService.Instance.send(wsClient.getCaptcha());
495   }
496
497   handleCaptchaPlay(i: Signup) {
498     // This was a bad bug, it should only build the new audio on a new file.
499     // Replays would stop prematurely if this was rebuilt every time.
500     i.state.captcha.match({
501       some: captcha =>
502         captcha.ok.match({
503           some: res => {
504             if (i.audio == null) {
505               let base64 = `data:audio/wav;base64,${res.wav}`;
506               i.audio = new Audio(base64);
507             }
508
509             i.audio.play();
510
511             i.setState({ captchaPlaying: true });
512
513             i.audio.addEventListener("ended", () => {
514               i.audio.currentTime = 0;
515               i.setState({ captchaPlaying: false });
516             });
517           },
518           none: void 0,
519         }),
520       none: void 0,
521     });
522   }
523
524   captchaPngSrc(captcha: CaptchaResponse) {
525     return `data:image/png;base64,${captcha.png}`;
526   }
527
528   parseMessage(msg: any) {
529     let op = wsUserOp(msg);
530     console.log(msg);
531     if (msg.error) {
532       toast(i18n.t(msg.error), "danger");
533       this.setState(this.emptyState);
534       this.setState(s => ((s.registerForm.captcha_answer = undefined), s));
535       // Refetch another captcha
536       // WebSocketService.Instance.send(wsClient.getCaptcha());
537       return;
538     } else {
539       if (op == UserOperation.Register) {
540         let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
541         this.setState(this.emptyState);
542         // Only log them in if a jwt was set
543         if (data.jwt.isSome()) {
544           UserService.Instance.login(data);
545           this.props.history.push("/communities");
546         } else {
547           if (data.verify_email_sent) {
548             toast(i18n.t("verify_email_sent"));
549           }
550           if (data.registration_created) {
551             toast(i18n.t("registration_application_sent"));
552           }
553           this.props.history.push("/");
554         }
555       } else if (op == UserOperation.GetCaptcha) {
556         let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse);
557         data.ok.match({
558           some: res => {
559             this.setState({ captcha: Some(data) });
560             this.setState(
561               s => ((s.registerForm.captcha_uuid = Some(res.uuid)), s)
562             );
563           },
564           none: void 0,
565         });
566       } else if (op == UserOperation.PasswordReset) {
567         toast(i18n.t("reset_password_mail_sent"));
568       } else if (op == UserOperation.GetSite) {
569         let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
570         this.setState({ siteRes: data });
571       }
572     }
573   }
574 }