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