-Subproject commit 713ceed9c7ef84deaa222e68361e670e0763cd83
+Subproject commit a1a19aea1ad7d91195775a5ccea62ccc9076a2c7
<path d="M8.72046 10.6397L14.9999 7.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
+ <symbol id="icon-eye" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+ </symbol>
+ <symbol id="icon-eye-slash" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
+ </symbol>
</defs>
</svg>
--- /dev/null
+import { Options, passwordStrength } from "check-password-strength";
+import classNames from "classnames";
+import { NoOptionI18nKeys } from "i18next";
+import { Component, FormEventHandler, linkEvent } from "inferno";
+import { NavLink } from "inferno-router";
+import { I18NextService } from "../../services";
+import { Icon } from "./icon";
+
+interface PasswordInputProps {
+ id: string;
+ value?: string;
+ onInput: FormEventHandler<HTMLInputElement>;
+ className?: string;
+ showStrength?: boolean;
+ label?: string | null;
+ showForgotLink?: boolean;
+}
+
+interface PasswordInputState {
+ show: boolean;
+}
+
+const passwordStrengthOptions: Options<string> = [
+ {
+ id: 0,
+ value: "very_weak",
+ minDiversity: 0,
+ minLength: 0,
+ },
+ {
+ id: 1,
+ value: "weak",
+ minDiversity: 2,
+ minLength: 10,
+ },
+ {
+ id: 2,
+ value: "medium",
+ minDiversity: 3,
+ minLength: 12,
+ },
+ {
+ id: 3,
+ value: "strong",
+ minDiversity: 4,
+ minLength: 14,
+ },
+];
+
+function handleToggleShow(i: PasswordInput) {
+ i.setState(prev => ({
+ ...prev,
+ show: !prev.show,
+ }));
+}
+
+class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
+ state: PasswordInputState = {
+ show: false,
+ };
+
+ constructor(props: PasswordInputProps, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ const {
+ props: {
+ id,
+ value,
+ onInput,
+ className,
+ showStrength,
+ label,
+ showForgotLink,
+ },
+ state: { show },
+ } = this;
+
+ return (
+ <>
+ <div className={classNames("row", className)}>
+ {label && (
+ <label className="col-sm-2 col-form-label" htmlFor={id}>
+ {label}
+ </label>
+ )}
+ <div className={`col-sm-${label ? 10 : 12}`}>
+ <div className="input-group">
+ <input
+ type={show ? "text" : "password"}
+ className="form-control"
+ aria-describedby={id}
+ autoComplete="on"
+ onInput={onInput}
+ value={value}
+ required
+ maxLength={60}
+ minLength={10}
+ />
+ <button
+ className="btn btn-outline-dark"
+ type="button"
+ id={id}
+ onClick={linkEvent(this, handleToggleShow)}
+ aria-label={I18NextService.i18n.t(
+ `${show ? "show" : "hide"}_password`
+ )}
+ data-tippy-content={I18NextService.i18n.t(
+ `${show ? "show" : "hide"}_password`
+ )}
+ >
+ <Icon icon={`eye${show ? "-slash" : ""}`} inline />
+ </button>
+ </div>
+ {showStrength && value && (
+ <div className={this.passwordColorClass}>
+ {I18NextService.i18n.t(
+ this.passwordStrength as NoOptionI18nKeys
+ )}
+ </div>
+ )}
+ {showForgotLink && (
+ <NavLink
+ className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
+ to="/login_reset"
+ >
+ {I18NextService.i18n.t("forgot_password")}
+ </NavLink>
+ )}
+ </div>
+ </div>
+ </>
+ );
+ }
+
+ get passwordStrength(): string | undefined {
+ const password = this.props.value;
+ return password
+ ? passwordStrength(password, passwordStrengthOptions).value
+ : undefined;
+ }
+
+ get passwordColorClass(): string {
+ const strength = this.passwordStrength;
+
+ if (strength && ["weak", "medium"].includes(strength)) {
+ return "text-warning";
+ } else if (strength == "strong") {
+ return "text-success";
+ } else {
+ return "text-danger";
+ }
+ }
+}
+
+export default PasswordInput;
import { myAuth, setIsoData } from "@utils/app";
import { isBrowser } from "@utils/browser";
import { Component, linkEvent } from "inferno";
-import { NavLink } from "inferno-router";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
+import PasswordInput from "../common/password-input";
interface State {
loginRes: RequestState<LoginResponse>;
/>
</div>
</div>
- <div className="mb-3 row">
- <label className="col-sm-2 col-form-label" htmlFor="login-password">
- {I18NextService.i18n.t("password")}
- </label>
- <div className="col-sm-10">
- <input
- type="password"
- id="login-password"
- value={this.state.form.password}
- onInput={linkEvent(this, this.handleLoginPasswordChange)}
- className="form-control"
- autoComplete="current-password"
- required
- maxLength={60}
- />
- <NavLink
- className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
- to="/login_reset"
- >
- {I18NextService.i18n.t("forgot_password")}
- </NavLink>
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="login-password"
+ value={this.state.form.password}
+ onInput={linkEvent(this, this.handleLoginPasswordChange)}
+ label={I18NextService.i18n.t("password")}
+ showForgotLink
+ />
</div>
{this.state.showTotp && (
<div className="mb-3 row">
import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
import { Spinner } from "../common/icon";
+import PasswordInput from "../common/password-input";
import { SiteForm } from "./site-form";
interface State {
/>
</div>
</div>
- <div className="mb-3 row">
- <label className="col-sm-2 col-form-label" htmlFor="password">
- {I18NextService.i18n.t("password")}
- </label>
- <div className="col-sm-10">
- <input
- type="password"
- id="password"
- value={this.state.form.password}
- onInput={linkEvent(this, this.handleRegisterPasswordChange)}
- className="form-control"
- required
- autoComplete="new-password"
- minLength={10}
- maxLength={60}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="password"
+ value={this.state.form.password}
+ onInput={linkEvent(this, this.handleRegisterPasswordChange)}
+ label={I18NextService.i18n.t("password")}
+ />
</div>
- <div className="mb-3 row">
- <label className="col-sm-2 col-form-label" htmlFor="verify-password">
- {I18NextService.i18n.t("verify_password")}
- </label>
- <div className="col-sm-10">
- <input
- type="password"
- id="verify-password"
- value={this.state.form.password_verify}
- onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
- className="form-control"
- required
- autoComplete="new-password"
- minLength={10}
- maxLength={60}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="verify-password"
+ value={this.state.form.password_verify}
+ onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
+ label={I18NextService.i18n.t("verify_password")}
+ />
</div>
<div className="mb-3 row">
<div className="col-sm-10">
import { myAuth, setIsoData } from "@utils/app";
import { isBrowser } from "@utils/browser";
import { validEmail } from "@utils/helpers";
-import { Options, passwordStrength } from "check-password-strength";
-import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
-
-const passwordStrengthOptions: Options<string> = [
- {
- id: 0,
- value: "very_weak",
- minDiversity: 0,
- minLength: 0,
- },
- {
- id: 1,
- value: "weak",
- minDiversity: 2,
- minLength: 10,
- },
- {
- id: 2,
- value: "medium",
- minDiversity: 3,
- minLength: 12,
- },
- {
- id: 3,
- value: "strong",
- minDiversity: 4,
- minLength: 14,
- },
-];
+import PasswordInput from "../common/password-input";
interface State {
registerRes: RequestState<LoginResponse>;
</div>
</div>
- <div className="mb-3 row">
- <label
- className="col-sm-2 col-form-label"
- htmlFor="register-password"
- >
- {I18NextService.i18n.t("password")}
- </label>
- <div className="col-sm-10">
- <input
- type="password"
- id="register-password"
- value={this.state.form.password}
- autoComplete="new-password"
- onInput={linkEvent(this, this.handleRegisterPasswordChange)}
- minLength={10}
- maxLength={60}
- className="form-control"
- required
- />
- {this.state.form.password && (
- <div className={this.passwordColorClass}>
- {I18NextService.i18n.t(
- this.passwordStrength as NoOptionI18nKeys
- )}
- </div>
- )}
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="register-password"
+ value={this.state.form.password}
+ onInput={linkEvent(this, this.handleRegisterPasswordChange)}
+ showStrength
+ label={I18NextService.i18n.t("password")}
+ />
</div>
- <div className="mb-3 row">
- <label
- className="col-sm-2 col-form-label"
- htmlFor="register-verify-password"
- >
- {I18NextService.i18n.t("verify_password")}
- </label>
- <div className="col-sm-10">
- <input
- type="password"
- id="register-verify-password"
- value={this.state.form.password_verify}
- autoComplete="new-password"
- onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
- maxLength={60}
- className="form-control"
- required
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="register-verify-password"
+ value={this.state.form.password_verify}
+ onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
+ label={I18NextService.i18n.t("verify_password")}
+ />
</div>
- {siteView.local_site.registration_mode == "RequireApplication" && (
+ {siteView.local_site.registration_mode === "RequireApplication" && (
<>
<div className="mb-3 row">
<div className="offset-sm-2 col-sm-10">
);
}
- get passwordStrength(): string | undefined {
- const password = this.state.form.password;
- return password
- ? passwordStrength(password, passwordStrengthOptions).value
- : undefined;
- }
-
- get passwordColorClass(): string {
- const strength = this.passwordStrength;
-
- if (strength && ["weak", "medium"].includes(strength)) {
- return "text-warning";
- } else if (strength == "strong") {
- return "text-success";
- } else {
- return "text-danger";
- }
- }
-
async handleRegisterSubmit(i: Signup, event: any) {
event.preventDefault();
const {
import { RequestState } from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
+import PasswordInput from "../common/password-input";
interface State {
passwordChangeRes: RequestState<LoginResponse>;
passwordChangeForm() {
return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
- <div className="mb-3 row">
- <label className="col-sm-2 col-form-label" htmlFor="new-password">
- {I18NextService.i18n.t("new_password")}
- </label>
- <div className="col-sm-10">
- <input
- id="new-password"
- type="password"
- value={this.state.form.password}
- onInput={linkEvent(this, this.handlePasswordChange)}
- className="form-control"
- required
- maxLength={60}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="new-password"
+ value={this.state.form.password}
+ onInput={linkEvent(this, this.handlePasswordChange)}
+ showStrength
+ label={I18NextService.i18n.t("new_password")}
+ />
</div>
- <div className="mb-3 row">
- <label className="col-sm-2 col-form-label" htmlFor="verify-password">
- {I18NextService.i18n.t("verify_password")}
- </label>
- <div className="col-sm-10">
- <input
- id="verify-password"
- type="password"
- value={this.state.form.password_verify}
- onInput={linkEvent(this, this.handleVerifyPasswordChange)}
- className="form-control"
- required
- maxLength={60}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="password"
+ value={this.state.form.password_verify}
+ onInput={linkEvent(this, this.handleVerifyPasswordChange)}
+ label={I18NextService.i18n.t("verify_password")}
+ />
</div>
<div className="mb-3 row">
<div className="col-sm-10">
import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
+import PasswordInput from "../common/password-input";
import { SearchableSelect } from "../common/searchable-select";
import { SortSelect } from "../common/sort-select";
import Tabs from "../common/tabs";
<>
<h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
- <div className="mb-3 row">
- <label className="col-sm-5 col-form-label" htmlFor="user-password">
- {I18NextService.i18n.t("new_password")}
- </label>
- <div className="col-sm-7">
- <input
- type="password"
- id="user-password"
- className="form-control"
- value={this.state.changePasswordForm.new_password}
- autoComplete="new-password"
- maxLength={60}
- onInput={linkEvent(this, this.handleNewPasswordChange)}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="new-password"
+ value={this.state.changePasswordForm.new_password}
+ onInput={linkEvent(this, this.handleNewPasswordChange)}
+ showStrength
+ label={I18NextService.i18n.t("new_password")}
+ />
</div>
- <div className="mb-3 row">
- <label
- className="col-sm-5 col-form-label"
- htmlFor="user-verify-password"
- >
- {I18NextService.i18n.t("verify_password")}
- </label>
- <div className="col-sm-7">
- <input
- type="password"
- id="user-verify-password"
- className="form-control"
- value={this.state.changePasswordForm.new_password_verify}
- autoComplete="new-password"
- maxLength={60}
- onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="verify-new-password"
+ value={this.state.changePasswordForm.new_password_verify}
+ onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
+ label={I18NextService.i18n.t("verify_password")}
+ />
</div>
- <div className="mb-3 row">
- <label
- className="col-sm-5 col-form-label"
- htmlFor="user-old-password"
- >
- {I18NextService.i18n.t("old_password")}
- </label>
- <div className="col-sm-7">
- <input
- type="password"
- id="user-old-password"
- className="form-control"
- value={this.state.changePasswordForm.old_password}
- autoComplete="new-password"
- maxLength={60}
- onInput={linkEvent(this, this.handleOldPasswordChange)}
- />
- </div>
+ <div className="mb-3">
+ <PasswordInput
+ id="user-old-password"
+ value={this.state.changePasswordForm.old_password}
+ onInput={linkEvent(this, this.handleOldPasswordChange)}
+ label={I18NextService.i18n.t("old_password")}
+ />
</div>
<div className="input-group mb-3">
<button
</button>
</div>
<hr />
- <div className="input-group mb-3">
+ <form
+ className="mb-3"
+ onSubmit={linkEvent(this, this.handleDeleteAccount)}
+ >
<button
+ type="button"
className="btn d-block btn-danger"
onClick={linkEvent(
this,
</button>
{this.state.deleteAccountShowConfirm && (
<>
- <div className="my-2 alert alert-danger" role="alert">
+ <label
+ className="my-2 alert alert-danger d-block"
+ role="alert"
+ htmlFor="password-delete-account"
+ >
{I18NextService.i18n.t("delete_account_confirm")}
- </div>
- <input
- type="password"
+ </label>
+ <PasswordInput
+ id="password-delete-account"
value={this.state.deleteAccountForm.password}
- autoComplete="new-password"
- maxLength={60}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
- className="form-control my-2"
+ className="my-2"
/>
<button
+ type="submit"
className="btn btn-danger me-4"
disabled={!this.state.deleteAccountForm.password}
- onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountRes.state === "loading" ? (
<Spinner />
</button>
<button
className="btn btn-secondary"
+ type="button"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
</button>
</>
)}
- </div>
+ </form>
</form>
</>
);
i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
}
- async handleDeleteAccount(i: Settings) {
+ async handleDeleteAccount(i: Settings, event: Event) {
+ event.preventDefault();
const password = i.state.deleteAccountForm.password;
if (password) {
i.setState({ deleteAccountRes: { state: "loading" } });