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