chore: ttag setup and authentication flow localization (#2987)
* chore: add ttag dependency * chore: wrap strings for authentication flows * chore: fix lint errors
This commit is contained in:
committed by
GitHub
parent
92b7be4221
commit
7f3d2d0c72
@@ -120,6 +120,7 @@
|
||||
"comlink": "^4.4.1",
|
||||
"fast-diff": "^1.3.0",
|
||||
"lexical": "0.38.1",
|
||||
"ttag": "^1.8.12",
|
||||
"unicode-script": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import { isErrorResponse } from '@standardnotes/snjs'
|
||||
import MergeLocalDataCheckbox from './MergeLocalDataCheckbox'
|
||||
import ConfirmNoMergeDialog from './ConfirmNoMergeDialog'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
@@ -165,9 +166,9 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
const confirmPasswordForm = (
|
||||
<>
|
||||
<div className="mb-3 px-3 text-sm">
|
||||
Because your notes are encrypted using your password,{' '}
|
||||
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
|
||||
password, you will permanently lose access to your data.
|
||||
{c('Info').jt`Because your notes are encrypted using your password, ${(
|
||||
<span className="text-danger">Standard Notes does not have a password reset option</span>
|
||||
)}. If you forget your password, you will permanently lose access to your data.`}
|
||||
</div>
|
||||
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
|
||||
{!isRegistering && (
|
||||
@@ -177,7 +178,7 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
left={[<Icon type="password" className="text-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Confirm password"
|
||||
placeholder={c('Label').t`Confirm password`}
|
||||
ref={passwordInputRef}
|
||||
value={confirmPassword}
|
||||
/>
|
||||
@@ -187,13 +188,13 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
primary
|
||||
fullWidth
|
||||
className="mb-3 mt-1"
|
||||
label={isRegistering ? 'Creating account...' : 'Create account & sign in'}
|
||||
label={isRegistering ? c('Action').t`Creating account...` : c('Action').t`Create account & sign in`}
|
||||
onClick={handleConfirmFormSubmit}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
label={c('Option').t`Stay signed in`}
|
||||
checked={!isEphemeral}
|
||||
onChange={handleEphemeralChange}
|
||||
disabled={isRegistering}
|
||||
@@ -215,13 +216,15 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
title={c('Action').t`Go back`}
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
|
||||
<div className="text-base font-bold">
|
||||
{captchaURL ? c('Title').t`Human verification` : c('Title').t`Confirm password`}
|
||||
</div>
|
||||
</div>
|
||||
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
|
||||
{showNoMergeConfirmation && (
|
||||
|
||||
@@ -16,6 +16,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import AdvancedOptions from './AdvancedOptions'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
@@ -101,12 +102,12 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
title={c('Action').t`Go back`}
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={handleClose}
|
||||
focusable={true}
|
||||
/>
|
||||
<div className="text-base font-bold">Create account</div>
|
||||
<div className="text-base font-bold">{c('Title').t`Create account`}</div>
|
||||
</div>
|
||||
<form onSubmit={handleRegisterFormSubmit} className="mb-1 px-3">
|
||||
<DecoratedInput
|
||||
@@ -115,7 +116,7 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
|
||||
left={[<Icon type="email" className="text-neutral" />]}
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Email"
|
||||
placeholder={c('Label').t`Email`}
|
||||
ref={emailInputRef}
|
||||
type="email"
|
||||
value={email}
|
||||
@@ -126,11 +127,17 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
|
||||
left={[<Icon type="password" className="text-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Password"
|
||||
placeholder={c('Label').t`Password`}
|
||||
ref={passwordInputRef}
|
||||
value={password}
|
||||
/>
|
||||
<Button className="mt-1" label="Next" primary onClick={handleRegisterFormSubmit} fullWidth={true} />
|
||||
<Button
|
||||
className="mt-1"
|
||||
label={c('Action').t`Next`}
|
||||
primary
|
||||
onClick={handleRegisterFormSubmit}
|
||||
fullWidth={true}
|
||||
/>
|
||||
</form>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<AdvancedOptions onPrivateUsernameModeChange={onPrivateUsernameChange} />
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useApplication } from '../ApplicationProvider'
|
||||
import MenuSection from '../Menu/MenuSection'
|
||||
import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: WebApplicationGroup
|
||||
@@ -138,12 +139,12 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
||||
<>
|
||||
<div className="mb-1 px-4 md:px-3">
|
||||
<div className="mb-3 text-base text-foreground lg:text-sm">
|
||||
You’re offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end
|
||||
encryption.
|
||||
{c('Info')
|
||||
.t`You’re offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.`}
|
||||
</div>
|
||||
<div className="flex items-center text-passive-1">
|
||||
<Icon type="cloud-off" className={`mr-2 ${MenuItemIconSize}`} />
|
||||
<span className="text-lg font-semibold lg:text-sm">Offline</span>
|
||||
<span className="text-lg font-semibold lg:text-sm">{c('Status').t`Offline`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -166,11 +167,11 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
||||
<>
|
||||
<MenuItem onClick={activateRegisterPane}>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
{c('Action').t`Create free account`}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={activateSignInPane}>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
{c('Action').t`Sign in`}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useApplication } from '../ApplicationProvider'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import MergeLocalDataCheckbox from './MergeLocalDataCheckbox'
|
||||
import ConfirmNoMergeDialog from './ConfirmNoMergeDialog'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
@@ -252,7 +253,7 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
|
||||
left={[<Icon type="email" className="text-neutral" />]}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
placeholder={c('Label').t`Email`}
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onFocus={resetInvalid}
|
||||
@@ -268,14 +269,14 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
onChange={handlePasswordChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Password"
|
||||
placeholder={c('Label').t`Password`}
|
||||
ref={passwordInputRef}
|
||||
value={password}
|
||||
/>
|
||||
{error ? <div className="my-2 text-danger">{error}</div> : null}
|
||||
<Button
|
||||
className="mb-3 mt-1"
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
label={isSigningIn ? c('Action').t`Signing in...` : c('Action').t`Sign in`}
|
||||
primary
|
||||
onClick={handleSignInFormSubmit}
|
||||
disabled={isSigningIn}
|
||||
@@ -283,7 +284,7 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
label={c('Option').t`Stay signed in`}
|
||||
checked={!isEphemeral}
|
||||
disabled={isSigningIn || isRecoverySignIn}
|
||||
onChange={handleEphemeralChange}
|
||||
@@ -325,13 +326,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
title={c('Action').t`Go back`}
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
|
||||
<div className="text-base font-bold">
|
||||
{showCaptcha ? c('Title').t`Human verification` : c('Title').t`Sign in`}
|
||||
</div>
|
||||
</div>
|
||||
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
|
||||
{showNoMergeConfirmation && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AccountIllustration } from '@standardnotes/icons'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -25,22 +26,24 @@ const Authentication: FunctionComponent<Props> = ({ application }) => {
|
||||
application.accountMenuController.setShow(true)
|
||||
}
|
||||
|
||||
const loginLink = (
|
||||
<button className="cursor-pointer border-0 bg-default p-0 text-info underline" onClick={clickSignIn}>
|
||||
{c('Action').t`Sign in`}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-4 md:px-12">
|
||||
<AccountIllustration className="mb-3" />
|
||||
<Title>You're not signed in</Title>
|
||||
<Title>{c('Title').t`You're not signed in`}</Title>
|
||||
<div className="mb-3 text-center text-base lg:text-sm">
|
||||
Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.
|
||||
</div>
|
||||
<Button primary label="Create free account" onClick={clickRegister} className="mb-3" />
|
||||
<div className="text-base lg:text-sm">
|
||||
Already have an account?{' '}
|
||||
<button className="cursor-pointer border-0 bg-default p-0 text-info underline" onClick={clickSignIn}>
|
||||
Sign in
|
||||
</button>
|
||||
{c('Info')
|
||||
.t`Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.`}
|
||||
</div>
|
||||
<Button primary label={c('Action').t`Create free account`} onClick={clickRegister} className="mb-3" />
|
||||
<div className="text-base lg:text-sm">{c('Info').jt`Already have an account? ${loginLink}`}</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
|
||||
import { isErrorResponse } from '@standardnotes/snjs'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -150,19 +151,19 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={c('Label').t`Email`}
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
|
||||
{isEmailInvalid ? <div className="mb-4 text-danger">{c('Error').t`Please provide a valid email.`}</div> : null}
|
||||
<FloatingLabelInput
|
||||
className="min-w-auto mb-4 md:min-w-90"
|
||||
id="purchase-create-account-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
label={c('Label').t`Password`}
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
@@ -172,7 +173,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
|
||||
id="create-account-confirm"
|
||||
type="password"
|
||||
label="Repeat password"
|
||||
label={c('Label').t`Repeat password`}
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
ref={confirmPasswordInputRef}
|
||||
@@ -180,7 +181,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
isInvalid={isPasswordNotMatching}
|
||||
/>
|
||||
{isPasswordNotMatching ? (
|
||||
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
|
||||
<div className="mb-4 text-danger">{c('Error').t`Passwords don't match. Please try again.`}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,8 +198,10 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
<DiamondIcon className="absolute -right-2 top-0 -z-[1] h-18 w-18 translate-x-1/2" />
|
||||
|
||||
<div className="mr-0 lg:mr-12">
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
|
||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||
// translator: Full sentence: "Create your free account"
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">{c('Title').t`Create your free account`}</h1>
|
||||
// translator: Full sentence: "Create your free account to continue to Standard Notes."
|
||||
<div className="mb-4 text-sm font-medium">{c('Info').t`to continue to Standard Notes.`}</div>
|
||||
{captchaURL ? captchaIframe : CreateAccountForm}
|
||||
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
|
||||
<div className="flex flex-col">
|
||||
@@ -207,7 +210,8 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
disabled={isCreatingAccount}
|
||||
className="mb-2 flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
|
||||
>
|
||||
Sign in instead
|
||||
// translator: "Instead" here refers to "instead of creating an account"
|
||||
{c('Action').t`Sign in instead`}
|
||||
</button>
|
||||
{!application.isNativeIOS() && (
|
||||
<button
|
||||
@@ -215,14 +219,14 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
disabled={isCreatingAccount}
|
||||
className="flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
|
||||
>
|
||||
Subscribe without account
|
||||
{c('Action').t`Subscribe without account`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="mb-4 py-2.5 md:mb-0"
|
||||
primary
|
||||
label={isCreatingAccount ? 'Creating account...' : 'Create account'}
|
||||
label={isCreatingAccount ? c('Action').t`Creating account...` : c('Action').t`Create account`}
|
||||
onClick={handleCreateAccount}
|
||||
disabled={isCreatingAccount}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isEmailValid } from '@/Utils'
|
||||
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
|
||||
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import { c } from 'ttag'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -102,7 +103,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
if ((err as Error).toString().includes('Invalid email or password')) {
|
||||
setIsEmailInvalid(true)
|
||||
setIsPasswordInvalid(true)
|
||||
setOtherErrorMessage('Invalid email or password.')
|
||||
setOtherErrorMessage(c('Error').t`Invalid email or password.`)
|
||||
setPassword('')
|
||||
} else {
|
||||
application.alerts.alert(err as string).catch(console.error)
|
||||
@@ -119,7 +120,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={c('Label').t`Email`}
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
@@ -127,13 +128,13 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid && !otherErrorMessage ? (
|
||||
<div className="mb-4 text-danger">Please provide a valid email.</div>
|
||||
<div className="mb-4 text-danger">{c('Error').t`Please provide a valid email.`}</div>
|
||||
) : null}
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
label={c('Label').t`Password`}
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
@@ -145,7 +146,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
<Button
|
||||
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
|
||||
primary
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
label={isSigningIn ? c('Action').t`Signing in...` : c('Action').t`Sign in`}
|
||||
onClick={handleSignIn}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
@@ -163,17 +164,18 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
<DiamondIcon className="absolute -right-2 top-0 -z-[1] h-18 w-18 translate-x-1/2" />
|
||||
|
||||
<div>
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
|
||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">{c('Title').t`Sign in`}</h1>
|
||||
<div className="mb-4 text-sm font-medium">{c('Info').t`to continue to Standard Notes.`}</div>
|
||||
{showCaptcha ? captchaIframe : signInForm}
|
||||
<div className="text-sm font-medium text-passive-1">
|
||||
Don’t have an account yet?{' '}
|
||||
<a
|
||||
className={`text-info ${isSigningIn ? 'cursor-not-allowed' : 'cursor-pointer '}`}
|
||||
onClick={handleCreateAccountInstead}
|
||||
>
|
||||
Create account
|
||||
</a>
|
||||
{c('Info').jt`Don’t have an account yet? ${(
|
||||
<a
|
||||
className={`text-info ${isSigningIn ? 'cursor-not-allowed' : 'cursor-pointer '}`}
|
||||
onClick={handleCreateAccountInstead}
|
||||
>
|
||||
{c('Action').t`Create account`}
|
||||
</a>
|
||||
)}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user