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