import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
PasswordResetForm,
GetSiteResponse,
+ GetCaptchaResponse,
WebSocketJsonResponse,
-} from '../interfaces';
+ Site,
+} from 'lemmy-js-client';
import { WebSocketService, UserService } from '../services';
import { wsJsonToRes, validEmail, toast } from '../utils';
import { i18n } from '../i18next';
-import { T } from 'inferno-i18next';
interface State {
loginForm: LoginForm;
registerForm: RegisterForm;
loginLoading: boolean;
registerLoading: boolean;
- enable_nsfw: boolean;
+ captcha: GetCaptchaResponse;
+ captchaPlaying: boolean;
+ site: Site;
}
export class Login extends Component<any, State> {
password_verify: undefined,
admin: false,
show_nsfw: false,
+ captcha_uuid: undefined,
+ captcha_answer: undefined,
},
loginLoading: false,
registerLoading: false,
- enable_nsfw: undefined,
+ captcha: undefined,
+ captchaPlaying: false,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
};
constructor(props: any, context: any) {
);
WebSocketService.Instance.getSite();
+ WebSocketService.Instance.getCaptcha();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ return `${i18n.t('login')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
render() {
return (
<div class="container">
+ <Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
<div class="col-12 col-lg-6">{this.registerForm()}</div>
return (
<div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
- <h5>Login</h5>
+ <h5>{i18n.t('login')}</h5>
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="email_or_username">#</T>
+ <label
+ class="col-sm-2 col-form-label"
+ htmlFor="login-email-or-username"
+ >
+ {i18n.t('email_or_username')}
</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
+ id="login-email-or-username"
value={this.state.loginForm.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)}
required
</div>
</div>
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="password">#</T>
+ <label class="col-sm-2 col-form-label" htmlFor="login-password">
+ {i18n.t('password')}
</label>
<div class="col-sm-10">
<input
type="password"
+ id="login-password"
value={this.state.loginForm.password}
onInput={linkEvent(this, this.handleLoginPasswordChange)}
class="form-control"
required
/>
- <button
- disabled={!validEmail(this.state.loginForm.username_or_email)}
- onClick={linkEvent(this, this.handlePasswordReset)}
- className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
- >
- <T i18nKey="forgot_password">#</T>
- </button>
+ {validEmail(this.state.loginForm.username_or_email) && (
+ <button
+ type="button"
+ onClick={linkEvent(this, this.handlePasswordReset)}
+ className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
+ >
+ {i18n.t('forgot_password')}
+ </button>
+ )}
</div>
</div>
<div class="form-group row">
</div>
);
}
+
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
- <h5>
- <T i18nKey="sign_up">#</T>
- </h5>
+ <h5>{i18n.t('sign_up')}</h5>
+
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="username">#</T>
+ <label class="col-sm-2 col-form-label" htmlFor="register-username">
+ {i18n.t('username')}
</label>
+
<div class="col-sm-10">
<input
type="text"
+ id="register-username"
class="form-control"
value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
/>
</div>
</div>
+
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="email">#</T>
+ <label class="col-sm-2 col-form-label" htmlFor="register-email">
+ {i18n.t('email')}
</label>
<div class="col-sm-10">
<input
type="email"
+ id="register-email"
class="form-control"
placeholder={i18n.t('optional')}
value={this.state.registerForm.email}
onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3}
/>
+ {!validEmail(this.state.registerForm.email) && (
+ <div class="mt-2 mb-0 alert alert-light" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ {i18n.t('no_password_reset')}
+ </div>
+ )}
</div>
</div>
+
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="password">#</T>
+ <label class="col-sm-2 col-form-label" htmlFor="register-password">
+ {i18n.t('password')}
</label>
<div class="col-sm-10">
<input
type="password"
+ id="register-password"
value={this.state.registerForm.password}
+ autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
class="form-control"
required
/>
</div>
</div>
+
<div class="form-group row">
- <label class="col-sm-2 col-form-label">
- <T i18nKey="verify_password">#</T>
+ <label
+ class="col-sm-2 col-form-label"
+ htmlFor="register-verify-password"
+ >
+ {i18n.t('verify_password')}
</label>
<div class="col-sm-10">
<input
type="password"
+ id="register-verify-password"
value={this.state.registerForm.password_verify}
+ autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
class="form-control"
required
/>
</div>
</div>
- {this.state.enable_nsfw && (
+
+ {this.state.captcha && (
+ <div class="form-group row">
+ <label class="col-sm-2" htmlFor="register-captcha">
+ <span class="mr-2">{i18n.t('enter_code')}</span>
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleRegenCaptcha)}
+ >
+ <svg class="icon icon-refresh-cw">
+ <use xlinkHref="#icon-refresh-cw"></use>
+ </svg>
+ </button>
+ </label>
+ {this.showCaptcha()}
+ <div class="col-sm-6">
+ <input
+ type="text"
+ class="form-control"
+ id="register-captcha"
+ value={this.state.registerForm.captcha_answer}
+ onInput={linkEvent(
+ this,
+ this.handleRegisterCaptchaAnswerChange
+ )}
+ required
+ />
+ </div>
+ </div>
+ )}
+ {this.state.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
+ id="register-show-nsfw"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
- <label class="form-check-label">
- <T i18nKey="show_nsfw">#</T>
+ <label class="form-check-label" htmlFor="register-show-nsfw">
+ {i18n.t('show_nsfw')}
</label>
</div>
</div>
);
}
+ showCaptcha() {
+ return (
+ <div class="col-sm-4">
+ {this.state.captcha.ok && (
+ <>
+ <img
+ class="rounded-top img-fluid"
+ src={this.captchaPngSrc()}
+ style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+ />
+ {this.state.captcha.ok.wav && (
+ <button
+ class="rounded-bottom btn btn-sm btn-secondary btn-block"
+ style="border-top-right-radius: 0; border-top-left-radius: 0;"
+ title={i18n.t('play_captcha_audio')}
+ onClick={linkEvent(this, this.handleCaptchaPlay)}
+ type="button"
+ disabled={this.state.captchaPlaying}
+ >
+ <svg class="icon icon-play">
+ <use xlinkHref="#icon-play"></use>
+ </svg>
+ </button>
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+
handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
i.state.loginLoading = true;
event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
-
WebSocketService.Instance.register(i.state.registerForm);
}
i.setState(i.state);
}
+ handleRegisterCaptchaAnswerChange(i: Login, event: any) {
+ i.state.registerForm.captcha_answer = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegenCaptcha(_i: Login, _event: any) {
+ event.preventDefault();
+ WebSocketService.Instance.getCaptcha();
+ }
+
handlePasswordReset(i: Login) {
event.preventDefault();
let resetForm: PasswordResetForm = {
WebSocketService.Instance.passwordReset(resetForm);
}
+ handleCaptchaPlay(i: Login) {
+ event.preventDefault();
+ let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
+ snd.play();
+ i.state.captchaPlaying = true;
+ i.setState(i.state);
+ snd.addEventListener('ended', () => {
+ snd.currentTime = 0;
+ i.state.captchaPlaying = false;
+ i.setState(this.state);
+ });
+ }
+
+ captchaPngSrc() {
+ return `data:image/png;base64,${this.state.captcha.ok.png}`;
+ }
+
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
this.state = this.emptyState;
+ this.state.registerForm.captcha_answer = undefined;
+ // Refetch another captcha
+ WebSocketService.Instance.getCaptcha();
this.setState(this.state);
return;
} else {
UserService.Instance.login(data);
WebSocketService.Instance.userJoin();
this.props.history.push('/communities');
+ } else if (res.op == UserOperation.GetCaptcha) {
+ let data = res.data as GetCaptchaResponse;
+ if (data.ok) {
+ this.state.captcha = data;
+ this.state.registerForm.captcha_uuid = data.ok.uuid;
+ this.setState(this.state);
+ }
} else if (res.op == UserOperation.PasswordReset) {
toast(i18n.t('reset_password_mail_sent'));
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
- this.state.enable_nsfw = data.site.enable_nsfw;
+ this.state.site = data.site;
this.setState(this.state);
- document.title = `${i18n.t('login')} - ${
- WebSocketService.Instance.site.name
- }`;
}
}
}