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