mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-18 15:35:26 +02:00
added translation
This commit is contained in:
parent
fdcdd1af67
commit
e4acfb2a5d
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-i18n/recommended",
|
||||||
"plugin:compat/recommended",
|
"plugin:compat/recommended",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
@ -17,7 +18,7 @@
|
|||||||
"ecmaVersion": 13,
|
"ecmaVersion": 13,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": ["ordered-imports", "react", "@typescript-eslint"],
|
"plugins": ["react-i18n", "ordered-imports", "react", "@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"compat/compat": "error",
|
"compat/compat": "error",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endOfLine": "lf",
|
"endOfLine": "lf",
|
||||||
"printWidth": 80,
|
"printWidth": 100,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
|
@ -24,12 +24,17 @@
|
|||||||
"deep-equal": "^2.0.5",
|
"deep-equal": "^2.0.5",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"group-array": "^1.0.0",
|
"group-array": "^1.0.0",
|
||||||
|
"i18next": "^22.0.4",
|
||||||
|
"i18next-browser-languagedetector": "^7.0.0",
|
||||||
|
"i18next-http-backend": "^2.0.0",
|
||||||
|
"i18next-localstorage-cache": "^1.1.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"in-milliseconds": "^1.2.0",
|
"in-milliseconds": "^1.2.0",
|
||||||
"in-seconds": "^1.2.0",
|
"in-seconds": "^1.2.0",
|
||||||
"openpgp": "^5.5.0",
|
"openpgp": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^12.0.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"react-router-dom": "^6.4.2",
|
"react-router-dom": "^6.4.2",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
@ -37,7 +42,8 @@
|
|||||||
"sort-array": "^4.1.5",
|
"sort-array": "^4.1.5",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
"use-system-theme": "^0.1.1",
|
"use-system-theme": "^0.1.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11",
|
||||||
|
"yup-locales": "^1.2.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
@ -62,6 +68,7 @@
|
|||||||
"eslint-plugin-compat": "^4.0.2",
|
"eslint-plugin-compat": "^4.0.2",
|
||||||
"eslint-plugin-ordered-imports": "^0.6.0",
|
"eslint-plugin-ordered-imports": "^0.6.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.31.10",
|
||||||
|
"eslint-plugin-react-i18n": "^1.0.3",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^3.1.0"
|
"vite": "^3.1.0"
|
||||||
|
334
public/locales/en/translation.json
Normal file
334
public/locales/en/translation.json
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"cancelLabel": "Cancel",
|
||||||
|
"emptyValue": "-",
|
||||||
|
"emptyUnavailableValue": "Unavailable",
|
||||||
|
|
||||||
|
"defaultValueSelection": "Default <{{value}}>",
|
||||||
|
"defaultValueSelectionRaw": "<{{value}}>",
|
||||||
|
"booleanSelection": {
|
||||||
|
"true": "Yes",
|
||||||
|
"false": "No"
|
||||||
|
},
|
||||||
|
"defaultError": "An error occurred.",
|
||||||
|
"defaultSuccess": "Success!",
|
||||||
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
|
|
||||||
|
"routes": {
|
||||||
|
"LoginRoute": {
|
||||||
|
"forms": {
|
||||||
|
"email": {
|
||||||
|
"title": "Sign in",
|
||||||
|
"description": "We'll send you a verification code to your email.",
|
||||||
|
"continueAction": "Send Code",
|
||||||
|
"form": {
|
||||||
|
"email": {
|
||||||
|
"label": "Email",
|
||||||
|
"placeholder": "johndoe@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmCode": {
|
||||||
|
"title": "You got mail!",
|
||||||
|
"description": "We sent you a code to your email. Enter it below to login",
|
||||||
|
"continueAction": "Log in",
|
||||||
|
"form": {
|
||||||
|
"code": {
|
||||||
|
"label": "Verification Code",
|
||||||
|
"errors": {
|
||||||
|
"invalidChars": "Invalid verification code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SignupRoute": {
|
||||||
|
"forms": {
|
||||||
|
"email": {
|
||||||
|
"title": "Sign up",
|
||||||
|
"description": "We only need your email and you are ready to go!",
|
||||||
|
"continueAction": "Continue",
|
||||||
|
"form": {
|
||||||
|
"email": {
|
||||||
|
"label": "Email",
|
||||||
|
"placeholder": "johndoe@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mailVerification": {
|
||||||
|
"title": "You got mail!",
|
||||||
|
"description": "We sent you an email with a link to confirm your email address. Please check your inbox and click on the link to continue.",
|
||||||
|
"editEmail": {
|
||||||
|
"title": "Edit email address?",
|
||||||
|
"description": "Would you like to return to the previous step and edit your email address?",
|
||||||
|
"continueAction": "Yes, edit email"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VerifyEmailRoute": {
|
||||||
|
"title": "Verify your email",
|
||||||
|
"isLoading": "Verifying your email...",
|
||||||
|
"isCodeInvalid": "Sorry, but this verification code is invalid.",
|
||||||
|
"errors": {
|
||||||
|
"code": {
|
||||||
|
"invalid": "The verification code is invalid."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CompleteAccountRoute": {
|
||||||
|
"forms": {
|
||||||
|
"generateReports": {
|
||||||
|
"title": "Generate Email Reports?",
|
||||||
|
"description": "Would you like to create fully encrypted email reports for your mails? Only you will be able to access it. Not even we can decrypt it.",
|
||||||
|
"continueAction": "Yes",
|
||||||
|
"cancelAction": "No"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"title": "Set up your password",
|
||||||
|
"description": "Please enter a safe password so that we can encrypt your data.",
|
||||||
|
"continueAction": "Continue",
|
||||||
|
"form": {
|
||||||
|
"password": {
|
||||||
|
"label": "Password",
|
||||||
|
"placeholder": "********"
|
||||||
|
},
|
||||||
|
"passwordConfirm": {
|
||||||
|
"label": "Confirm Password",
|
||||||
|
"placeholder": "Re-enter your password",
|
||||||
|
"mustMatchHelperText": "Passwords do not match."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"available": {
|
||||||
|
"title": "Encryption already enabled",
|
||||||
|
"description": "You already have encryption enabled. Changing passwords is currently not supported."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AliasesRoute": {
|
||||||
|
"title": "Aliases",
|
||||||
|
"pageActions": {
|
||||||
|
"search": {
|
||||||
|
"label": "Search",
|
||||||
|
"placeholder": "Search for names"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"createRandomAlias": {
|
||||||
|
"label": "Create Random Alias"
|
||||||
|
},
|
||||||
|
"createCustomAlias": {
|
||||||
|
"label": "Create Custom Alias",
|
||||||
|
"description": "You can define your own custom alias. Note that a random suffix will be added at the end to avoid duplicates.",
|
||||||
|
"continueAction": "Create Alias",
|
||||||
|
"form": {
|
||||||
|
"address": {
|
||||||
|
"label": "Address",
|
||||||
|
"placeholder": "awesome-fish"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AliasDetailRoute": {
|
||||||
|
"title": "Alias Details",
|
||||||
|
"sections": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"description": "These settings apply to this alias only. You can either set a value manually or refer to your default settings. Note that this does change in behavior. When you set a value to refer to your default setting, the alias will always use the latest value. So when you change your default setting, the alias will automatically use the new value."
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"title": "Notes",
|
||||||
|
"form": {
|
||||||
|
"createdAt": {
|
||||||
|
"label": "Created at",
|
||||||
|
"empty": "Unavailable"
|
||||||
|
},
|
||||||
|
"personalNotes": {
|
||||||
|
"label": "Personal Notes",
|
||||||
|
"empty": "-",
|
||||||
|
"helperText": "You can enter personal notes for this alias here. Notes are encrypted."
|
||||||
|
},
|
||||||
|
"websites": {
|
||||||
|
"label": "Websites",
|
||||||
|
"emptyText": "You haven't used this alias on any site yet.",
|
||||||
|
"placeholder": "https://example.com",
|
||||||
|
"helperText": "Add a website to this alias. Used to autofill."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReportsRoute": {
|
||||||
|
"title": "Reports",
|
||||||
|
"pageActions": {
|
||||||
|
"sort": {
|
||||||
|
"List": "List reports by their date",
|
||||||
|
"GroupByAlias": "Group reports by their alias"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReportDetailRoute": {
|
||||||
|
"title": "Report Details",
|
||||||
|
"sections": {
|
||||||
|
"information": {
|
||||||
|
"title": "Email Information",
|
||||||
|
"form": {
|
||||||
|
"from": {
|
||||||
|
"label": "From"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"label": "To"
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"label": "Subject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackers": {
|
||||||
|
"title": "Trackers",
|
||||||
|
"results": {
|
||||||
|
"imageTrackers": {
|
||||||
|
"text_zero": "No image trackers found",
|
||||||
|
"text_one": "Removed 1 image tracker",
|
||||||
|
"text_other": "Removed {{count}} image trackers"
|
||||||
|
},
|
||||||
|
"proxiedImages": {
|
||||||
|
"text_zero": "No images found",
|
||||||
|
"text_one": "Proxying 1 image",
|
||||||
|
"text_other": "Proxying {{count}} images",
|
||||||
|
"status": {
|
||||||
|
"isStored": "Stored on Server",
|
||||||
|
"isProxying": "Proxying image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SettingsRoute": {
|
||||||
|
"title": "Settings",
|
||||||
|
"forms": {
|
||||||
|
"aliasPreferences": {
|
||||||
|
"title": "Alias Preferences",
|
||||||
|
"description": "Select the default behavior for your aliases. This will only affect aliases that do not have a custom behavior set."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"components": {
|
||||||
|
"NavigationButton": {
|
||||||
|
"overview": "Overview",
|
||||||
|
"aliases": "Aliases",
|
||||||
|
"reports": "Reports",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"AuthenticateRoute": {
|
||||||
|
"signup": "Sign up",
|
||||||
|
"login": "Log in"
|
||||||
|
},
|
||||||
|
"EnterDecryptionPassword": {
|
||||||
|
"title": "Decrypt Reports",
|
||||||
|
"description": "Please enter your password so that your reports can de decrypted.",
|
||||||
|
"cancelAction": "Decrypt later",
|
||||||
|
"continueAction": "Continue",
|
||||||
|
"form": {
|
||||||
|
"password": {
|
||||||
|
"label": "Password",
|
||||||
|
"placeholder": "********",
|
||||||
|
"errors": {
|
||||||
|
"invalidPassword": "Password is invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ResendMailButton": {
|
||||||
|
"label": "Resend Mail"
|
||||||
|
},
|
||||||
|
"OpenMailButton": {
|
||||||
|
"label": "Open Mail"
|
||||||
|
},
|
||||||
|
"DecryptionPasswordMissingAlert": {
|
||||||
|
"unavailable": {
|
||||||
|
"title": "Encryption required",
|
||||||
|
"description": "You need to set up encryption to use this feature.",
|
||||||
|
"continueAction": "Set up encryption"
|
||||||
|
},
|
||||||
|
"passwordRequired": {
|
||||||
|
"title": "Password required",
|
||||||
|
"description": "Your decryption password is required to view this section.",
|
||||||
|
"continueAction": "Enter password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TimedButton": {
|
||||||
|
"remainingTime_one": "({{count}})",
|
||||||
|
"remainingTime_other": "({{count}})"
|
||||||
|
},
|
||||||
|
"ErrorLoadingDataMessage": {
|
||||||
|
"tryAgain": "Try Again"
|
||||||
|
},
|
||||||
|
"AliasTypeIndicator": {
|
||||||
|
"random": "This is a randomly generated alias",
|
||||||
|
"custom": "This is a custom-made alias"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"relations": {
|
||||||
|
"alias": {
|
||||||
|
"mutations": {
|
||||||
|
"success": {
|
||||||
|
"aliasCreation": "Created Alias successfully!",
|
||||||
|
"aliasUpdated": "Updated Alias successfully!",
|
||||||
|
"notesUpdated": "Updated & encrypted notes successfully!",
|
||||||
|
"aliasChangedToEnabled": "Alias has been enabled",
|
||||||
|
"aliasChangedToDisabled": "Alias has been disabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"removeTrackers": {
|
||||||
|
"label": "Remove Trackers",
|
||||||
|
"helperText": "Remove single-pixel image trackers as well as url trackers."
|
||||||
|
},
|
||||||
|
"createMailReports": {
|
||||||
|
"label": "Create Mail Reports",
|
||||||
|
"helperText": "Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."
|
||||||
|
},
|
||||||
|
"proxyImages": {
|
||||||
|
"label": "Proxy Images",
|
||||||
|
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the original server has no idea you have opened the mail. After the cache time, the image is loaded from the original server, but it gets proxied by us. This means the original server will not be able to access neither your IP address nor your user agent."
|
||||||
|
},
|
||||||
|
"imageProxyFormat": {
|
||||||
|
"label": "Image File Type",
|
||||||
|
"enumTexts": {
|
||||||
|
"jpeg": "JPEG",
|
||||||
|
"png": "PNG",
|
||||||
|
"webp": "WEBP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"imageProxyUserAgent": {
|
||||||
|
"label": "Image Proxy User Agent",
|
||||||
|
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used by the proxy. User Agents are kept up-to-date.",
|
||||||
|
"enumTexts": {
|
||||||
|
"apple-mail": "Apple Mail",
|
||||||
|
"google-mail": "Google Mail",
|
||||||
|
"outlook-windows": "Outlook / Windows",
|
||||||
|
"outlook-macos": "Outlook / MacOS",
|
||||||
|
"firefox": "Firefox Browser",
|
||||||
|
"chrome": "Chrome Browser"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveAction": "Save Settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"emailMeta": {
|
||||||
|
"flow": "{{from}} -> {{to}}",
|
||||||
|
"emptySubject": "<No Subject>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,8 @@ import SettingsRoute from "~/routes/SettingsRoute"
|
|||||||
import SignupRoute from "~/routes/SignupRoute"
|
import SignupRoute from "~/routes/SignupRoute"
|
||||||
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
||||||
|
|
||||||
|
import "./init-i18n"
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
import {FaHashtag, FaRandom} from "react-icons/fa"
|
import {FaHashtag, FaRandom} from "react-icons/fa"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Box, Tooltip} from "@mui/material"
|
import {Box, Tooltip} from "@mui/material"
|
||||||
|
|
||||||
import {AliasType} from "~/server-types"
|
import {AliasType} from "~/server-types"
|
||||||
|
import {createEnumMapFromTranslation} from "~/utils"
|
||||||
|
|
||||||
export interface AliasTypeIndicatorProps {
|
export interface AliasTypeIndicatorProps {
|
||||||
type: AliasType
|
type: AliasType
|
||||||
@ -14,16 +16,16 @@ const ALIAS_TYPE_ICON_MAP: Record<AliasType, ReactElement> = {
|
|||||||
[AliasType.CUSTOM]: <FaHashtag />,
|
[AliasType.CUSTOM]: <FaHashtag />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALIAS_TYPE_TOOLTIP_MAP: Record<AliasType, string> = {
|
const ALIAS_TYPE_TOOLTIP_MAP = createEnumMapFromTranslation(
|
||||||
[AliasType.RANDOM]: "This is a randomly generated alias",
|
"components.AliasTypeIndicator",
|
||||||
[AliasType.CUSTOM]: "This is a custom-made alias",
|
AliasType,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
export default function AliasTypeIndicator({type}: AliasTypeIndicatorProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
export default function AliasTypeIndicator({
|
|
||||||
type,
|
|
||||||
}: AliasTypeIndicatorProps): ReactElement {
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={ALIAS_TYPE_TOOLTIP_MAP[type]} arrow>
|
<Tooltip title={t(ALIAS_TYPE_TOOLTIP_MAP[type] as string)} arrow>
|
||||||
<Box display="flex" justifyContent="center" alignItems="center">
|
<Box display="flex" justifyContent="center" alignItems="center">
|
||||||
{ALIAS_TYPE_ICON_MAP[type]}
|
{ALIAS_TYPE_ICON_MAP[type]}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {useContext} from "react"
|
import {useContext} from "react"
|
||||||
import {MdLock} from "react-icons/md"
|
import {MdLock} from "react-icons/md"
|
||||||
import {Link as RouterLink} from "react-router-dom"
|
import {Link as RouterLink} from "react-router-dom"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Button, Grid, Typography, useTheme} from "@mui/material"
|
import {Button, Grid, Typography, useTheme} from "@mui/material"
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ export interface WithEncryptionRequiredProps {
|
|||||||
export default function DecryptionPasswordMissingAlert({
|
export default function DecryptionPasswordMissingAlert({
|
||||||
children = <></>,
|
children = <></>,
|
||||||
}: WithEncryptionRequiredProps): JSX.Element {
|
}: WithEncryptionRequiredProps): JSX.Element {
|
||||||
|
const {t} = useTranslation()
|
||||||
const {handleAnchorClick} = useContext(LockNavigationContext)
|
const {handleAnchorClick} = useContext(LockNavigationContext)
|
||||||
const {encryptionStatus} = useContext(AuthContext)
|
const {encryptionStatus} = useContext(AuthContext)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -22,7 +24,7 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
case EncryptionStatus.Unavailable: {
|
case EncryptionStatus.Unavailable: {
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
paddingY={2}
|
padding={4}
|
||||||
bgcolor={theme.palette.background.default}
|
bgcolor={theme.palette.background.default}
|
||||||
container
|
container
|
||||||
gap={2}
|
gap={2}
|
||||||
@ -31,12 +33,12 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h2">
|
<Typography variant="h6" component="h2">
|
||||||
Encryption required
|
{t("components.DecryptionPasswordMissingAlert.unavailable.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography>
|
<Typography>
|
||||||
You need to set up encryption to use this feature.
|
{t("components.DecryptionPasswordMissingAlert.unavailable.description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -47,7 +49,9 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
startIcon={<MdLock />}
|
startIcon={<MdLock />}
|
||||||
onClick={handleAnchorClick}
|
onClick={handleAnchorClick}
|
||||||
>
|
>
|
||||||
Setup encryption
|
{t(
|
||||||
|
"components.DecryptionPasswordMissingAlert.unavailable.continueAction",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -57,7 +61,7 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
case EncryptionStatus.PasswordRequired: {
|
case EncryptionStatus.PasswordRequired: {
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
paddingY={2}
|
padding={4}
|
||||||
bgcolor={theme.palette.background.default}
|
bgcolor={theme.palette.background.default}
|
||||||
container
|
container
|
||||||
gap={2}
|
gap={2}
|
||||||
@ -66,13 +70,14 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h2">
|
<Typography variant="h6" component="h2">
|
||||||
Password required
|
{t("components.DecryptionPasswordMissingAlert.passwordRequired.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography>
|
<Typography>
|
||||||
Your decryption password is required to view this
|
{t(
|
||||||
section.
|
"components.DecryptionPasswordMissingAlert.passwordRequired.description",
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -82,7 +87,9 @@ export default function DecryptionPasswordMissingAlert({
|
|||||||
startIcon={<MdLock />}
|
startIcon={<MdLock />}
|
||||||
onClick={handleAnchorClick}
|
onClick={handleAnchorClick}
|
||||||
>
|
>
|
||||||
Enter Password
|
{t(
|
||||||
|
"components.DecryptionPasswordMissingAlert.passwordRequired.continueAction",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
import {Alert, Button, Grid} from "@mui/material"
|
import {Alert, Button, Grid} from "@mui/material"
|
||||||
@ -11,13 +12,17 @@ export default function ErrorLoadingDataMessage({
|
|||||||
message,
|
message,
|
||||||
onRetry,
|
onRetry,
|
||||||
}: ErrorLoadingDataMessageProps): ReactElement {
|
}: ErrorLoadingDataMessageProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2} flexDirection="column" alignItems="center">
|
<Grid container spacing={2} flexDirection="column" alignItems="center">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Alert severity="error">{message}</Alert>
|
<Alert severity="error">{message}</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button onClick={onRetry}>Try Again</Button>
|
<Button onClick={onRetry}>
|
||||||
|
{t("components.ErrorLoadingDataMessage.tryAgain")}
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {ReactElement, useEffect, useState} from "react"
|
import {ReactElement} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {CircularProgress, Grid, Typography} from "@mui/material"
|
import {CircularProgress, Grid, Typography} from "@mui/material"
|
||||||
|
|
||||||
@ -6,24 +7,8 @@ export interface LoadingDataProps {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoadingData({
|
export default function LoadingData({message = "Loading"}: LoadingDataProps): ReactElement {
|
||||||
message = "Loading",
|
const {t} = useTranslation()
|
||||||
}: LoadingDataProps): ReactElement {
|
|
||||||
const [ellipsis, setEllipsis] = useState<string>("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setEllipsis((value: string) => {
|
|
||||||
if (value.length === 3) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return value + "."
|
|
||||||
})
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2} direction="column" alignItems="center">
|
<Grid container spacing={2} direction="column" alignItems="center">
|
||||||
@ -31,7 +16,7 @@ export default function LoadingData({
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="caption">Loading{ellipsis}</Typography>
|
<Typography variant="caption">{t("general.loading")}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {ReactElement, useEffect, useLayoutEffect, useRef, useState} from "react"
|
import {ReactElement, useEffect, useLayoutEffect, useRef, useState} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {UseMutationResult} from "@tanstack/react-query"
|
import {UseMutationResult} from "@tanstack/react-query"
|
||||||
import {Alert, AlertProps, Snackbar} from "@mui/material"
|
import {Alert, AlertProps, Snackbar} from "@mui/material"
|
||||||
|
|
||||||
import {FastAPIError} from "~/utils"
|
import {FastAPIError} from "~/utils"
|
||||||
import {SimpleDetailResponse} from "~/server-types"
|
import {SimpleDetailResponse} from "~/server-types"
|
||||||
import getErrorMessage from "~/utils/get-error-message"
|
|
||||||
|
|
||||||
export interface MutationStatusSnackbarProps<
|
export interface MutationStatusSnackbarProps<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
@ -29,12 +29,9 @@ export default function MutationStatusSnackbar<
|
|||||||
mutation,
|
mutation,
|
||||||
successMessage,
|
successMessage,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}: MutationStatusSnackbarProps<
|
}: MutationStatusSnackbarProps<TData, TError, TVariables, TContext>): ReactElement {
|
||||||
TData,
|
const {t} = useTranslation()
|
||||||
TError,
|
|
||||||
TVariables,
|
|
||||||
TContext
|
|
||||||
>): ReactElement {
|
|
||||||
const $severity = useRef<AlertProps["severity"]>()
|
const $severity = useRef<AlertProps["severity"]>()
|
||||||
const $message = useRef<string>()
|
const $message = useRef<string>()
|
||||||
|
|
||||||
@ -60,22 +57,22 @@ export default function MutationStatusSnackbar<
|
|||||||
$message.current = (() => {
|
$message.current = (() => {
|
||||||
if (mutation.isError) {
|
if (mutation.isError) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return errorMessage ?? getErrorMessage(mutation.error)
|
return (
|
||||||
|
errorMessage ||
|
||||||
|
(mutation.error.response?.data as any).detail ||
|
||||||
|
t("general.defaultError")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mutation.isSuccess) {
|
if (mutation.isSuccess) {
|
||||||
return successMessage ?? mutation.data?.detail ?? "Success!"
|
return successMessage ?? mutation.data?.detail ?? t("general.defaultSuccess")
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
}, [mutation.isSuccess, mutation.isError])
|
}, [mutation.isSuccess, mutation.isError])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Snackbar
|
<Snackbar open={open} onClose={() => setOpen(false)} autoHideDuration={5000}>
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
autoHideDuration={5000}
|
|
||||||
>
|
|
||||||
<Alert severity={$severity.current} variant="filled">
|
<Alert severity={$severity.current} variant="filled">
|
||||||
{$message.current}
|
{$message.current}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -5,24 +5,21 @@ import UAParser from "ua-parser-js"
|
|||||||
import {Button} from "@mui/material"
|
import {Button} from "@mui/material"
|
||||||
|
|
||||||
import {APP_LINK_MAP} from "~/utils"
|
import {APP_LINK_MAP} from "~/utils"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface OpenMailButtonProps {
|
export interface OpenMailButtonProps {
|
||||||
domain: string
|
domain: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OpenMailButton({
|
export default function OpenMailButton({domain}: OpenMailButtonProps): ReactElement {
|
||||||
domain,
|
const {t} = useTranslation()
|
||||||
}: OpenMailButtonProps): ReactElement {
|
|
||||||
const userAgent = new UAParser()
|
const userAgent = new UAParser()
|
||||||
|
|
||||||
if (userAgent.getOS().name === "Android" && APP_LINK_MAP[domain]) {
|
if (userAgent.getOS().name === "Android" && APP_LINK_MAP[domain]) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button startIcon={<IoMdMailOpen />} variant="text" href={APP_LINK_MAP[domain].android}>
|
||||||
startIcon={<IoMdMailOpen />}
|
{t("components.OpenMailButton.label")}
|
||||||
variant="text"
|
|
||||||
href={APP_LINK_MAP[domain].android}
|
|
||||||
>
|
|
||||||
Open Mail
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
21
src/components/SimpleInformationContainer.tsx
Normal file
21
src/components/SimpleInformationContainer.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
|
||||||
|
import {Grid} from "@mui/material"
|
||||||
|
|
||||||
|
export interface SimpleInformationContainerProps {
|
||||||
|
children: ReactElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleInformationContainer({
|
||||||
|
children,
|
||||||
|
}: SimpleInformationContainerProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2} direction="column">
|
||||||
|
{children.map(child => (
|
||||||
|
<Grid item key={child.key}>
|
||||||
|
{child}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
21
src/components/SimpleMultipleSections.tsx
Normal file
21
src/components/SimpleMultipleSections.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
|
||||||
|
import {Grid} from "@mui/material"
|
||||||
|
|
||||||
|
export interface SimpleMultipleSectionsProps {
|
||||||
|
children: ReactElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleMultipleSections({
|
||||||
|
children,
|
||||||
|
}: SimpleMultipleSectionsProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6} direction="column">
|
||||||
|
{children.map(child => (
|
||||||
|
<Grid item key={child.key}>
|
||||||
|
{child}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
37
src/components/SimpleOverlayInformation.tsx
Normal file
37
src/components/SimpleOverlayInformation.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
|
import {Grid, Typography} from "@mui/material"
|
||||||
|
|
||||||
|
export interface SimpleOverlayInformationProps {
|
||||||
|
label: string
|
||||||
|
emptyText?: string
|
||||||
|
icon?: ReactElement
|
||||||
|
children?: ReactElement | string | boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleOverlayInformation({
|
||||||
|
label,
|
||||||
|
emptyText,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: SimpleOverlayInformationProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const emptyTextValue = emptyText ?? t("general.emptyValue")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={1} direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="overline">{label}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={1} flexDirection="row" alignItems="center">
|
||||||
|
{icon && <Grid item>{icon}</Grid>}
|
||||||
|
<Grid item>
|
||||||
|
{children || <Typography variant="body2">{emptyTextValue}</Typography>}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
21
src/components/SimpleSection.tsx
Normal file
21
src/components/SimpleSection.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {ReactElement, ReactNode} from "react"
|
||||||
|
|
||||||
|
import {Grid, Typography} from "@mui/material"
|
||||||
|
|
||||||
|
export interface SimpleSectionProps {
|
||||||
|
label: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleSection({label, children}: SimpleSectionProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<Grid container direction="column" spacing={1}>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h6" component="h2">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>{children}</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
@ -5,6 +5,7 @@ import {LoadingButton, LoadingButtonProps} from "@mui/lab"
|
|||||||
|
|
||||||
import {useIntervalUpdate} from "~/hooks"
|
import {useIntervalUpdate} from "~/hooks"
|
||||||
import {isDev} from "~/constants/development"
|
import {isDev} from "~/constants/development"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface TimedButtonProps extends LoadingButtonProps {
|
export interface TimedButtonProps extends LoadingButtonProps {
|
||||||
interval: number
|
interval: number
|
||||||
@ -17,7 +18,10 @@ export default function TimedButton({
|
|||||||
disabled: parentDisabled = false,
|
disabled: parentDisabled = false,
|
||||||
...props
|
...props
|
||||||
}: TimedButtonProps): ReactElement {
|
}: TimedButtonProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const [startDate, resetInterval] = useIntervalUpdate(1000)
|
const [startDate, resetInterval] = useIntervalUpdate(1000)
|
||||||
|
|
||||||
const secondsPassed = differenceInSeconds(new Date(), startDate)
|
const secondsPassed = differenceInSeconds(new Date(), startDate)
|
||||||
const secondsLeft = (isDev ? 3 : interval) - secondsPassed
|
const secondsLeft = (isDev ? 3 : interval) - secondsPassed
|
||||||
|
|
||||||
@ -30,8 +34,10 @@ export default function TimedButton({
|
|||||||
onClick?.(event)
|
onClick?.(event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{children}</span>
|
<span>{children} </span>
|
||||||
{secondsLeft > 0 && <span> ({secondsLeft})</span>}
|
{secondsLeft > 0 && (
|
||||||
|
<span>{t("components.TimedButton.remainingTime", {count: secondsLeft})}</span>
|
||||||
|
)}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -30,3 +30,9 @@ export * from "./DecryptionPasswordMissingAlert"
|
|||||||
export {default as DecryptionPasswordMissingAlert} from "./DecryptionPasswordMissingAlert"
|
export {default as DecryptionPasswordMissingAlert} from "./DecryptionPasswordMissingAlert"
|
||||||
export * from "./FaviconImage"
|
export * from "./FaviconImage"
|
||||||
export {default as FaviconImage} from "./FaviconImage"
|
export {default as FaviconImage} from "./FaviconImage"
|
||||||
|
export * from "./SimpleOverlayInformation"
|
||||||
|
export {default as SimpleOverlayInformation} from "./SimpleOverlayInformation"
|
||||||
|
export * from "./SimpleInformationContainer"
|
||||||
|
export {default as SimpleInformationContainer} from "./SimpleInformationContainer"
|
||||||
|
|
||||||
|
export * as SimplePageBuilder from "./simple-page-builder"
|
||||||
|
5
src/components/simple-page-builder.ts
Normal file
5
src/components/simple-page-builder.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export {default as Page} from "./SimplePage"
|
||||||
|
export {default as Section} from "./SimpleSection"
|
||||||
|
export {default as MultipleSections} from "./SimpleMultipleSections"
|
||||||
|
export {default as OverlayInformation} from "./SimpleOverlayInformation"
|
||||||
|
export {default as InformationContainer} from "./SimpleInformationContainer"
|
11
src/constants/enum-mappings.ts
Normal file
11
src/constants/enum-mappings.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {ImageProxyFormatType, ProxyUserAgentType} from "~/server-types"
|
||||||
|
import {createEnumMapFromTranslation} from "~/utils"
|
||||||
|
|
||||||
|
export const IMAGE_PROXY_FORMAT_TYPE_NAME_MAP = createEnumMapFromTranslation(
|
||||||
|
"relations.alias.settings.imageProxyFormat.enumTexts",
|
||||||
|
ImageProxyFormatType,
|
||||||
|
)
|
||||||
|
export const IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP = createEnumMapFromTranslation(
|
||||||
|
"relations.alias.settings.imageProxyUserAgent.enumTexts",
|
||||||
|
ProxyUserAgentType,
|
||||||
|
)
|
@ -1,22 +0,0 @@
|
|||||||
import {ImageProxyFormatType, ProxyUserAgentType} from "~/server-types"
|
|
||||||
|
|
||||||
export const IMAGE_PROXY_FORMAT_TYPE_NAME_MAP: Record<
|
|
||||||
ImageProxyFormatType,
|
|
||||||
string
|
|
||||||
> = {
|
|
||||||
[ImageProxyFormatType.JPEG]: "JPEG",
|
|
||||||
[ImageProxyFormatType.PNG]: "PNG",
|
|
||||||
[ImageProxyFormatType.WEBP]: "WebP",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP: Record<
|
|
||||||
ProxyUserAgentType,
|
|
||||||
string
|
|
||||||
> = {
|
|
||||||
[ProxyUserAgentType.APPLE_MAIL]: "Apple Mail",
|
|
||||||
[ProxyUserAgentType.GOOGLE_MAIL]: "Google Mail",
|
|
||||||
[ProxyUserAgentType.CHROME]: "Chrome Browser",
|
|
||||||
[ProxyUserAgentType.FIREFOX]: "Firefox Browser",
|
|
||||||
[ProxyUserAgentType.OUTLOOK_MACOS]: "Outlook / MacOS",
|
|
||||||
[ProxyUserAgentType.OUTLOOK_WINDOWS]: "Outlook / Windows",
|
|
||||||
}
|
|
30
src/init-i18n.ts
Normal file
30
src/init-i18n.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {initReactI18next} from "react-i18next"
|
||||||
|
import {de} from "yup-locales"
|
||||||
|
import {setLocale} from "yup"
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line ordered-imports/ordered-imports
|
||||||
|
import Cache from "i18next-localstorage-cache"
|
||||||
|
import HttpApi from "i18next-http-backend"
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector"
|
||||||
|
import i18n from "i18next"
|
||||||
|
|
||||||
|
import {isDev} from "~/constants/development"
|
||||||
|
|
||||||
|
i18n.use(HttpApi)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(Cache)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
debug: isDev,
|
||||||
|
fallbackLng: "en",
|
||||||
|
load: "languageOnly",
|
||||||
|
backend: {
|
||||||
|
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||||
|
},
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setLocale(de)
|
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import {URL_REGEX} from "~/constants/values"
|
import {URL_REGEX} from "~/constants/values"
|
||||||
import {whenEnterPressed} from "~/utils"
|
import {whenEnterPressed} from "~/utils"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface AddWebsiteFieldProps {
|
export interface AddWebsiteFieldProps {
|
||||||
onAdd: (website: string) => Promise<void>
|
onAdd: (website: string) => Promise<void>
|
||||||
@ -32,6 +33,7 @@ export default function AddWebsiteField({
|
|||||||
onAdd,
|
onAdd,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: AddWebsiteFieldProps): ReactElement {
|
}: AddWebsiteFieldProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const websiteFormik = useFormik<WebsiteForm>({
|
const websiteFormik = useFormik<WebsiteForm>({
|
||||||
validationSchema: WEBSITE_SCHEMA,
|
validationSchema: WEBSITE_SCHEMA,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -60,13 +62,18 @@ export default function AddWebsiteField({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2} direction="column">
|
<Grid container direction="column">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FormGroup row>
|
<FormGroup row>
|
||||||
<TextField
|
<TextField
|
||||||
name="url"
|
name="url"
|
||||||
id="url"
|
id="url"
|
||||||
label="Website"
|
label={t(
|
||||||
|
"routes.AliasDetailRoute.sections.notes.form.websites.label",
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.AliasDetailRoute.sections.notes.form.websites.placeholder",
|
||||||
|
)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={websiteFormik.values.url}
|
value={websiteFormik.values.url}
|
||||||
onChange={websiteFormik.handleChange}
|
onChange={websiteFormik.handleChange}
|
||||||
@ -106,7 +113,9 @@ export default function AddWebsiteField({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(websiteFormik.touched.url && websiteFormik.errors.url) ||
|
{(websiteFormik.touched.url && websiteFormik.errors.url) ||
|
||||||
"Add a website to this alias. Used to autofill."}
|
t(
|
||||||
|
"routes.AliasDetailRoute.sections.notes.form.websites.helperText",
|
||||||
|
)}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import {useParams} from "react-router"
|
import {useParams} from "react-router"
|
||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Grid, Typography} from "@mui/material"
|
import {Grid, Typography} from "@mui/material"
|
||||||
|
|
||||||
import {AliasTypeIndicator, DecryptionPasswordMissingAlert} from "~/components"
|
import {AliasTypeIndicator, DecryptionPasswordMissingAlert, SimplePageBuilder} from "~/components"
|
||||||
import {Alias, DecryptedAlias} from "~/server-types"
|
import {Alias, DecryptedAlias} from "~/server-types"
|
||||||
import {useUIState} from "~/hooks"
|
import {useUIState} from "~/hooks"
|
||||||
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
|
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
|
||||||
@ -15,25 +16,17 @@ export interface AliasDetailsProps {
|
|||||||
alias: Alias | DecryptedAlias
|
alias: Alias | DecryptedAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDomain = (url: string): string => {
|
export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): ReactElement {
|
||||||
const {hostname, port} = new URL(url)
|
const {t} = useTranslation()
|
||||||
return `${hostname}${port ? `:${port}` : ""}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AliasDetails({
|
|
||||||
alias: aliasValue,
|
|
||||||
}: AliasDetailsProps): ReactElement {
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const {encryptionStatus} = useContext(AuthContext)
|
const {encryptionStatus} = useContext(AuthContext)
|
||||||
const address = atob(params.addressInBase64 as string)
|
const address = atob(params.addressInBase64 as string)
|
||||||
|
|
||||||
const [aliasUIState, setAliasUIState] = useUIState<Alias | DecryptedAlias>(
|
const [aliasUIState, setAliasUIState] = useUIState<Alias | DecryptedAlias>(aliasValue)
|
||||||
aliasValue,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={4}>
|
<SimplePageBuilder.MultipleSections>
|
||||||
<Grid item>
|
{[
|
||||||
<Grid container spacing={1} direction="row" alignItems="center">
|
<Grid container spacing={1} direction="row" alignItems="center">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<AliasTypeIndicator type={aliasUIState.type} />
|
<AliasTypeIndicator type={aliasUIState.type} />
|
||||||
@ -48,45 +41,25 @@ export default function AliasDetails({
|
|||||||
onChanged={setAliasUIState}
|
onChanged={setAliasUIState}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>,
|
||||||
</Grid>
|
<div key="notes">
|
||||||
<Grid item width="100%">
|
{encryptionStatus === EncryptionStatus.Available ? (
|
||||||
{encryptionStatus === EncryptionStatus.Available ? (
|
<AliasNotesForm
|
||||||
<AliasNotesForm
|
id={aliasUIState.id}
|
||||||
id={aliasUIState.id}
|
notes={(aliasUIState as DecryptedAlias).notes}
|
||||||
notes={(aliasUIState as DecryptedAlias).notes}
|
|
||||||
onChanged={setAliasUIState}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DecryptionPasswordMissingAlert />
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h6" component="h3">
|
|
||||||
Settings
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<AliasPreferencesForm
|
|
||||||
alias={aliasUIState}
|
|
||||||
onChanged={setAliasUIState}
|
onChanged={setAliasUIState}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
) : (
|
||||||
<Grid item>
|
<DecryptionPasswordMissingAlert />
|
||||||
<Typography variant="body2">
|
)}
|
||||||
These settings apply to this alias only. You can
|
</div>,
|
||||||
either set a value manually or refer to your default
|
<SimplePageBuilder.Section
|
||||||
settings. Note that this does change in behavior.
|
label={t("routes.AliasDetailRoute.sections.settings.title")}
|
||||||
When you set a value to refer to your default
|
key="settings"
|
||||||
setting, the alias will always use the latest value.
|
>
|
||||||
So when you change your default setting, the alias
|
<AliasPreferencesForm alias={aliasUIState} onChanged={setAliasUIState} />
|
||||||
will automatically use the new value.
|
</SimplePageBuilder.Section>,
|
||||||
</Typography>
|
]}
|
||||||
</Grid>
|
</SimplePageBuilder.MultipleSections>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,10 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {ErrorSnack, FaviconImage, SuccessSnack} from "~/components"
|
import {ErrorSnack, FaviconImage, SimpleOverlayInformation, SuccessSnack} from "~/components"
|
||||||
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
|
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
|
||||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField"
|
import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
||||||
@ -57,27 +58,22 @@ const SCHEMA = yup.object().shape({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function AliasNotesForm({
|
export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormProps): ReactElement {
|
||||||
id,
|
const {t} = useTranslation()
|
||||||
notes,
|
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext)
|
||||||
onChanged,
|
const {mutateAsync, isSuccess} = useMutation<Alias, AxiosError, UpdateAliasData>(
|
||||||
}: AliasNotesFormProps): ReactElement {
|
values => updateAlias(id, values),
|
||||||
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} =
|
{
|
||||||
useContext(AuthContext)
|
onSuccess: newAlias => {
|
||||||
const {mutateAsync, isSuccess} = useMutation<
|
;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes(
|
||||||
Alias,
|
newAlias.encryptedNotes,
|
||||||
AxiosError,
|
_decryptUsingMasterPassword,
|
||||||
UpdateAliasData
|
)
|
||||||
>(values => updateAlias(id, values), {
|
|
||||||
onSuccess: newAlias => {
|
|
||||||
;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes(
|
|
||||||
newAlias.encryptedNotes,
|
|
||||||
_decryptUsingMasterPassword,
|
|
||||||
)
|
|
||||||
|
|
||||||
onChanged(newAlias as any as DecryptedAlias)
|
onChanged(newAlias as any as DecryptedAlias)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
const initialValues = useMemo(
|
const initialValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
personalNotes: notes.data.personalNotes,
|
personalNotes: notes.data.personalNotes,
|
||||||
@ -101,9 +97,7 @@ export default function AliasNotesForm({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = _encryptUsingMasterPassword(
|
const data = _encryptUsingMasterPassword(JSON.stringify(newNotes))
|
||||||
JSON.stringify(newNotes),
|
|
||||||
)
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
encryptedNotes: data,
|
encryptedNotes: data,
|
||||||
})
|
})
|
||||||
@ -118,12 +112,12 @@ export default function AliasNotesForm({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<Grid container direction="column" spacing={4}>
|
<Grid container direction="column" spacing={1}>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={1} direction="row">
|
<Grid container spacing={1} direction="row">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h3">
|
<Typography variant="h6" component="h3">
|
||||||
Notes
|
{t("routes.AliasDetailRoute.sections.notes.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -133,13 +127,9 @@ export default function AliasNotesForm({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (
|
||||||
isInEditMode &&
|
isInEditMode &&
|
||||||
!deepEqual(
|
!deepEqual(initialValues, formik.values, {
|
||||||
initialValues,
|
strict: true,
|
||||||
formik.values,
|
})
|
||||||
{
|
|
||||||
strict: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
await formik.submitForm()
|
await formik.submitForm()
|
||||||
}
|
}
|
||||||
@ -147,117 +137,93 @@ export default function AliasNotesForm({
|
|||||||
setIsInEditMode(!isInEditMode)
|
setIsInEditMode(!isInEditMode)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isInEditMode ? (
|
{isInEditMode ? <MdCheckCircle /> : <FaPen />}
|
||||||
<MdCheckCircle />
|
|
||||||
) : (
|
|
||||||
<FaPen />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={4} direction="column">
|
<Grid container spacing={2} direction="column">
|
||||||
{notes.data.createdAt && (
|
{notes.data.createdAt && (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid
|
<SimpleOverlayInformation
|
||||||
container
|
emptyText={t("general.emptyUnavailableValue")}
|
||||||
spacing={1}
|
icon={<MdEditCalendar />}
|
||||||
flexDirection="row"
|
label={t(
|
||||||
alignItems="center"
|
"routes.AliasDetailRoute.sections.notes.form.createdAt.label",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Grid item>
|
{notes.data.createdAt && (
|
||||||
<MdEditCalendar />
|
<Tooltip title={notes.data.createdAt.toISOString()}>
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Tooltip
|
|
||||||
title={notes.data.createdAt.toISOString()}
|
|
||||||
>
|
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{format(
|
{format(notes.data.createdAt, "Pp")}
|
||||||
notes.data.createdAt,
|
|
||||||
"Pp",
|
|
||||||
)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Grid>
|
)}
|
||||||
</Grid>
|
</SimpleOverlayInformation>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={1} direction="column">
|
<SimpleOverlayInformation
|
||||||
<Grid item>
|
label={t(
|
||||||
<Typography variant="overline">
|
"routes.AliasDetailRoute.sections.notes.form.personalNotes.label",
|
||||||
Personal Notes
|
)}
|
||||||
</Typography>
|
>
|
||||||
</Grid>
|
{isInEditMode ? (
|
||||||
<Grid item>
|
<TextField
|
||||||
{isInEditMode ? (
|
label="Personal Notes"
|
||||||
<TextField
|
multiline
|
||||||
label="Personal Notes"
|
fullWidth
|
||||||
multiline
|
key="personalNotes"
|
||||||
fullWidth
|
id="personalNotes"
|
||||||
key="personalNotes"
|
name="personalNotes"
|
||||||
id="personalNotes"
|
value={formik.values.personalNotes}
|
||||||
name="personalNotes"
|
onChange={formik.handleChange}
|
||||||
value={
|
onBlur={formik.handleBlur}
|
||||||
formik.values.personalNotes
|
disabled={formik.isSubmitting}
|
||||||
}
|
error={
|
||||||
onChange={formik.handleChange}
|
formik.touched.personalNotes &&
|
||||||
onBlur={formik.handleBlur}
|
Boolean(formik.errors.personalNotes)
|
||||||
disabled={formik.isSubmitting}
|
}
|
||||||
error={
|
helperText={
|
||||||
formik.touched
|
(formik.touched.personalNotes &&
|
||||||
.personalNotes &&
|
formik.errors.personalNotes) ||
|
||||||
Boolean(
|
t(
|
||||||
formik.errors
|
"routes.AliasDetailRoute.sections.notes.form.personalNotes.helperText",
|
||||||
.personalNotes,
|
)
|
||||||
)
|
}
|
||||||
}
|
InputProps={{
|
||||||
helperText={
|
startAdornment: (
|
||||||
(formik.touched
|
<InputAdornment position="start">
|
||||||
.personalNotes &&
|
<RiStickyNoteFill />
|
||||||
formik.errors
|
</InputAdornment>
|
||||||
.personalNotes) ||
|
),
|
||||||
"You can enter personal notes for this alias here. Notes are encrypted."
|
}}
|
||||||
}
|
/>
|
||||||
InputProps={{
|
) : (
|
||||||
startAdornment: (
|
notes.data.personalNotes
|
||||||
<InputAdornment position="start">
|
)}
|
||||||
<RiStickyNoteFill />
|
</SimpleOverlayInformation>
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Typography>
|
|
||||||
{notes.data.personalNotes}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={1} direction="column">
|
<SimpleOverlayInformation
|
||||||
<Grid item>
|
label={t(
|
||||||
<Typography variant="overline">
|
"routes.AliasDetailRoute.sections.notes.form.websites.label",
|
||||||
Websites
|
)}
|
||||||
</Typography>
|
emptyText={t(
|
||||||
</Grid>
|
"routes.AliasDetailRoute.sections.notes.form.websites.emptyText",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isInEditMode ? (
|
{isInEditMode ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<AddWebsiteField
|
<AddWebsiteField
|
||||||
onAdd={async website => {
|
onAdd={async website => {
|
||||||
await formik.setFieldValue(
|
await formik.setFieldValue("websites", [
|
||||||
"websites",
|
...formik.values.websites,
|
||||||
[
|
{
|
||||||
...formik.values
|
url: website,
|
||||||
.websites,
|
},
|
||||||
{
|
])
|
||||||
url: website,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
/>
|
/>
|
||||||
@ -267,26 +233,15 @@ export default function AliasNotesForm({
|
|||||||
render={arrayHelpers => (
|
render={arrayHelpers => (
|
||||||
<List>
|
<List>
|
||||||
{formik.values.websites.map(
|
{formik.values.websites.map(
|
||||||
(
|
(website, index) => (
|
||||||
website,
|
<ListItem key={website.url}>
|
||||||
index,
|
|
||||||
) => (
|
|
||||||
<ListItem
|
|
||||||
key={
|
|
||||||
website.url
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FaviconImage
|
<FaviconImage
|
||||||
url={
|
url={website.url}
|
||||||
website.url
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
{
|
{website.url}
|
||||||
website.url
|
|
||||||
}
|
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -309,45 +264,24 @@ export default function AliasNotesForm({
|
|||||||
/>
|
/>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : notes.data.websites.length ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
{notes.data.websites.length ? (
|
<List>
|
||||||
<List>
|
{notes.data.websites.map(website => (
|
||||||
{notes.data.websites.map(
|
<ListItem key={website.url}>
|
||||||
website => (
|
<ListItemIcon>
|
||||||
<ListItem
|
<FaviconImage
|
||||||
key={
|
width={20}
|
||||||
website.url
|
url={website.url}
|
||||||
}
|
/>
|
||||||
>
|
</ListItemIcon>
|
||||||
<ListItemIcon>
|
<ListItemText>{website.url}</ListItemText>
|
||||||
<FaviconImage
|
</ListItem>
|
||||||
width={
|
))}
|
||||||
20
|
</List>
|
||||||
}
|
|
||||||
url={
|
|
||||||
website.url
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
{
|
|
||||||
website.url
|
|
||||||
}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2">
|
|
||||||
You haven't used this
|
|
||||||
alias on any site yet.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
) : null}
|
||||||
</Grid>
|
</SimpleOverlayInformation>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -355,7 +289,7 @@ export default function AliasNotesForm({
|
|||||||
</form>
|
</form>
|
||||||
<ErrorSnack message={formik.errors.detail} />
|
<ErrorSnack message={formik.errors.detail} />
|
||||||
<SuccessSnack
|
<SuccessSnack
|
||||||
message={isSuccess && "Updated notes successfully!"}
|
message={isSuccess && t("relations.alias.mutations.success.notesUpdated")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -5,26 +5,22 @@ import {useFormik} from "formik"
|
|||||||
import {FaFile} from "react-icons/fa"
|
import {FaFile} from "react-icons/fa"
|
||||||
import {MdCheckCircle} from "react-icons/md"
|
import {MdCheckCircle} from "react-icons/md"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {LoadingButton} from "@mui/lab"
|
import {LoadingButton} from "@mui/lab"
|
||||||
import {Collapse, Grid} from "@mui/material"
|
import {Collapse, Grid, Typography} from "@mui/material"
|
||||||
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import Icon from "@mdi/react"
|
import Icon from "@mdi/react"
|
||||||
|
|
||||||
import {
|
import {Alias, DecryptedAlias, ImageProxyFormatType, ProxyUserAgentType} from "~/server-types"
|
||||||
Alias,
|
|
||||||
DecryptedAlias,
|
|
||||||
ImageProxyFormatType,
|
|
||||||
ProxyUserAgentType,
|
|
||||||
} from "~/server-types"
|
|
||||||
import {
|
|
||||||
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
|
|
||||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
|
||||||
} from "~/constants/enum_mappings"
|
|
||||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
import {ErrorSnack, SuccessSnack} from "~/components"
|
import {ErrorSnack, SuccessSnack} from "~/components"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
|
import {
|
||||||
|
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
|
||||||
|
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
||||||
|
} from "~/constants/enum-mappings"
|
||||||
import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation"
|
import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation"
|
||||||
import SelectField from "~/route-widgets/SettingsRoute/SelectField"
|
import SelectField from "~/route-widgets/SettingsRoute/SelectField"
|
||||||
|
|
||||||
@ -44,29 +40,36 @@ interface Form {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA = yup.object().shape({
|
|
||||||
removeTrackers: yup.mixed<boolean | null>().oneOf([true, false, null]),
|
|
||||||
createMailReport: yup.mixed<boolean | null>().oneOf([true, false, null]),
|
|
||||||
proxyImages: yup.mixed<boolean | null>().oneOf([true, false, null]),
|
|
||||||
imageProxyFormat: yup
|
|
||||||
.mixed<ImageProxyFormatType>()
|
|
||||||
.oneOf([null, ...Object.values(ImageProxyFormatType)]),
|
|
||||||
imageProxyUserAgent: yup
|
|
||||||
.mixed<ProxyUserAgentType>()
|
|
||||||
.oneOf([null, ...Object.values(ProxyUserAgentType)]),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function AliasPreferencesForm({
|
export default function AliasPreferencesForm({
|
||||||
alias,
|
alias,
|
||||||
onChanged,
|
onChanged,
|
||||||
}: AliasPreferencesFormProps): ReactElement {
|
}: AliasPreferencesFormProps): ReactElement {
|
||||||
const {mutateAsync, isSuccess} = useMutation<
|
const {t} = useTranslation()
|
||||||
Alias,
|
const SCHEMA = yup.object().shape({
|
||||||
AxiosError,
|
removeTrackers: yup
|
||||||
UpdateAliasData
|
.mixed<boolean | null>()
|
||||||
>(data => updateAlias(alias.id, data), {
|
.oneOf([true, false, null])
|
||||||
onSuccess: onChanged,
|
.label(t("relations.alias.settings.removeTrackers.label")),
|
||||||
|
createMailReport: yup
|
||||||
|
.mixed<boolean | null>()
|
||||||
|
.oneOf([true, false, null])
|
||||||
|
.label(t("relations.alias.settings.createMailReport.label")),
|
||||||
|
proxyImages: yup.mixed<boolean | null>().oneOf([true, false, null]),
|
||||||
|
imageProxyFormat: yup
|
||||||
|
.mixed<ImageProxyFormatType>()
|
||||||
|
.oneOf([null, ...Object.values(ImageProxyFormatType)])
|
||||||
|
.label(t("relations.alias.settings.imageProxyFormat.label")),
|
||||||
|
imageProxyUserAgent: yup
|
||||||
|
.mixed<ProxyUserAgentType>()
|
||||||
|
.oneOf([null, ...Object.values(ProxyUserAgentType)])
|
||||||
|
.label(t("relations.alias.settings.imageProxyUserAgent.label")),
|
||||||
})
|
})
|
||||||
|
const {mutateAsync, isSuccess} = useMutation<Alias, AxiosError, UpdateAliasData>(
|
||||||
|
data => updateAlias(alias.id, data),
|
||||||
|
{
|
||||||
|
onSuccess: onChanged,
|
||||||
|
},
|
||||||
|
)
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -97,6 +100,7 @@ export default function AliasPreferencesForm({
|
|||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
|
marginTop={1}
|
||||||
spacing={4}
|
spacing={4}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
@ -105,7 +109,7 @@ export default function AliasPreferencesForm({
|
|||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Remove Trackers"
|
label={t("relations.alias.settings.removeTrackers.label")}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
icon={<BsShieldShaded />}
|
icon={<BsShieldShaded />}
|
||||||
name="removeTrackers"
|
name="removeTrackers"
|
||||||
@ -113,14 +117,9 @@ export default function AliasPreferencesForm({
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Create Reports"
|
label={t("relations.alias.settings.createMailReports.label")}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
icon={
|
icon={<Icon path={mdiTextBoxMultiple} size={0.8} />}
|
||||||
<Icon
|
|
||||||
path={mdiTextBoxMultiple}
|
|
||||||
size={0.8}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="createMailReport"
|
name="createMailReport"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -128,23 +127,20 @@ export default function AliasPreferencesForm({
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Proxy Images"
|
label={t("relations.alias.settings.proxyImages.label")}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
icon={<BsImage />}
|
icon={<BsImage />}
|
||||||
name="proxyImages"
|
name="proxyImages"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Collapse
|
<Collapse in={formik.values.proxyImages !== false}>
|
||||||
in={
|
|
||||||
formik.values.proxyImages !==
|
|
||||||
false
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Image File Type"
|
label={t(
|
||||||
|
"relations.alias.settings.imageProxyFormat.label",
|
||||||
|
)}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
icon={<FaFile />}
|
icon={<FaFile />}
|
||||||
name="imageProxyFormat"
|
name="imageProxyFormat"
|
||||||
@ -155,7 +151,9 @@ export default function AliasPreferencesForm({
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Image Proxy User Agent"
|
label={t(
|
||||||
|
"relations.alias.settings.imageProxyUserAgent.label",
|
||||||
|
)}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
name="imageProxyUserAgent"
|
name="imageProxyUserAgent"
|
||||||
valueTextMap={
|
valueTextMap={
|
||||||
@ -177,15 +175,20 @@ export default function AliasPreferencesForm({
|
|||||||
type="submit"
|
type="submit"
|
||||||
startIcon={<MdCheckCircle />}
|
startIcon={<MdCheckCircle />}
|
||||||
>
|
>
|
||||||
Save Settings
|
{t("relations.alias.settings.saveAction")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{t("routes.AliasDetailRoute.sections.settings.description")}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
<FormikAutoLockNavigation formik={formik} />
|
<FormikAutoLockNavigation formik={formik} />
|
||||||
<ErrorSnack message={formik.errors.detail} />
|
<ErrorSnack message={formik.errors.detail} />
|
||||||
<SuccessSnack
|
<SuccessSnack
|
||||||
message={isSuccess && "Updated Alias successfully!"}
|
message={isSuccess && t("relations.alias.mutations.success.aliasUpdated")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {ReactElement, useContext, useEffect, useState} from "react"
|
import {ReactElement, useContext, useState} from "react"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Switch} from "@mui/material"
|
import {Switch} from "@mui/material"
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
@ -8,6 +9,7 @@ import {Alias, DecryptedAlias} from "~/server-types"
|
|||||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {ErrorSnack, SuccessSnack} from "~/components"
|
import {ErrorSnack, SuccessSnack} from "~/components"
|
||||||
|
import {useUIState} from "~/hooks"
|
||||||
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
||||||
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
||||||
|
|
||||||
@ -23,10 +25,11 @@ export default function ChangeAliasActivationStatusSwitch({
|
|||||||
isActive,
|
isActive,
|
||||||
onChanged,
|
onChanged,
|
||||||
}: ChangeAliasActivationStatusSwitchProps): ReactElement {
|
}: ChangeAliasActivationStatusSwitchProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const {_decryptUsingMasterPassword, encryptionStatus} =
|
const {_decryptUsingMasterPassword, encryptionStatus} =
|
||||||
useContext(AuthContext)
|
useContext(AuthContext)
|
||||||
|
|
||||||
const [isActiveUIState, setIsActiveUIState] = useState<boolean>(true)
|
const [isActiveUIState, setIsActiveUIState] = useUIState<boolean>(isActive)
|
||||||
|
|
||||||
const [successMessage, setSuccessMessage] = useState<string>("")
|
const [successMessage, setSuccessMessage] = useState<string>("")
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("")
|
const [errorMessage, setErrorMessage] = useState<string>("")
|
||||||
@ -50,10 +53,6 @@ export default function ChangeAliasActivationStatusSwitch({
|
|||||||
setErrorMessage(parseFastAPIError(error).detail as string),
|
setErrorMessage(parseFastAPIError(error).detail as string),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsActiveUIState(isActive)
|
|
||||||
}, [isActive])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Switch
|
<Switch
|
||||||
@ -68,9 +67,17 @@ export default function ChangeAliasActivationStatusSwitch({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!isActiveUIState) {
|
if (!isActiveUIState) {
|
||||||
setSuccessMessage("Alias activated successfully!")
|
setSuccessMessage(
|
||||||
|
t(
|
||||||
|
"relations.alias.mutations.success.aliasChangedToEnabled",
|
||||||
|
) as string,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setSuccessMessage("Alias deactivated successfully!")
|
setSuccessMessage(
|
||||||
|
t(
|
||||||
|
"relations.alias.mutations.success.aliasChangedToDisabled",
|
||||||
|
) as string,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
|
@ -21,6 +21,7 @@ import {Alias, AliasType} from "~/server-types"
|
|||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {ErrorSnack, SuccessSnack} from "~/components"
|
import {ErrorSnack, SuccessSnack} from "~/components"
|
||||||
import {DEFAULT_ALIAS_NOTE} from "~/constants/values"
|
import {DEFAULT_ALIAS_NOTE} from "~/constants/values"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
||||||
import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog"
|
import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog"
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export interface CreateAliasButtonProps {
|
|||||||
export default function CreateAliasButton({
|
export default function CreateAliasButton({
|
||||||
onCreated,
|
onCreated,
|
||||||
}: CreateAliasButtonProps): ReactElement {
|
}: CreateAliasButtonProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const {_encryptUsingMasterPassword, encryptionStatus} =
|
const {_encryptUsingMasterPassword, encryptionStatus} =
|
||||||
useContext(AuthContext)
|
useContext(AuthContext)
|
||||||
|
|
||||||
@ -83,7 +85,7 @@ export default function CreateAliasButton({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Create random alias
|
{t("routes.AliasesRoute.actions.createRandomAlias.label")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@ -108,7 +110,11 @@ export default function CreateAliasButton({
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FaPen />
|
<FaPen />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Create Custom Alias" />
|
<ListItemText
|
||||||
|
primary={t(
|
||||||
|
"routes.AliasesRoute.actions.createCustomAlias.label",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
@ -123,7 +129,10 @@ export default function CreateAliasButton({
|
|||||||
/>
|
/>
|
||||||
<ErrorSnack message={errorMessage} />
|
<ErrorSnack message={errorMessage} />
|
||||||
<SuccessSnack
|
<SuccessSnack
|
||||||
message={isSuccess && "Created Alias successfully!"}
|
message={
|
||||||
|
isSuccess &&
|
||||||
|
t("relations.alias.mutations.success.aliasCreation")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,7 @@ import {useLoaderData} from "react-router-dom"
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {TiCancel} from "react-icons/ti"
|
import {TiCancel} from "react-icons/ti"
|
||||||
import {FaPen} from "react-icons/fa"
|
import {FaPen} from "react-icons/fa"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -44,6 +45,7 @@ export default function CustomAliasDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
}: CustomAliasDialogProps): ReactElement {
|
}: CustomAliasDialogProps): ReactElement {
|
||||||
const serverSettings = useLoaderData() as ServerSettings
|
const serverSettings = useLoaderData() as ServerSettings
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
const schema = yup.object().shape({
|
||||||
local: yup
|
local: yup
|
||||||
@ -75,11 +77,14 @@ export default function CustomAliasDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} open={visible} keepMounted={false}>
|
<Dialog onClose={onClose} open={visible} keepMounted={false}>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<DialogTitle>Create Custom Alias</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t("routes.AliasesRoute.actions.createCustomAlias.label")}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
You can define your own custom alias. Note that a random
|
{t(
|
||||||
suffix will be added at the end to avoid duplicates.
|
"routes.AliasesRoute.actions.createCustomAlias.description",
|
||||||
|
)}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<Box paddingY={4}>
|
<Box paddingY={4}>
|
||||||
<TextField
|
<TextField
|
||||||
@ -88,7 +93,12 @@ export default function CustomAliasDialog({
|
|||||||
autoFocus
|
autoFocus
|
||||||
name="local"
|
name="local"
|
||||||
id="local"
|
id="local"
|
||||||
label="Address"
|
label={t(
|
||||||
|
"routes.AliasesRoute.actions.createCustomAlias.form.address.label",
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.AliasesRoute.actions.createCustomAlias.form.address.placeholder",
|
||||||
|
)}
|
||||||
value={formik.values.local}
|
value={formik.values.local}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
@ -122,7 +132,7 @@ export default function CustomAliasDialog({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} startIcon={<TiCancel />}>
|
<Button onClick={onClose} startIcon={<TiCancel />}>
|
||||||
Cancel
|
{t("general.cancelLabel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
@ -131,7 +141,9 @@ export default function CustomAliasDialog({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create Alias
|
{t(
|
||||||
|
"routes.AliasesRoute.actions.createCustomAlias.continueAction",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</form>
|
</form>
|
||||||
|
@ -6,6 +6,7 @@ import {Link as RouterLink, useLocation} from "react-router-dom"
|
|||||||
|
|
||||||
import {Button} from "@mui/material"
|
import {Button} from "@mui/material"
|
||||||
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import Icon from "@mdi/react"
|
import Icon from "@mdi/react"
|
||||||
import LockNavigationContext from "~/LockNavigationContext/LockNavigationContext"
|
import LockNavigationContext from "~/LockNavigationContext/LockNavigationContext"
|
||||||
|
|
||||||
@ -28,10 +29,10 @@ const SECTION_ICON_MAP: Record<NavigationSection, ReactElement> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SECTION_TEXT_MAP: Record<NavigationSection, string> = {
|
const SECTION_TEXT_MAP: Record<NavigationSection, string> = {
|
||||||
[NavigationSection.Overview]: "Overview",
|
[NavigationSection.Overview]: "components.NavigationButton.overview",
|
||||||
[NavigationSection.Aliases]: "Aliases",
|
[NavigationSection.Aliases]: "components.NavigationButton.aliases",
|
||||||
[NavigationSection.Reports]: "Reports",
|
[NavigationSection.Reports]: "components.NavigationButton.reports",
|
||||||
[NavigationSection.Settings]: "Settings",
|
[NavigationSection.Settings]: "components.NavigationButton.settings",
|
||||||
}
|
}
|
||||||
|
|
||||||
const PATH_SECTION_MAP: Record<string, NavigationSection> = {
|
const PATH_SECTION_MAP: Record<string, NavigationSection> = {
|
||||||
@ -44,12 +45,13 @@ const PATH_SECTION_MAP: Record<string, NavigationSection> = {
|
|||||||
export default function NavigationButton({
|
export default function NavigationButton({
|
||||||
section,
|
section,
|
||||||
}: NavigationButtonProps): ReactElement {
|
}: NavigationButtonProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const {handleAnchorClick} = useContext(LockNavigationContext)
|
const {handleAnchorClick} = useContext(LockNavigationContext)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const currentSection = PATH_SECTION_MAP[location.pathname.split("/")[1]]
|
const currentSection = PATH_SECTION_MAP[location.pathname.split("/")[1]]
|
||||||
const Icon = SECTION_ICON_MAP[section]
|
const Icon = SECTION_ICON_MAP[section]
|
||||||
const text = SECTION_TEXT_MAP[section]
|
const text = t(SECTION_TEXT_MAP[section])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -5,6 +5,7 @@ import React, {ReactElement} from "react"
|
|||||||
import {Box, Button, Grid, Typography} from "@mui/material"
|
import {Box, Button, Grid, Typography} from "@mui/material"
|
||||||
import {MultiStepFormElement} from "~/components"
|
import {MultiStepFormElement} from "~/components"
|
||||||
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
import {mdiTextBoxMultiple} from "@mdi/js/commonjs/mdi"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import Icon from "@mdi/react"
|
import Icon from "@mdi/react"
|
||||||
|
|
||||||
export interface GenerateEmailReportsFormProps {
|
export interface GenerateEmailReportsFormProps {
|
||||||
@ -16,6 +17,8 @@ export default function GenerateEmailReportsForm({
|
|||||||
onNo,
|
onNo,
|
||||||
onYes,
|
onYes,
|
||||||
}: GenerateEmailReportsFormProps): ReactElement {
|
}: GenerateEmailReportsFormProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiStepFormElement>
|
<MultiStepFormElement>
|
||||||
<Grid
|
<Grid
|
||||||
@ -29,40 +32,26 @@ export default function GenerateEmailReportsForm({
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid
|
<Grid container direction="column" spacing={4} alignItems="center">
|
||||||
container
|
|
||||||
direction="column"
|
|
||||||
spacing={4}
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={4} direction="column">
|
<Grid container spacing={4} direction="column">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="h6" component="h2" align="center">
|
||||||
variant="h6"
|
{t(
|
||||||
component="h2"
|
"routes.CompleteAccountRoute.forms.generateReports.title",
|
||||||
align="center"
|
)}
|
||||||
>
|
|
||||||
Generate Email Reports?
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box display="flex" justifyContent="center">
|
<Box display="flex" justifyContent="center">
|
||||||
<Icon
|
<Icon path={mdiTextBoxMultiple} size={2} />
|
||||||
path={mdiTextBoxMultiple}
|
|
||||||
size={2}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="subtitle1" component="p">
|
||||||
variant="subtitle1"
|
{t(
|
||||||
component="p"
|
"routes.CompleteAccountRoute.forms.generateReports.description",
|
||||||
>
|
)}
|
||||||
Would you like to create fully encrypted
|
|
||||||
email reports for your mails? Only you
|
|
||||||
will be able to access it. Not even we
|
|
||||||
can decrypt it.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -72,12 +61,10 @@ export default function GenerateEmailReportsForm({
|
|||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={2} direction="row">
|
<Grid container spacing={2} direction="row">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button startIcon={<TiCancel />} color="secondary" onClick={onNo}>
|
||||||
startIcon={<TiCancel />}
|
{t(
|
||||||
color="secondary"
|
"routes.CompleteAccountRoute.forms.generateReports.cancelAction",
|
||||||
onClick={onNo}
|
)}
|
||||||
>
|
|
||||||
No
|
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -86,7 +73,9 @@ export default function GenerateEmailReportsForm({
|
|||||||
color="primary"
|
color="primary"
|
||||||
onClick={onYes}
|
onClick={onYes}
|
||||||
>
|
>
|
||||||
Yes
|
{t(
|
||||||
|
"routes.CompleteAccountRoute.forms.generateReports.continueAction",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -2,6 +2,8 @@ import * as yup from "yup"
|
|||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md"
|
import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md"
|
||||||
import {generateKey, readKey} from "openpgp"
|
import {generateKey, readKey} from "openpgp"
|
||||||
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement, useContext, useMemo} from "react"
|
import React, {ReactElement, useContext, useMemo} from "react"
|
||||||
import passwordGenerator from "secure-random-password"
|
import passwordGenerator from "secure-random-password"
|
||||||
|
|
||||||
@ -17,7 +19,6 @@ import {MASTER_PASSWORD_LENGTH} from "~/constants/values"
|
|||||||
import {AuthenticationDetails, UserNote} from "~/server-types"
|
import {AuthenticationDetails, UserNote} from "~/server-types"
|
||||||
import {UpdateAccountData, updateAccount} from "~/apis"
|
import {UpdateAccountData, updateAccount} from "~/apis"
|
||||||
import {encryptUserNote} from "~/utils/encrypt-user-note"
|
import {encryptUserNote} from "~/utils/encrypt-user-note"
|
||||||
import {AxiosError} from "axios"
|
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
|
|
||||||
export interface PasswordFormProps {
|
export interface PasswordFormProps {
|
||||||
@ -30,20 +31,25 @@ interface Form {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
export default function PasswordForm({onDone}: PasswordFormProps): ReactElement {
|
||||||
password: yup.string().required(),
|
const {t} = useTranslation()
|
||||||
passwordConfirmation: yup
|
|
||||||
.string()
|
|
||||||
.required()
|
|
||||||
.oneOf([yup.ref("password"), null], "Passwords must match"),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function PasswordForm({
|
|
||||||
onDone,
|
|
||||||
}: PasswordFormProps): ReactElement {
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const theme = useSystemPreferredTheme()
|
const theme = useSystemPreferredTheme()
|
||||||
|
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
password: yup.string().required(),
|
||||||
|
passwordConfirmation: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.oneOf(
|
||||||
|
[yup.ref("password"), null],
|
||||||
|
t(
|
||||||
|
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.mustMatchHelperText",
|
||||||
|
) as string,
|
||||||
|
)
|
||||||
|
.label(t("routes.CompleteAccountRoute.forms.password.form.passwordConfirm.label")),
|
||||||
|
})
|
||||||
|
|
||||||
const {_setDecryptionPassword, login} = useContext(AuthContext)
|
const {_setDecryptionPassword, login} = useContext(AuthContext)
|
||||||
|
|
||||||
const awaitGenerateKey = useMemo(
|
const awaitGenerateKey = useMemo(
|
||||||
@ -57,16 +63,15 @@ export default function PasswordForm({
|
|||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const {mutateAsync} = useMutation<
|
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, UpdateAccountData>(
|
||||||
AuthenticationDetails,
|
updateAccount,
|
||||||
AxiosError,
|
{
|
||||||
UpdateAccountData
|
onSuccess: ({user}) => {
|
||||||
>(updateAccount, {
|
login(user)
|
||||||
onSuccess: ({user}) => {
|
onDone()
|
||||||
login(user)
|
},
|
||||||
onDone()
|
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -85,10 +90,7 @@ export default function PasswordForm({
|
|||||||
values.password,
|
values.password,
|
||||||
user.email.address,
|
user.email.address,
|
||||||
)
|
)
|
||||||
const encryptedMasterPassword = encryptString(
|
const encryptedMasterPassword = encryptString(masterPassword, encryptionPassword)
|
||||||
masterPassword,
|
|
||||||
encryptionPassword,
|
|
||||||
)
|
|
||||||
const note: UserNote = {
|
const note: UserNote = {
|
||||||
theme,
|
theme,
|
||||||
privateKey: keyPair.privateKey,
|
privateKey: keyPair.privateKey,
|
||||||
@ -109,7 +111,7 @@ export default function PasswordForm({
|
|||||||
encryptedNotes,
|
encryptedNotes,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrors({detail: "An error occurred"})
|
setErrors({detail: t("general.defaultError")})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -128,18 +130,13 @@ export default function PasswordForm({
|
|||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={2} direction="column">
|
<Grid container spacing={2} direction="column">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="h6" component="h2" align="center">
|
||||||
variant="h6"
|
{t("routes.CompleteAccountRoute.forms.password.title")}
|
||||||
component="h2"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
Set up your password
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="subtitle1" component="p">
|
<Typography variant="subtitle1" component="p">
|
||||||
Please enter a safe password so that we can
|
{t("routes.CompleteAccountRoute.forms.password.description")}
|
||||||
encrypt your data.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -151,19 +148,20 @@ export default function PasswordForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label={t(
|
||||||
|
"routes.CompleteAccountRoute.forms.password.form.password.label",
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.CompleteAccountRoute.forms.password.form.password.placeholder",
|
||||||
|
)}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={formik.values.password}
|
value={formik.values.password}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={
|
||||||
formik.touched.password &&
|
formik.touched.password && Boolean(formik.errors.password)
|
||||||
Boolean(formik.errors.password)
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
formik.touched.password &&
|
|
||||||
formik.errors.password
|
|
||||||
}
|
}
|
||||||
|
helperText={formik.touched.password && formik.errors.password}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -178,15 +176,18 @@ export default function PasswordForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
id="passwordConfirmation"
|
id="passwordConfirmation"
|
||||||
name="passwordConfirmation"
|
name="passwordConfirmation"
|
||||||
label="Confirm Password"
|
label={t(
|
||||||
|
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.label",
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.placeholder",
|
||||||
|
)}
|
||||||
value={formik.values.passwordConfirmation}
|
value={formik.values.passwordConfirmation}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={
|
||||||
formik.touched.passwordConfirmation &&
|
formik.touched.passwordConfirmation &&
|
||||||
Boolean(
|
Boolean(formik.errors.passwordConfirmation)
|
||||||
formik.errors.passwordConfirmation,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
helperText={
|
helperText={
|
||||||
formik.touched.passwordConfirmation &&
|
formik.touched.passwordConfirmation &&
|
||||||
@ -210,7 +211,7 @@ export default function PasswordForm({
|
|||||||
loading={formik.isSubmitting}
|
loading={formik.isSubmitting}
|
||||||
startIcon={<MdChevronRight />}
|
startIcon={<MdChevronRight />}
|
||||||
>
|
>
|
||||||
Continue
|
{t("routes.CompleteAccountRoute.forms.password.continueAction")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
import {FaHashtag} from "react-icons/fa"
|
import {FaHashtag} from "react-icons/fa"
|
||||||
import {MdChevronRight, MdMail} from "react-icons/md"
|
import {MdChevronRight, MdMail} from "react-icons/md"
|
||||||
|
|
||||||
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
import ResendMailButton from "./ResendMailButton"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import {Box, Grid, InputAdornment, TextField, Typography} from "@mui/material"
|
import {Box, Grid, InputAdornment, TextField, Typography} from "@mui/material"
|
||||||
import {LoadingButton} from "@mui/lab"
|
import {LoadingButton} from "@mui/lab"
|
||||||
|
|
||||||
import {AuthenticationDetails, ServerUser} from "~/server-types"
|
import {AuthenticationDetails, ServerSettings, ServerUser} from "~/server-types"
|
||||||
import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis"
|
import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis"
|
||||||
import {MultiStepFormElement} from "~/components"
|
import {MultiStepFormElement} from "~/components"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
|
|
||||||
import ResendMailButton from "./ResendMailButton"
|
|
||||||
import useSchema from "./use-schema"
|
|
||||||
|
|
||||||
export interface ConfirmCodeFormProps {
|
export interface ConfirmCodeFormProps {
|
||||||
onConfirm: (user: ServerUser) => void
|
onConfirm: (user: ServerUser) => void
|
||||||
email: string
|
email: string
|
||||||
@ -32,16 +34,36 @@ export default function ConfirmCodeForm({
|
|||||||
email,
|
email,
|
||||||
sameRequestToken,
|
sameRequestToken,
|
||||||
}: ConfirmCodeFormProps): ReactElement {
|
}: ConfirmCodeFormProps): ReactElement {
|
||||||
const schema = useSchema()
|
const settings = useLoaderData() as ServerSettings
|
||||||
const {mutateAsync} = useMutation<
|
const {t} = useTranslation()
|
||||||
AuthenticationDetails,
|
const SCHEMA = yup.object().shape({
|
||||||
AxiosError,
|
code: yup
|
||||||
VerifyLoginWithEmailData
|
.string()
|
||||||
>(verifyLoginWithEmail, {
|
.required()
|
||||||
onSuccess: ({user}) => onConfirm(user),
|
.min(settings.emailLoginTokenLength)
|
||||||
|
.max(settings.emailLoginTokenLength)
|
||||||
|
.test(
|
||||||
|
"chars",
|
||||||
|
t("routes.LoginRoute.forms.confirmCode.form.code.errors.invalidChars") as string,
|
||||||
|
code => {
|
||||||
|
if (!code) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const chars = settings.emailLoginTokenChars.split("")
|
||||||
|
|
||||||
|
return code.split("").every(char => chars.includes(char))
|
||||||
|
},
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyLoginWithEmailData>(
|
||||||
|
verifyLoginWithEmail,
|
||||||
|
{
|
||||||
|
onSuccess: ({user}) => onConfirm(user),
|
||||||
|
},
|
||||||
|
)
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: schema,
|
validationSchema: SCHEMA,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
code: "",
|
code: "",
|
||||||
detail: "",
|
detail: "",
|
||||||
@ -54,7 +76,8 @@ export default function ConfirmCodeForm({
|
|||||||
token: values.code,
|
token: values.code,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrors(parseFastAPIError(error as AxiosError))
|
const errors = parseFastAPIError(error as AxiosError)
|
||||||
|
setErrors({code: errors.detail})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -71,7 +94,7 @@ export default function ConfirmCodeForm({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h1" align="center">
|
<Typography variant="h6" component="h1" align="center">
|
||||||
You got mail!
|
{t("routes.LoginRoute.forms.confirmCode.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -80,13 +103,8 @@ export default function ConfirmCodeForm({
|
|||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
variant="subtitle1"
|
{t("routes.LoginRoute.forms.confirmCode.description")}
|
||||||
component="p"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
We sent you a code to your email. Enter it below to
|
|
||||||
login.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -95,17 +113,12 @@ export default function ConfirmCodeForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
name="code"
|
name="code"
|
||||||
id="code"
|
id="code"
|
||||||
label="Code"
|
label={t("routes.LoginRoute.forms.confirmCode.form.code.label")}
|
||||||
value={formik.values.code}
|
value={formik.values.code}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={formik.touched.code && Boolean(formik.errors.code)}
|
||||||
formik.touched.code &&
|
helperText={formik.touched.code && formik.errors.code}
|
||||||
Boolean(formik.errors.code)
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
formik.touched.code && formik.errors.code
|
|
||||||
}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -116,12 +129,7 @@ export default function ConfirmCodeForm({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid
|
<Grid width="100%" container display="flex" justifyContent="space-between">
|
||||||
width="100%"
|
|
||||||
container
|
|
||||||
display="flex"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<ResendMailButton
|
<ResendMailButton
|
||||||
email={email}
|
email={email}
|
||||||
@ -135,7 +143,7 @@ export default function ConfirmCodeForm({
|
|||||||
type="submit"
|
type="submit"
|
||||||
startIcon={<MdChevronRight />}
|
startIcon={<MdChevronRight />}
|
||||||
>
|
>
|
||||||
Login
|
{t("routes.LoginRoute.forms.confirmCode.continueAction")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -8,6 +8,7 @@ import {resendEmailLoginCode} from "~/apis"
|
|||||||
import {MutationStatusSnackbar, TimedButton} from "~/components"
|
import {MutationStatusSnackbar, TimedButton} from "~/components"
|
||||||
import {ServerSettings, SimpleDetailResponse} from "~/server-types"
|
import {ServerSettings, SimpleDetailResponse} from "~/server-types"
|
||||||
import {MdMail} from "react-icons/md"
|
import {MdMail} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface ResendMailButtonProps {
|
export interface ResendMailButtonProps {
|
||||||
email: string
|
email: string
|
||||||
@ -19,6 +20,7 @@ export default function ResendMailButton({
|
|||||||
sameRequestToken,
|
sameRequestToken,
|
||||||
}: ResendMailButtonProps): ReactElement {
|
}: ResendMailButtonProps): ReactElement {
|
||||||
const settings = useLoaderData() as ServerSettings
|
const settings = useLoaderData() as ServerSettings
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
||||||
resendEmailLoginCode({
|
resendEmailLoginCode({
|
||||||
@ -35,7 +37,7 @@ export default function ResendMailButton({
|
|||||||
startIcon={<MdMail />}
|
startIcon={<MdMail />}
|
||||||
onClick={() => mutate()}
|
onClick={() => mutate()}
|
||||||
>
|
>
|
||||||
Resend Mail
|
{t("components.ResendMailButton.label")}
|
||||||
</TimedButton>
|
</TimedButton>
|
||||||
<MutationStatusSnackbar mutation={mutation} />
|
<MutationStatusSnackbar mutation={mutation} />
|
||||||
</>
|
</>
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import * as yup from "yup"
|
|
||||||
import {useLoaderData} from "react-router-dom"
|
|
||||||
|
|
||||||
import {ServerSettings} from "~/server-types"
|
|
||||||
|
|
||||||
export default function useSchema(): yup.ObjectSchema<any> {
|
|
||||||
const settings = useLoaderData() as ServerSettings
|
|
||||||
|
|
||||||
return yup.object().shape({
|
|
||||||
code: yup
|
|
||||||
.string()
|
|
||||||
.required()
|
|
||||||
.min(settings.emailLoginTokenLength)
|
|
||||||
.max(settings.emailLoginTokenLength)
|
|
||||||
.test("chars", "This code is not valid.", code => {
|
|
||||||
if (!code) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const chars = settings.emailLoginTokenChars.split("")
|
|
||||||
|
|
||||||
return code.split("").every(char => chars.includes(char))
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ import {ReactElement} from "react"
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
import {MdEmail} from "react-icons/md"
|
import {MdEmail} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import {InputAdornment, TextField} from "@mui/material"
|
import {InputAdornment, TextField} from "@mui/material"
|
||||||
@ -20,18 +21,18 @@ interface Form {
|
|||||||
detail: string
|
detail: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA = yup.object().shape({
|
|
||||||
email: yup.string().email().required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
|
export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
|
||||||
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(
|
const {t} = useTranslation()
|
||||||
loginWithEmail,
|
const SCHEMA = yup.object().shape({
|
||||||
{
|
email: yup
|
||||||
onSuccess: ({sameRequestToken}) =>
|
.string()
|
||||||
onLogin(formik.values.email, sameRequestToken),
|
.email()
|
||||||
},
|
.required()
|
||||||
)
|
.label(t("routes.LoginRoute.forms.email.form.email.label")),
|
||||||
|
})
|
||||||
|
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, {
|
||||||
|
onSuccess: ({sameRequestToken}) => onLogin(formik.values.email, sameRequestToken),
|
||||||
|
})
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: SCHEMA,
|
validationSchema: SCHEMA,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -51,9 +52,9 @@ export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
|
|||||||
<MultiStepFormElement>
|
<MultiStepFormElement>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<SimpleForm
|
<SimpleForm
|
||||||
title="Sign in"
|
title={t("routes.LoginRoute.forms.email.title")}
|
||||||
description="We'll send you a verification code to your email."
|
description={t("routes.LoginRoute.forms.email.description")}
|
||||||
continueActionLabel="Send code"
|
continueActionLabel={t("routes.LoginRoute.forms.email.continueAction")}
|
||||||
nonFieldError={formik.errors.detail}
|
nonFieldError={formik.errors.detail}
|
||||||
isSubmitting={formik.isSubmitting}
|
isSubmitting={formik.isSubmitting}
|
||||||
>
|
>
|
||||||
@ -64,17 +65,13 @@ export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
|
|||||||
name="email"
|
name="email"
|
||||||
id="email"
|
id="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
placeholder={t("routes.LoginRoute.forms.email.form.email.placeholder")}
|
||||||
inputMode="email"
|
inputMode="email"
|
||||||
value={formik.values.email}
|
value={formik.values.email}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={formik.touched.email && Boolean(formik.errors.email)}
|
||||||
formik.touched.email &&
|
helperText={formik.touched.email && formik.errors.email}
|
||||||
Boolean(formik.errors.email)
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
formik.touched.email && formik.errors.email
|
|
||||||
}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
|
@ -2,6 +2,7 @@ import {BsImage} from "react-icons/bs"
|
|||||||
import {ReactElement, useState} from "react"
|
import {ReactElement, useState} from "react"
|
||||||
import {MdLocationOn} from "react-icons/md"
|
import {MdLocationOn} from "react-icons/md"
|
||||||
import {useLoaderData} from "react-router-dom"
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import addHours from "date-fns/addHours"
|
import addHours from "date-fns/addHours"
|
||||||
import isBefore from "date-fns/isBefore"
|
import isBefore from "date-fns/isBefore"
|
||||||
|
|
||||||
@ -17,14 +18,14 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
|
||||||
import {DecryptedReportContent, ServerSettings} from "~/server-types"
|
import {DecryptedReportContent, ServerSettings} from "~/server-types"
|
||||||
|
import {isDev} from "~/constants/development"
|
||||||
|
|
||||||
export interface ProxiedImagesListItemProps {
|
export interface ProxiedImagesListItemProps {
|
||||||
images: DecryptedReportContent["messageDetails"]["content"]["proxiedImages"]
|
images: DecryptedReportContent["messageDetails"]["content"]["proxiedImages"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProxiedImagesListItem({
|
export default function ProxiedImagesListItem({images}: ProxiedImagesListItemProps): ReactElement {
|
||||||
images,
|
const {t} = useTranslation()
|
||||||
}: ProxiedImagesListItemProps): ReactElement {
|
|
||||||
const serverSettings = useLoaderData() as ServerSettings
|
const serverSettings = useLoaderData() as ServerSettings
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
@ -42,14 +43,18 @@ export default function ProxiedImagesListItem({
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<BsImage />
|
<BsImage />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Proxying {images.length} images</ListItemText>
|
<ListItemText>
|
||||||
|
{t("routes.ReportDetailRoute.sections.trackers.results.proxiedImages.text", {
|
||||||
|
count: images.length,
|
||||||
|
})}
|
||||||
|
</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
<Collapse in={showProxiedImages}>
|
<Collapse in={showProxiedImages}>
|
||||||
<Box bgcolor={theme.palette.background.default}>
|
<Box bgcolor={theme.palette.background.default}>
|
||||||
<List>
|
<List>
|
||||||
{images.map(image => (
|
{images.map(image => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
href={image.serverUrl}
|
href={isDev ? image.url : image.serverUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
key={image.imageProxyId}
|
key={image.imageProxyId}
|
||||||
>
|
>
|
||||||
@ -79,9 +84,13 @@ export default function ProxiedImagesListItem({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "Stored on Server."
|
return t(
|
||||||
|
"routes.ReportDetailRoute.sections.trackers.results.proxiedImages.status.isStored",
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return "Proxying through Server."
|
return t(
|
||||||
|
"routes.ReportDetailRoute.sections.trackers.results.proxiedImages.status.isProxying",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import {DecryptedReportContent} from "~/server-types"
|
import {DecryptedReportContent} from "~/server-types"
|
||||||
import {BsShieldShaded} from "react-icons/bs"
|
import {BsShieldShaded} from "react-icons/bs"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface SinglePixelImageTrackersListItemProps {
|
export interface SinglePixelImageTrackersListItemProps {
|
||||||
images: DecryptedReportContent["messageDetails"]["content"]["singlePixelImages"]
|
images: DecryptedReportContent["messageDetails"]["content"]["singlePixelImages"]
|
||||||
@ -22,6 +23,7 @@ export interface SinglePixelImageTrackersListItemProps {
|
|||||||
export default function SinglePixelImageTrackersListItem({
|
export default function SinglePixelImageTrackersListItem({
|
||||||
images,
|
images,
|
||||||
}: SinglePixelImageTrackersListItemProps): ReactElement {
|
}: SinglePixelImageTrackersListItemProps): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const [showImageTrackers, setShowImageTrackers] = useState<boolean>(false)
|
const [showImageTrackers, setShowImageTrackers] = useState<boolean>(false)
|
||||||
@ -45,30 +47,24 @@ export default function SinglePixelImageTrackersListItem({
|
|||||||
<BsShieldShaded />
|
<BsShieldShaded />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
Removed {images.length} image trackers
|
{t("routes.ReportDetailRoute.sections.trackers.results.imageTrackers.text", {
|
||||||
|
count: images.length,
|
||||||
|
})}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
<Collapse in={showImageTrackers}>
|
<Collapse in={showImageTrackers}>
|
||||||
<Box bgcolor={theme.palette.background.default}>
|
<Box bgcolor={theme.palette.background.default}>
|
||||||
<List>
|
<List>
|
||||||
{Object.entries(imagesPerTracker).map(
|
{Object.entries(imagesPerTracker).map(([trackerName, images]) => (
|
||||||
([trackerName, images]) => (
|
<>
|
||||||
<>
|
<Typography variant="caption" component="h3" ml={1}>
|
||||||
<Typography
|
{trackerName}
|
||||||
variant="caption"
|
</Typography>
|
||||||
component="h3"
|
{images.map(image => (
|
||||||
ml={1}
|
<ListItem key={image.source}>{image.source}</ListItem>
|
||||||
>
|
))}
|
||||||
{trackerName}
|
</>
|
||||||
</Typography>
|
))}
|
||||||
{images.map(image => (
|
|
||||||
<ListItem key={image.source}>
|
|
||||||
{image.source}
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
@ -4,6 +4,7 @@ import {useNavigate} from "react-router-dom"
|
|||||||
import {ListItemButton, ListItemText} from "@mui/material"
|
import {ListItemButton, ListItemText} from "@mui/material"
|
||||||
|
|
||||||
import {DecryptedReportContent} from "~/server-types"
|
import {DecryptedReportContent} from "~/server-types"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface ReportInformationItemProps {
|
export interface ReportInformationItemProps {
|
||||||
report: DecryptedReportContent
|
report: DecryptedReportContent
|
||||||
@ -13,16 +14,20 @@ export default function ReportInformationItem({
|
|||||||
report,
|
report,
|
||||||
}: ReportInformationItemProps): ReactElement {
|
}: ReportInformationItemProps): ReactElement {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton onClick={() => navigate(`/reports/${report.id}`)}>
|
<ListItemButton onClick={() => navigate(`/reports/${report.id}`)}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
report.messageDetails.content.subject ?? (
|
report.messageDetails.content.subject ?? (
|
||||||
<i>{"<No Subject>"}</i>
|
<i>{t("relations.report.emailMeta.emptySubject")}</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`}
|
secondary={t("relations.report.emailMeta.flow", {
|
||||||
|
from: report.messageDetails.meta.from,
|
||||||
|
to: report.messageDetails.meta.to,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ import * as yup from "yup"
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
import {MdCheckCircle, MdImage} from "react-icons/md"
|
import {MdCheckCircle, MdImage} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement, useContext} from "react"
|
import React, {ReactElement, useContext} from "react"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
@ -21,11 +22,7 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
import {LoadingButton} from "@mui/lab"
|
import {LoadingButton} from "@mui/lab"
|
||||||
|
|
||||||
import {
|
import {ImageProxyFormatType, ProxyUserAgentType, SimpleDetailResponse} from "~/server-types"
|
||||||
ImageProxyFormatType,
|
|
||||||
ProxyUserAgentType,
|
|
||||||
SimpleDetailResponse,
|
|
||||||
} from "~/server-types"
|
|
||||||
import {UpdatePreferencesData, updatePreferences} from "~/apis"
|
import {UpdatePreferencesData, updatePreferences} from "~/apis"
|
||||||
import {useUser} from "~/hooks"
|
import {useUser} from "~/hooks"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
@ -33,7 +30,7 @@ import {SuccessSnack} from "~/components"
|
|||||||
import {
|
import {
|
||||||
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
|
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
|
||||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
||||||
} from "~/constants/enum_mappings"
|
} from "~/constants/enum-mappings"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
import ErrorSnack from "~/components/ErrorSnack"
|
import ErrorSnack from "~/components/ErrorSnack"
|
||||||
|
|
||||||
@ -47,23 +44,26 @@ interface Form {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA = yup.object().shape({
|
|
||||||
removeTrackers: yup.boolean(),
|
|
||||||
createMailReport: yup.boolean(),
|
|
||||||
proxyImages: yup.boolean(),
|
|
||||||
imageProxyFormat: yup
|
|
||||||
.mixed<ImageProxyFormatType>()
|
|
||||||
.oneOf(Object.values(ImageProxyFormatType))
|
|
||||||
.required(),
|
|
||||||
imageProxyUserAgent: yup
|
|
||||||
.mixed<ProxyUserAgentType>()
|
|
||||||
.oneOf(Object.values(ProxyUserAgentType))
|
|
||||||
.required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function AliasesPreferencesForm(): ReactElement {
|
export default function AliasesPreferencesForm(): ReactElement {
|
||||||
const {_updateUser} = useContext(AuthContext)
|
const {_updateUser} = useContext(AuthContext)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const SCHEMA = yup.object().shape({
|
||||||
|
removeTrackers: yup.boolean().label(t("relations.alias.settings.removeTrackers.label")),
|
||||||
|
createMailReport: yup.boolean().label(t("relations.alias.settings.createMailReport.label")),
|
||||||
|
proxyImages: yup.boolean().label(t("relations.alias.settings.proxyImages.label")),
|
||||||
|
imageProxyFormat: yup
|
||||||
|
.mixed<ImageProxyFormatType>()
|
||||||
|
.oneOf(Object.values(ImageProxyFormatType))
|
||||||
|
.required()
|
||||||
|
.label(t("relations.alias.settings.imageProxyFormat.label")),
|
||||||
|
imageProxyUserAgent: yup
|
||||||
|
.mixed<ProxyUserAgentType>()
|
||||||
|
.oneOf(Object.values(ProxyUserAgentType))
|
||||||
|
.required()
|
||||||
|
.label(t("relations.alias.settings.imageProxyUserAgent.label")),
|
||||||
|
})
|
||||||
|
|
||||||
const {mutateAsync, data} = useMutation<
|
const {mutateAsync, data} = useMutation<
|
||||||
SimpleDetailResponse,
|
SimpleDetailResponse,
|
||||||
AxiosError,
|
AxiosError,
|
||||||
@ -110,22 +110,15 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<Grid
|
<Grid container spacing={4} flexDirection="column" alignItems="center">
|
||||||
container
|
|
||||||
spacing={4}
|
|
||||||
flexDirection="column"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h3">
|
<Typography variant="h6" component="h3">
|
||||||
Aliases Preferences
|
{t("routes.SettingsRoute.forms.aliasPreferences.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="body1" component="p">
|
<Typography variant="body1" component="p">
|
||||||
Select the default behavior for your aliases. This
|
{t("routes.SettingsRoute.forms.aliasPreferences.description")}
|
||||||
will only affect aliases that do not have a custom
|
|
||||||
behavior set.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -136,7 +129,7 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
spacing={4}
|
spacing={4}
|
||||||
alignItems="flex-end"
|
alignItems="flex-end"
|
||||||
>
|
>
|
||||||
<Grid item md={6}>
|
<Grid item md={6} xs={12}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
@ -144,9 +137,7 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
name="removeTrackers"
|
name="removeTrackers"
|
||||||
id="removeTrackers"
|
id="removeTrackers"
|
||||||
checked={
|
checked={formik.values.removeTrackers}
|
||||||
formik.values.removeTrackers
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
/>
|
/>
|
||||||
@ -162,11 +153,11 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
>
|
>
|
||||||
{(formik.touched.createMailReport &&
|
{(formik.touched.createMailReport &&
|
||||||
formik.errors.createMailReport) ||
|
formik.errors.createMailReport) ||
|
||||||
"Remove single-pixel image trackers as well as url trackers."}
|
t("relations.alias.settings.removeTrackers.helperText")}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6}>
|
<Grid item md={6} xs={12}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
@ -174,10 +165,7 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
name="createMailReport"
|
name="createMailReport"
|
||||||
id="createMailReport"
|
id="createMailReport"
|
||||||
checked={
|
checked={formik.values.createMailReport}
|
||||||
formik.values
|
|
||||||
.createMailReport
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
/>
|
/>
|
||||||
@ -193,7 +181,9 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
>
|
>
|
||||||
{(formik.touched.createMailReport &&
|
{(formik.touched.createMailReport &&
|
||||||
formik.errors.createMailReport) ||
|
formik.errors.createMailReport) ||
|
||||||
"Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."}
|
t(
|
||||||
|
"relations.alias.settings.createMailReports.helperText",
|
||||||
|
)}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -205,9 +195,7 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
name="proxyImages"
|
name="proxyImages"
|
||||||
id="proxyImages"
|
id="proxyImages"
|
||||||
checked={
|
checked={formik.values.proxyImages}
|
||||||
formik.values.proxyImages
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
/>
|
/>
|
||||||
@ -217,29 +205,24 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
/>
|
/>
|
||||||
<FormHelperText
|
<FormHelperText
|
||||||
error={Boolean(
|
error={Boolean(
|
||||||
formik.touched.proxyImages &&
|
formik.touched.proxyImages && formik.errors.proxyImages,
|
||||||
formik.errors.proxyImages,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(formik.touched.proxyImages &&
|
{(formik.touched.proxyImages &&
|
||||||
formik.errors.proxyImages) ||
|
formik.errors.proxyImages) ||
|
||||||
"Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the original server has no idea you have opened the mail. After the cache time, the image is loaded from the original server, but it gets proxied by us. This means the original server will not be able to access neither your IP address nor your user agent."}
|
t("relations.alias.settings.proxyImages.helperText")}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Collapse in={formik.values.proxyImages}>
|
<Collapse in={formik.values.proxyImages}>
|
||||||
<Grid
|
<Grid
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection={
|
flexDirection={isLarge ? "row" : "column"}
|
||||||
isLarge ? "row" : "column"
|
|
||||||
}
|
|
||||||
container
|
container
|
||||||
marginY={2}
|
marginY={2}
|
||||||
spacing={4}
|
spacing={4}
|
||||||
alignItems={
|
alignItems={isLarge ? "flex-start" : "flex-end"}
|
||||||
isLarge ? "flex-start" : "flex-end"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Grid item md={6}>
|
<Grid item md={6} xs={12}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -254,62 +237,38 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
name="imageProxyFormat"
|
name="imageProxyFormat"
|
||||||
id="imageProxyFormat"
|
id="imageProxyFormat"
|
||||||
label="Image File Type"
|
label="Image File Type"
|
||||||
value={
|
value={formik.values.imageProxyFormat}
|
||||||
formik.values
|
onChange={formik.handleChange}
|
||||||
.imageProxyFormat
|
disabled={formik.isSubmitting}
|
||||||
}
|
|
||||||
onChange={
|
|
||||||
formik.handleChange
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
formik.isSubmitting
|
|
||||||
}
|
|
||||||
error={
|
error={
|
||||||
formik.touched
|
formik.touched.imageProxyFormat &&
|
||||||
.imageProxyFormat &&
|
Boolean(formik.errors.imageProxyFormat)
|
||||||
Boolean(
|
|
||||||
formik.errors
|
|
||||||
.imageProxyFormat,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
helperText={
|
helperText={
|
||||||
formik.touched
|
formik.touched.imageProxyFormat &&
|
||||||
.imageProxyFormat &&
|
formik.errors.imageProxyFormat
|
||||||
formik.errors
|
|
||||||
.imageProxyFormat
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{Object.entries(
|
{Object.entries(
|
||||||
ImageProxyFormatType,
|
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
|
||||||
).map(([key, value]) => (
|
).map(([value, translationString]) => (
|
||||||
<MenuItem
|
<MenuItem key={value} value={value}>
|
||||||
key={key}
|
{t(translationString)}
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP[
|
|
||||||
value
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<FormHelperText
|
<FormHelperText
|
||||||
error={Boolean(
|
error={Boolean(
|
||||||
formik.touched
|
formik.touched.imageProxyFormat &&
|
||||||
.imageProxyFormat &&
|
formik.errors.imageProxyFormat,
|
||||||
formik.errors
|
|
||||||
.imageProxyFormat,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formik.touched
|
{formik.touched.imageProxyFormat &&
|
||||||
.imageProxyFormat &&
|
formik.errors.imageProxyFormat}
|
||||||
formik.errors
|
|
||||||
.imageProxyFormat}
|
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6}>
|
<Grid item md={6} xs={12}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -317,59 +276,37 @@ export default function AliasesPreferencesForm(): ReactElement {
|
|||||||
name="imageProxyUserAgent"
|
name="imageProxyUserAgent"
|
||||||
id="imageProxyUserAgent"
|
id="imageProxyUserAgent"
|
||||||
label="Image Proxy User Agent"
|
label="Image Proxy User Agent"
|
||||||
value={
|
value={formik.values.imageProxyUserAgent}
|
||||||
formik.values
|
onChange={formik.handleChange}
|
||||||
.imageProxyUserAgent
|
disabled={formik.isSubmitting}
|
||||||
}
|
|
||||||
onChange={
|
|
||||||
formik.handleChange
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
formik.isSubmitting
|
|
||||||
}
|
|
||||||
error={
|
error={
|
||||||
formik.touched
|
formik.touched.imageProxyUserAgent &&
|
||||||
.imageProxyUserAgent &&
|
Boolean(formik.errors.imageProxyUserAgent)
|
||||||
Boolean(
|
|
||||||
formik.errors
|
|
||||||
.imageProxyUserAgent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
helperText={
|
helperText={
|
||||||
formik.touched
|
formik.touched.imageProxyUserAgent &&
|
||||||
.imageProxyUserAgent &&
|
formik.errors.imageProxyUserAgent
|
||||||
formik.errors
|
|
||||||
.imageProxyUserAgent
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{Object.entries(
|
{Object.entries(
|
||||||
ProxyUserAgentType,
|
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
||||||
).map(([key, value]) => (
|
).map(([value, translationString]) => (
|
||||||
<MenuItem
|
<MenuItem key={value} value={value}>
|
||||||
key={key}
|
{t(translationString)}
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP[
|
|
||||||
value
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<FormHelperText
|
<FormHelperText
|
||||||
error={Boolean(
|
error={Boolean(
|
||||||
formik.touched
|
formik.touched.imageProxyUserAgent &&
|
||||||
.imageProxyUserAgent &&
|
formik.errors.imageProxyUserAgent,
|
||||||
formik.errors
|
|
||||||
.imageProxyUserAgent,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(formik.touched
|
{(formik.touched.imageProxyUserAgent &&
|
||||||
.imageProxyUserAgent &&
|
formik.errors.imageProxyUserAgent) ||
|
||||||
formik.errors
|
t(
|
||||||
.imageProxyUserAgent) ||
|
"relations.alias.settings.imageProxyUserAgent.helperText",
|
||||||
"An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used by the proxy. User Agents are kept up-to-date."}
|
)}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -21,26 +22,25 @@ export interface SelectFieldProps {
|
|||||||
icon?: ReactElement
|
icon?: ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
const BOOLEAN_SELECT_TEXT_MAP: Record<string, string> = {
|
|
||||||
true: "Yes",
|
|
||||||
false: "No",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SelectField({
|
export default function SelectField({
|
||||||
label,
|
label,
|
||||||
formik,
|
formik,
|
||||||
icon,
|
icon,
|
||||||
name,
|
name,
|
||||||
valueTextMap = BOOLEAN_SELECT_TEXT_MAP,
|
valueTextMap: parentValueTextMap,
|
||||||
}: SelectFieldProps): ReactElement {
|
}: SelectFieldProps): ReactElement {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const BOOLEAN_SELECT_TEXT_MAP: Record<string, string> = {
|
||||||
|
true: "general.booleanSelection.true",
|
||||||
|
false: "general.booleanSelection.false",
|
||||||
|
}
|
||||||
|
const valueTextMap = parentValueTextMap ?? BOOLEAN_SELECT_TEXT_MAP
|
||||||
|
|
||||||
const labelId = `${name}-label`
|
const labelId = `${name}-label`
|
||||||
const preferenceName = `alias${
|
const preferenceName = `alias${name.charAt(0).toUpperCase() + name.slice(1)}`
|
||||||
name.charAt(0).toUpperCase() + name.slice(1)
|
|
||||||
}`
|
|
||||||
const value = user.preferences[preferenceName as keyof User["preferences"]]
|
const value = user.preferences[preferenceName as keyof User["preferences"]]
|
||||||
const defaultValueText = valueTextMap[value.toString()]
|
const defaultValueText = t(valueTextMap[value.toString()])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
@ -52,9 +52,7 @@ export default function SelectField({
|
|||||||
label={label}
|
label={label}
|
||||||
labelId={labelId}
|
labelId={labelId}
|
||||||
startAdornment={
|
startAdornment={
|
||||||
icon ? (
|
icon ? <InputAdornment position="start">{icon}</InputAdornment> : undefined
|
||||||
<InputAdornment position="start">{icon}</InputAdornment>
|
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
value={(formik.values[name] ?? "null").toString()}
|
value={(formik.values[name] ?? "null").toString()}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
@ -77,25 +75,31 @@ export default function SelectField({
|
|||||||
error={Boolean(formik.touched[name] && formik.errors[name])}
|
error={Boolean(formik.touched[name] && formik.errors[name])}
|
||||||
renderValue={value =>
|
renderValue={value =>
|
||||||
value === "null" ? (
|
value === "null" ? (
|
||||||
<i>{`<${defaultValueText}>`}</i>
|
<i>
|
||||||
|
{t("general.defaultValueSelectionRaw", {
|
||||||
|
value: defaultValueText,
|
||||||
|
})}
|
||||||
|
</i>
|
||||||
) : (
|
) : (
|
||||||
valueTextMap[value.toString()]
|
t(valueTextMap[value.toString()])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem value="null">
|
<MenuItem value="null">
|
||||||
<i>{`Default <${defaultValueText}>`}</i>
|
<i>
|
||||||
|
{t("general.defaultValueSelection", {
|
||||||
|
value: defaultValueText,
|
||||||
|
})}
|
||||||
|
</i>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{valueTextMap &&
|
{valueTextMap &&
|
||||||
Object.entries(valueTextMap).map(([value, text]) => (
|
Object.entries(valueTextMap).map(([value, translationString]) => (
|
||||||
<MenuItem key={value} value={value}>
|
<MenuItem key={value} value={value}>
|
||||||
{text}
|
{t(translationString)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<FormHelperText
|
<FormHelperText error={Boolean(formik.touched[name] && formik.errors[name])}>
|
||||||
error={Boolean(formik.touched[name] && formik.errors[name])}
|
|
||||||
>
|
|
||||||
{formik.touched[name] && formik.errors[name]}
|
{formik.touched[name] && formik.errors[name]}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -13,6 +13,7 @@ import {MultiStepFormElement, SimpleForm} from "~/components"
|
|||||||
import {SignupResult, checkIsDomainDisposable, signup} from "~/apis"
|
import {SignupResult, checkIsDomainDisposable, signup} from "~/apis"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {ServerSettings} from "~/server-types"
|
import {ServerSettings} from "~/server-types"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
export interface EmailFormProps {
|
export interface EmailFormProps {
|
||||||
serverSettings: ServerSettings
|
serverSettings: ServerSettings
|
||||||
@ -24,20 +25,19 @@ interface Form {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA = yup.object().shape({
|
export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): ReactElement {
|
||||||
email: yup.string().email().required(),
|
const {t} = useTranslation()
|
||||||
})
|
const SCHEMA = yup.object().shape({
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.required()
|
||||||
|
.label(t("routes.SignupRoute.forms.email.form.email.label")),
|
||||||
|
})
|
||||||
|
|
||||||
export default function EmailForm({
|
const {mutateAsync} = useMutation<SignupResult, AxiosError, string>(signup, {
|
||||||
onSignUp,
|
onSuccess: ({normalizedEmail}) => onSignUp(normalizedEmail),
|
||||||
serverSettings,
|
})
|
||||||
}: EmailFormProps): ReactElement {
|
|
||||||
const {mutateAsync} = useMutation<SignupResult, AxiosError, string>(
|
|
||||||
signup,
|
|
||||||
{
|
|
||||||
onSuccess: ({normalizedEmail}) => onSignUp(normalizedEmail),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: SCHEMA,
|
validationSchema: SCHEMA,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -46,9 +46,7 @@ export default function EmailForm({
|
|||||||
onSubmit: async (values, {setErrors}) => {
|
onSubmit: async (values, {setErrors}) => {
|
||||||
// Check is email disposable
|
// Check is email disposable
|
||||||
try {
|
try {
|
||||||
const isDisposable = await checkIsDomainDisposable(
|
const isDisposable = await checkIsDomainDisposable(values.email.split("@")[1])
|
||||||
values.email.split("@")[1],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isDisposable) {
|
if (isDisposable) {
|
||||||
setErrors({
|
setErrors({
|
||||||
@ -76,9 +74,9 @@ export default function EmailForm({
|
|||||||
<MultiStepFormElement>
|
<MultiStepFormElement>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<SimpleForm
|
<SimpleForm
|
||||||
title="Sign up"
|
title={t("routes.SignupRoute.forms.email.title")}
|
||||||
description="We only need your email and you are ready to go!"
|
description={t("routes.SignupRoute.forms.email.description")}
|
||||||
continueActionLabel="Sign up"
|
continueActionLabel={t("routes.SignupRoute.forms.email.continueAction")}
|
||||||
nonFieldError={formik.errors.detail}
|
nonFieldError={formik.errors.detail}
|
||||||
isSubmitting={formik.isSubmitting}
|
isSubmitting={formik.isSubmitting}
|
||||||
>
|
>
|
||||||
@ -88,18 +86,16 @@ export default function EmailForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
name="email"
|
name="email"
|
||||||
id="email"
|
id="email"
|
||||||
label="Email"
|
label={t("routes.SignupRoute.forms.email.form.email.label")}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.SignupRoute.forms.email.form.email.placeholder",
|
||||||
|
)}
|
||||||
inputMode="email"
|
inputMode="email"
|
||||||
value={formik.values.email}
|
value={formik.values.email}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={formik.touched.email && Boolean(formik.errors.email)}
|
||||||
formik.touched.email &&
|
helperText={formik.touched.email && formik.errors.email}
|
||||||
Boolean(formik.errors.email)
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
formik.touched.email && formik.errors.email
|
|
||||||
}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -113,9 +109,7 @@ export default function EmailForm({
|
|||||||
</form>
|
</form>
|
||||||
</MultiStepFormElement>
|
</MultiStepFormElement>
|
||||||
{!serverSettings.otherRelaysEnabled && (
|
{!serverSettings.otherRelaysEnabled && (
|
||||||
<DetectEmailAutofillService
|
<DetectEmailAutofillService domains={serverSettings.otherRelayDomains} />
|
||||||
domains={serverSettings.otherRelayDomains}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {useLoaderData} from "react-router-dom"
|
import {useLoaderData} from "react-router-dom"
|
||||||
import {MdMail} from "react-icons/md"
|
import {MdMail} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
@ -13,9 +14,8 @@ export interface ResendMailButtonProps {
|
|||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ResendMailButton({
|
export default function ResendMailButton({email}: ResendMailButtonProps): ReactElement {
|
||||||
email,
|
const {t} = useTranslation()
|
||||||
}: ResendMailButtonProps): ReactElement {
|
|
||||||
const settings = useLoaderData() as ServerSettings
|
const settings = useLoaderData() as ServerSettings
|
||||||
|
|
||||||
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
||||||
@ -30,7 +30,7 @@ export default function ResendMailButton({
|
|||||||
startIcon={<MdMail />}
|
startIcon={<MdMail />}
|
||||||
onClick={() => mutate()}
|
onClick={() => mutate()}
|
||||||
>
|
>
|
||||||
Resend Mail
|
{t("components.ResendMailButton.label")}
|
||||||
</TimedButton>
|
</TimedButton>
|
||||||
<MutationStatusSnackbar mutation={mutation} />
|
<MutationStatusSnackbar mutation={mutation} />
|
||||||
</>
|
</>
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
|
||||||
import {MultiStepFormElement, OpenMailButton} from "~/components"
|
import {MultiStepFormElement, OpenMailButton} from "~/components"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import ResendMailButton from "~/route-widgets/SignupRoute/YouGotMail/ResendMailButton"
|
import ResendMailButton from "~/route-widgets/SignupRoute/YouGotMail/ResendMailButton"
|
||||||
|
|
||||||
export interface YouGotMailProps {
|
export interface YouGotMailProps {
|
||||||
@ -21,10 +22,9 @@ export interface YouGotMailProps {
|
|||||||
onGoBack: () => void
|
onGoBack: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function YouGotMail({
|
export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactElement {
|
||||||
email,
|
const {t} = useTranslation()
|
||||||
onGoBack,
|
|
||||||
}: YouGotMailProps): ReactElement {
|
|
||||||
const [askToEditEmail, setAskToEditEmail] = useState<boolean>(false)
|
const [askToEditEmail, setAskToEditEmail] = useState<boolean>(false)
|
||||||
|
|
||||||
const domain = email.split("@")[1]
|
const domain = email.split("@")[1]
|
||||||
@ -43,30 +43,21 @@ export default function YouGotMail({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h2" align="center">
|
<Typography variant="h6" component="h2" align="center">
|
||||||
You got mail!
|
{t("routes.SignupRoute.forms.mailVerification.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="subtitle1" component="p">
|
<Typography variant="subtitle1" component="p">
|
||||||
We sent you an email with a link to confirm your
|
{t("routes.SignupRoute.forms.mailVerification.description")}
|
||||||
email address. Please check your inbox and click on
|
|
||||||
the link to continue.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid
|
<Grid container alignItems="center" direction="row" spacing={2}>
|
||||||
container
|
|
||||||
alignItems="center"
|
|
||||||
direction="row"
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<code>{email}</code>
|
<code>{email}</code>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<IconButton
|
<IconButton onClick={() => setAskToEditEmail(true)}>
|
||||||
onClick={() => setAskToEditEmail(true)}
|
|
||||||
>
|
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -81,21 +72,21 @@ export default function YouGotMail({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</MultiStepFormElement>
|
</MultiStepFormElement>
|
||||||
<Dialog open={askToEditEmail}>
|
<Dialog open={askToEditEmail}>
|
||||||
<DialogTitle>Edit email address?</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t("routes.SignupRoute.forms.mailVerification.editEmail.title")}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Would you like to return to the previous step and edit
|
{t("routes.SignupRoute.forms.mailVerification.editEmail.description")}
|
||||||
your email address?
|
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button startIcon={<MdCancel />} onClick={() => setAskToEditEmail(false)}>
|
||||||
startIcon={<MdCancel />}
|
{t("general.cancelLabel")}
|
||||||
onClick={() => setAskToEditEmail(false)}
|
</Button>
|
||||||
>
|
<Button onClick={onGoBack}>
|
||||||
Cancel
|
{t("routes.SignupRoute.forms.mailVerification.editEmail.continueAction")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onGoBack}>Yes, edit email</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext} from "react"
|
||||||
import {useParams} from "react-router-dom"
|
import {useParams} from "react-router-dom"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
|||||||
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
||||||
|
|
||||||
export default function AliasDetailRoute(): ReactElement {
|
export default function AliasDetailRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const address = atob(params.addressInBase64 as string)
|
const address = atob(params.addressInBase64 as string)
|
||||||
const {_decryptUsingMasterPassword, encryptionStatus} =
|
const {_decryptUsingMasterPassword, encryptionStatus} =
|
||||||
@ -34,7 +36,7 @@ export default function AliasDetailRoute(): ReactElement {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimplePage title="Alias Details">
|
<SimplePage title={t("routes.AliasDetailRoute.title")}>
|
||||||
<QueryResult<Alias | DecryptedAlias> query={query}>
|
<QueryResult<Alias | DecryptedAlias> query={query}>
|
||||||
{alias => <AliasDetails alias={alias} />}
|
{alias => <AliasDetails alias={alias} />}
|
||||||
</QueryResult>
|
</QueryResult>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {ReactElement, useState, useTransition} from "react"
|
import {ReactElement, useState, useTransition} from "react"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {MdSearch} from "react-icons/md"
|
import {MdSearch} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
import {InputAdornment, TextField} from "@mui/material"
|
import {InputAdornment, TextField} from "@mui/material"
|
||||||
@ -11,6 +12,8 @@ import AliasesDetails from "~/route-widgets/AliasesRoute/AliasesDetails"
|
|||||||
import getAliases from "~/apis/get-aliases"
|
import getAliases from "~/apis/get-aliases"
|
||||||
|
|
||||||
export default function AliasesRoute(): ReactElement {
|
export default function AliasesRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState<string>("")
|
const [searchValue, setSearchValue] = useState<string>("")
|
||||||
const [queryValue, setQueryValue] = useState<string>("")
|
const [queryValue, setQueryValue] = useState<string>("")
|
||||||
const [, startTransition] = useTransition()
|
const [, startTransition] = useTransition()
|
||||||
@ -25,7 +28,7 @@ export default function AliasesRoute(): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimplePage
|
<SimplePage
|
||||||
title="Aliases"
|
title={t("routes.AliasesRoute.title")}
|
||||||
pageOptionsActions={
|
pageOptionsActions={
|
||||||
<TextField
|
<TextField
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
@ -35,7 +38,10 @@ export default function AliasesRoute(): ReactElement {
|
|||||||
setQueryValue(event.target.value)
|
setQueryValue(event.target.value)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
label="Search"
|
label={t("routes.AliasesRoute.pageActions.search.label")}
|
||||||
|
placeholder={t(
|
||||||
|
"routes.AliasesRoute.pageActions.search.placeholder",
|
||||||
|
)}
|
||||||
id="search"
|
id="search"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
import {Link as RouterLink, Outlet} from "react-router-dom"
|
import {Link as RouterLink, Outlet} from "react-router-dom"
|
||||||
import {MdAdd, MdLogin} from "react-icons/md"
|
import {MdAdd, MdLogin} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Box, Button, Grid} from "@mui/material"
|
import {Box, Button, Grid} from "@mui/material"
|
||||||
|
|
||||||
export default function AuthenticateRoute(): ReactElement {
|
export default function AuthenticateRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
@ -15,12 +18,7 @@ export default function AuthenticateRoute(): ReactElement {
|
|||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Grid
|
<Grid container spacing={2} justifyContent="center" marginBottom={2}>
|
||||||
container
|
|
||||||
spacing={2}
|
|
||||||
justifyContent="center"
|
|
||||||
marginBottom={2}
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
@ -29,7 +27,7 @@ export default function AuthenticateRoute(): ReactElement {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={<MdAdd />}
|
startIcon={<MdAdd />}
|
||||||
>
|
>
|
||||||
Sign Up
|
{t("components.AuthenticateRoute.signup")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -40,7 +38,7 @@ export default function AuthenticateRoute(): ReactElement {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={<MdLogin />}
|
startIcon={<MdLogin />}
|
||||||
>
|
>
|
||||||
Login
|
{t("components.AuthenticateRoute.login")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {ReactElement, useContext, useState} from "react"
|
import {ReactElement, useContext, useState} from "react"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {Grid, Paper, Typography} from "@mui/material"
|
import {Grid, Paper, Typography} from "@mui/material"
|
||||||
|
|
||||||
@ -9,16 +10,15 @@ import GenerateEmailReportsForm from "~/route-widgets/CompleteAccountRoute/Gener
|
|||||||
import PasswordForm from "~/route-widgets/CompleteAccountRoute/PasswordForm"
|
import PasswordForm from "~/route-widgets/CompleteAccountRoute/PasswordForm"
|
||||||
|
|
||||||
export default function CompleteAccountRoute(): ReactElement {
|
export default function CompleteAccountRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const {encryptionStatus} = useContext(AuthContext)
|
const {encryptionStatus} = useContext(AuthContext)
|
||||||
const navigateToNext = useNavigateToNext()
|
const navigateToNext = useNavigateToNext()
|
||||||
|
|
||||||
// If query `setup` is `true`, skip directly to the setup
|
// If query `setup` is `true`, skip directly to the setup
|
||||||
const [showGenerationReportForm, setShowGenerationReportForm] = useState(
|
const [showGenerationReportForm, setShowGenerationReportForm] = useState(() => {
|
||||||
() => {
|
const searchParams = new URLSearchParams(location.search)
|
||||||
const searchParams = new URLSearchParams(location.search)
|
return searchParams.get("setup") === "true"
|
||||||
return searchParams.get("setup") === "true"
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (encryptionStatus === EncryptionStatus.Unavailable) {
|
if (encryptionStatus === EncryptionStatus.Unavailable) {
|
||||||
return (
|
return (
|
||||||
@ -29,10 +29,7 @@ export default function CompleteAccountRoute(): ReactElement {
|
|||||||
onYes={() => setShowGenerationReportForm(true)}
|
onYes={() => setShowGenerationReportForm(true)}
|
||||||
onNo={navigateToNext}
|
onNo={navigateToNext}
|
||||||
/>,
|
/>,
|
||||||
<PasswordForm
|
<PasswordForm onDone={navigateToNext} key="password_form" />,
|
||||||
onDone={navigateToNext}
|
|
||||||
key="password_form"
|
|
||||||
/>,
|
|
||||||
]}
|
]}
|
||||||
index={showGenerationReportForm ? 1 : 0}
|
index={showGenerationReportForm ? 1 : 0}
|
||||||
/>
|
/>
|
||||||
@ -51,13 +48,12 @@ export default function CompleteAccountRoute(): ReactElement {
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h1" align="center">
|
<Typography variant="h6" component="h1" align="center">
|
||||||
Encryption already enabled
|
{t("routes.CompleteAccountRoute.forms.available.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
You already have encryption enabled. Changing passwords
|
{t("routes.CompleteAccountRoute.forms.available.description")}
|
||||||
is currently not supported.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as yup from "yup"
|
import * as yup from "yup"
|
||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext} from "react"
|
||||||
import {useLocation, useNavigate} from "react-router-dom"
|
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
import {MdLock} from "react-icons/md"
|
import {MdLock} from "react-icons/md"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
|
||||||
import {InputAdornment} from "@mui/material"
|
import {InputAdornment} from "@mui/material"
|
||||||
|
|
||||||
import {buildEncryptionPassword} from "~/utils"
|
import {buildEncryptionPassword} from "~/utils"
|
||||||
import {useUser} from "~/hooks"
|
import {useNavigateToNext, useUser} from "~/hooks"
|
||||||
import {PasswordField, SimpleForm} from "~/components"
|
import {PasswordField, SimpleForm} from "~/components"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ const schema = yup.object().shape({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function EnterDecryptionPassword(): ReactElement {
|
export default function EnterDecryptionPassword(): ReactElement {
|
||||||
const navigate = useNavigate()
|
const {t} = useTranslation()
|
||||||
const location = useLocation()
|
const navigateToNext = useNavigateToNext()
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const {_setDecryptionPassword} = useContext(AuthContext)
|
const {_setDecryptionPassword} = useContext(AuthContext)
|
||||||
|
|
||||||
@ -37,11 +37,13 @@ export default function EnterDecryptionPassword(): ReactElement {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!_setDecryptionPassword(decryptionPassword)) {
|
if (!_setDecryptionPassword(decryptionPassword)) {
|
||||||
setErrors({password: "Password is invalid."})
|
setErrors({
|
||||||
|
password: t(
|
||||||
|
"components.EnterDecryptionPassword.form.password.errors.invalidPassword",
|
||||||
|
),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const nextUrl =
|
navigateToNext()
|
||||||
new URLSearchParams(location.search).get("next") || "/"
|
|
||||||
setTimeout(() => navigate(nextUrl), 0)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -49,10 +51,16 @@ export default function EnterDecryptionPassword(): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<SimpleForm
|
<SimpleForm
|
||||||
title="Decrypt reports"
|
title={t("components.EnterDecryptionPassword.title")}
|
||||||
description="Please enter your password so that your reports can de decrypted."
|
description={t(
|
||||||
cancelActionLabel="Don't decrypt"
|
"components.EnterDecryptionPassword.description",
|
||||||
continueActionLabel="Continue"
|
)}
|
||||||
|
cancelActionLabel={t(
|
||||||
|
"components.EnterDecryptionPassword.cancelAction",
|
||||||
|
)}
|
||||||
|
continueActionLabel={t(
|
||||||
|
"components.EnterDecryptionPassword.continueAction",
|
||||||
|
)}
|
||||||
isSubmitting={formik.isSubmitting}
|
isSubmitting={formik.isSubmitting}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
@ -61,7 +69,12 @@ export default function EnterDecryptionPassword(): ReactElement {
|
|||||||
fullWidth
|
fullWidth
|
||||||
name="password"
|
name="password"
|
||||||
id="password"
|
id="password"
|
||||||
label="Password"
|
label={t(
|
||||||
|
"components.EnterDecryptionPassword.form.password.label",
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"components.EnterDecryptionPassword.form.password.placeholder",
|
||||||
|
)}
|
||||||
value={formik.values.password}
|
value={formik.values.password}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import {useParams} from "react-router-dom"
|
import {useParams} from "react-router-dom"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
import {Box, Grid, List, Typography} from "@mui/material"
|
import {List} from "@mui/material"
|
||||||
|
|
||||||
import {Report} from "~/server-types"
|
import {DecryptedReportContent, Report} from "~/server-types"
|
||||||
import {getReport} from "~/apis"
|
import {getReport} from "~/apis"
|
||||||
import {DecryptReport} from "~/components"
|
import {DecryptReport, SimpleOverlayInformation, SimplePageBuilder} from "~/components"
|
||||||
|
import {WithEncryptionRequired} from "~/hocs"
|
||||||
import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem"
|
import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem"
|
||||||
import QueryResult from "~/components/QueryResult"
|
import QueryResult from "~/components/QueryResult"
|
||||||
import SimplePage from "~/components/SimplePage"
|
|
||||||
import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem"
|
import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem"
|
||||||
|
|
||||||
export default function ReportDetailRoute(): ReactElement {
|
function ReportDetailRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|
||||||
const query = useQuery<Report, AxiosError>(["get_report", params.id], () =>
|
const query = useQuery<Report, AxiosError>(["get_report", params.id], () =>
|
||||||
@ -21,102 +23,89 @@ export default function ReportDetailRoute(): ReactElement {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimplePage title="Report Details">
|
<SimplePageBuilder.Page title="Report Details">
|
||||||
<QueryResult<Report> query={query}>
|
<QueryResult<Report> query={query}>
|
||||||
{encryptedReport => (
|
{encryptedReport => (
|
||||||
<DecryptReport
|
<DecryptReport encryptedContent={encryptedReport.encryptedContent}>
|
||||||
encryptedContent={encryptedReport.encryptedContent}
|
|
||||||
>
|
|
||||||
{report => (
|
{report => (
|
||||||
<Grid container spacing={4}>
|
<SimplePageBuilder.MultipleSections>
|
||||||
<Grid item xs={12}>
|
{[
|
||||||
<Typography variant="h6" component="h2">
|
<SimplePageBuilder.Section
|
||||||
Email information
|
key="information"
|
||||||
</Typography>
|
label={t(
|
||||||
<Grid container columnSpacing={4}>
|
"routes.ReportDetailRoute.sections.information.title",
|
||||||
<Grid item xs={12} md={6} lg={4}>
|
)}
|
||||||
<Box component="dl">
|
>
|
||||||
<Typography
|
<SimplePageBuilder.InformationContainer>
|
||||||
variant="overline"
|
{[
|
||||||
component="dt"
|
<SimpleOverlayInformation
|
||||||
>
|
key="from"
|
||||||
From
|
label={t(
|
||||||
</Typography>
|
"routes.ReportDetailRoute.sections.information.form.from.label",
|
||||||
<Typography
|
)}
|
||||||
variant="body1"
|
|
||||||
component="dd"
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
report.messageDetails
|
(report as DecryptedReportContent)
|
||||||
.meta.from
|
.messageDetails.meta.from
|
||||||
}
|
}
|
||||||
</Typography>
|
</SimpleOverlayInformation>,
|
||||||
</Box>
|
|
||||||
</Grid>
|
<SimpleOverlayInformation
|
||||||
<Grid item xs={12} md={6} lg={4}>
|
key="to"
|
||||||
<Box component="dl">
|
label={t(
|
||||||
<Typography
|
"routes.ReportDetailRoute.sections.information.form.to.label",
|
||||||
variant="overline"
|
)}
|
||||||
component="dt"
|
|
||||||
>
|
|
||||||
To
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
component="dd"
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
report.messageDetails
|
(report as DecryptedReportContent)
|
||||||
.meta.to
|
.messageDetails.meta.to
|
||||||
}
|
}
|
||||||
</Typography>
|
</SimpleOverlayInformation>,
|
||||||
</Box>
|
|
||||||
</Grid>
|
<SimpleOverlayInformation
|
||||||
<Grid item xs={12} lg={4}>
|
key="subject"
|
||||||
<Box component="dl">
|
label={t(
|
||||||
<Typography
|
"routes.ReportDetailRoute.sections.information.form.subject.label",
|
||||||
variant="overline"
|
)}
|
||||||
component="dt"
|
|
||||||
>
|
|
||||||
Subject
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
component="dd"
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
report.messageDetails
|
(report as DecryptedReportContent)
|
||||||
.content.subject
|
.messageDetails.content.subject
|
||||||
}
|
}
|
||||||
</Typography>
|
</SimpleOverlayInformation>,
|
||||||
</Box>
|
]}
|
||||||
</Grid>
|
</SimplePageBuilder.InformationContainer>
|
||||||
</Grid>
|
</SimplePageBuilder.Section>,
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
<SimplePageBuilder.Section
|
||||||
<Typography variant="h6" component="h2">
|
key="trackers"
|
||||||
Trackers
|
label={t(
|
||||||
</Typography>
|
"routes.ReportDetailRoute.sections.trackers.title",
|
||||||
<List>
|
)}
|
||||||
<SinglePixelImageTrackersListItem
|
>
|
||||||
images={
|
<List>
|
||||||
report.messageDetails.content
|
<SinglePixelImageTrackersListItem
|
||||||
.singlePixelImages
|
images={
|
||||||
}
|
(report as DecryptedReportContent)
|
||||||
/>
|
.messageDetails.content.singlePixelImages
|
||||||
<ProxiedImagesListItem
|
}
|
||||||
images={
|
/>
|
||||||
report.messageDetails.content
|
<ProxiedImagesListItem
|
||||||
.proxiedImages
|
images={
|
||||||
}
|
(report as DecryptedReportContent)
|
||||||
/>
|
.messageDetails.content.proxiedImages
|
||||||
</List>
|
}
|
||||||
</Grid>
|
/>
|
||||||
</Grid>
|
</List>
|
||||||
|
</SimplePageBuilder.Section>,
|
||||||
|
]}
|
||||||
|
</SimplePageBuilder.MultipleSections>
|
||||||
)}
|
)}
|
||||||
</DecryptReport>
|
</DecryptReport>
|
||||||
)}
|
)}
|
||||||
</QueryResult>
|
</QueryResult>
|
||||||
</SimplePage>
|
</SimplePageBuilder.Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default WithEncryptionRequired(ReportDetailRoute)
|
||||||
|
@ -2,22 +2,18 @@ import {ReactElement, useState} from "react"
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {MdList} from "react-icons/md"
|
import {MdList} from "react-icons/md"
|
||||||
import {FaMask} from "react-icons/fa"
|
import {FaMask} from "react-icons/fa"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import groupArray from "group-array"
|
import groupArray from "group-array"
|
||||||
import sortArray from "sort-array"
|
import sortArray from "sort-array"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
import {
|
import {InputAdornment, List, MenuItem, TextField, Typography} from "@mui/material"
|
||||||
InputAdornment,
|
|
||||||
List,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material"
|
|
||||||
|
|
||||||
import {DecryptedReportContent, PaginationResult, Report} from "~/server-types"
|
import {DecryptedReportContent, PaginationResult, Report} from "~/server-types"
|
||||||
import {getReports} from "~/apis"
|
import {getReports} from "~/apis"
|
||||||
import {WithEncryptionRequired} from "~/hocs"
|
import {WithEncryptionRequired} from "~/hocs"
|
||||||
import {DecryptReport} from "~/components"
|
import {DecryptReport} from "~/components"
|
||||||
|
import {createEnumMapFromTranslation} from "~/utils"
|
||||||
import QueryResult from "~/components/QueryResult"
|
import QueryResult from "~/components/QueryResult"
|
||||||
import ReportInformationItem from "~/route-widgets/ReportsRoute/ReportInformationItem"
|
import ReportInformationItem from "~/route-widgets/ReportsRoute/ReportInformationItem"
|
||||||
import SimplePage from "~/components/SimplePage"
|
import SimplePage from "~/components/SimplePage"
|
||||||
@ -27,25 +23,21 @@ enum SortingView {
|
|||||||
GroupByAlias = "GroupByAlias",
|
GroupByAlias = "GroupByAlias",
|
||||||
}
|
}
|
||||||
|
|
||||||
const SORTING_VIEW_NAME_MAP: Record<SortingView, string> = {
|
|
||||||
[SortingView.List]: "List reports by their date",
|
|
||||||
[SortingView.GroupByAlias]: "Group reports by their aliases",
|
|
||||||
}
|
|
||||||
|
|
||||||
const SORTING_VIEW_ICON_MAP: Record<SortingView, ReactElement> = {
|
const SORTING_VIEW_ICON_MAP: Record<SortingView, ReactElement> = {
|
||||||
[SortingView.List]: <MdList />,
|
[SortingView.List]: <MdList />,
|
||||||
[SortingView.GroupByAlias]: <FaMask />,
|
[SortingView.GroupByAlias]: <FaMask />,
|
||||||
}
|
}
|
||||||
|
const SORTING_VIEW_NAME_MAP: Record<SortingView, string> = createEnumMapFromTranslation(
|
||||||
|
"routes.ReportsRoute.pageActions.sort",
|
||||||
|
SortingView,
|
||||||
|
)
|
||||||
|
|
||||||
function ReportsRoute(): ReactElement {
|
function ReportsRoute(): ReactElement {
|
||||||
const query = useQuery<PaginationResult<Report>, AxiosError>(
|
const {t} = useTranslation()
|
||||||
["get_reports"],
|
|
||||||
getReports,
|
|
||||||
)
|
|
||||||
|
|
||||||
const [sortingView, setSortingView] = useState<SortingView>(
|
const query = useQuery<PaginationResult<Report>, AxiosError>(["get_reports"], getReports)
|
||||||
SortingView.List,
|
|
||||||
)
|
const [sortingView, setSortingView] = useState<SortingView>(SortingView.List)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimplePage
|
<SimplePage
|
||||||
@ -53,9 +45,7 @@ function ReportsRoute(): ReactElement {
|
|||||||
pageOptionsActions={
|
pageOptionsActions={
|
||||||
<TextField
|
<TextField
|
||||||
value={sortingView}
|
value={sortingView}
|
||||||
onChange={event =>
|
onChange={event => setSortingView(event.target.value as SortingView)}
|
||||||
setSortingView(event.target.value as SortingView)
|
|
||||||
}
|
|
||||||
label="Sorting"
|
label="Sorting"
|
||||||
id="sorting"
|
id="sorting"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -69,7 +59,7 @@ function ReportsRoute(): ReactElement {
|
|||||||
>
|
>
|
||||||
{Object.keys(SORTING_VIEW_NAME_MAP).map(name => (
|
{Object.keys(SORTING_VIEW_NAME_MAP).map(name => (
|
||||||
<MenuItem key={name} value={name}>
|
<MenuItem key={name} value={name}>
|
||||||
{SORTING_VIEW_NAME_MAP[name as SortingView]}
|
{t(SORTING_VIEW_NAME_MAP[name as SortingView])}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
@ -115,18 +105,12 @@ function ReportsRoute(): ReactElement {
|
|||||||
>
|
>
|
||||||
{alias}
|
{alias}
|
||||||
</Typography>
|
</Typography>
|
||||||
{reports.map(
|
{reports.map(report => (
|
||||||
report => (
|
<ReportInformationItem
|
||||||
<ReportInformationItem
|
report={report}
|
||||||
report={
|
key={report.id}
|
||||||
report
|
/>
|
||||||
}
|
))}
|
||||||
key={
|
|
||||||
report.id
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
import {Grid, Typography} from "@mui/material"
|
import {SimplePageBuilder} from "~/components"
|
||||||
|
|
||||||
import AliasesPreferencesForm from "~/route-widgets/SettingsRoute/AliasesPreferencesForm"
|
import AliasesPreferencesForm from "~/route-widgets/SettingsRoute/AliasesPreferencesForm"
|
||||||
|
|
||||||
export default function SettingsRoute(): ReactElement {
|
export default function SettingsRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={4}>
|
<SimplePageBuilder.Page title={t("routes.SettingsRoute.title")}>
|
||||||
<Grid item>
|
<AliasesPreferencesForm />
|
||||||
<Typography variant="h5" component="h2">
|
</SimplePageBuilder.Page>
|
||||||
Settings
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<AliasesPreferencesForm />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {useLoaderData, useNavigate} from "react-router-dom"
|
|||||||
import {useAsync, useLocalStorage} from "react-use"
|
import {useAsync, useLocalStorage} from "react-use"
|
||||||
import {MdCancel} from "react-icons/md"
|
import {MdCancel} from "react-icons/md"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
import React, {ReactElement, useContext} from "react"
|
import React, {ReactElement, useContext} from "react"
|
||||||
|
|
||||||
import {Grid, Paper, Typography, useTheme} from "@mui/material"
|
import {Grid, Paper, Typography, useTheme} from "@mui/material"
|
||||||
@ -18,6 +19,8 @@ const emailSchema = yup.string().email()
|
|||||||
export default function VerifyEmailRoute(): ReactElement {
|
export default function VerifyEmailRoute(): ReactElement {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const {login} = useContext(AuthContext)
|
const {login} = useContext(AuthContext)
|
||||||
const [_, setEmail] = useLocalStorage<string>("signup-form-state-email", "")
|
const [_, setEmail] = useLocalStorage<string>("signup-form-state-email", "")
|
||||||
const {email, token} = useQueryParams<{
|
const {email, token} = useQueryParams<{
|
||||||
@ -29,7 +32,7 @@ export default function VerifyEmailRoute(): ReactElement {
|
|||||||
const tokenSchema = yup
|
const tokenSchema = yup
|
||||||
.string()
|
.string()
|
||||||
.length(serverSettings.emailVerificationLength)
|
.length(serverSettings.emailVerificationLength)
|
||||||
.test("token", "Invalid token", token => {
|
.test("token", t("routes.VerifyEmailRoute.errors.code.invalid") as string, token => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -38,17 +41,16 @@ export default function VerifyEmailRoute(): ReactElement {
|
|||||||
|
|
||||||
return token.split("").every(char => chars.includes(char))
|
return token.split("").every(char => chars.includes(char))
|
||||||
})
|
})
|
||||||
const {mutateAsync} = useMutation<
|
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyEmailData>(
|
||||||
AuthenticationDetails,
|
verifyEmail,
|
||||||
AxiosError,
|
{
|
||||||
VerifyEmailData
|
onSuccess: ({user}) => {
|
||||||
>(verifyEmail, {
|
setEmail("")
|
||||||
onSuccess: ({user}) => {
|
login(user)
|
||||||
setEmail("")
|
navigate("/auth/complete-account")
|
||||||
login(user)
|
},
|
||||||
navigate("/auth/complete-account")
|
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
const {loading} = useAsync(async () => {
|
const {loading} = useAsync(async () => {
|
||||||
await emailSchema.validate(email)
|
await emailSchema.validate(email)
|
||||||
await tokenSchema.validate(token)
|
await tokenSchema.validate(token)
|
||||||
@ -71,34 +73,23 @@ export default function VerifyEmailRoute(): ReactElement {
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h5" component="h1" align="center">
|
<Typography variant="h5" component="h1" align="center">
|
||||||
Verify your email
|
{t("routes.VerifyEmailRoute.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
variant="subtitle1"
|
{t("routes.VerifyEmailRoute.isLoading")}
|
||||||
component="p"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
Verifying your email...
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<MdCancel
|
<MdCancel size={100} color={theme.palette.error.main} />
|
||||||
size={100}
|
|
||||||
color={theme.palette.error.main}
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
variant="subtitle1"
|
{t("routes.VerifyEmailRoute.isCodeInvalid")}
|
||||||
component="p"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
Sorry, but this verification code is invalid.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
|
9
src/utils/create-enum-map-from-translation.ts
Normal file
9
src/utils/create-enum-map-from-translation.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function createEnumMapFromTranslation<T extends Record<string, string>>(
|
||||||
|
prefix: string,
|
||||||
|
TEnum: T,
|
||||||
|
): Record<keyof T, string> {
|
||||||
|
return Object.fromEntries(Object.values(TEnum).map(key => [key, `${prefix}.${key}`])) as Record<
|
||||||
|
keyof T,
|
||||||
|
string
|
||||||
|
>
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
import {AxiosError} from "axios"
|
|
||||||
|
|
||||||
import {FastAPIError} from "~/utils/parse-fastapi-error"
|
|
||||||
|
|
||||||
export default function getErrorMessage(
|
|
||||||
error: AxiosError<FastAPIError>,
|
|
||||||
): string {
|
|
||||||
if (typeof error.response?.data?.detail === "string") {
|
|
||||||
return error.response.data.detail
|
|
||||||
}
|
|
||||||
|
|
||||||
return "There was an error."
|
|
||||||
}
|
|
@ -12,3 +12,5 @@ export * from "./decrypt-string"
|
|||||||
export {default as decryptString} from "./decrypt-string"
|
export {default as decryptString} from "./decrypt-string"
|
||||||
export * from "./when-enter-pressed"
|
export * from "./when-enter-pressed"
|
||||||
export {default as whenEnterPressed} from "./when-enter-pressed"
|
export {default as whenEnterPressed} from "./when-enter-pressed"
|
||||||
|
export * from "./create-enum-map-from-translation"
|
||||||
|
export {default as createEnumMapFromTranslation} from "./create-enum-map-from-translation"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user