diff --git a/public/locales/en.json b/public/locales/en.json index e7123c5..956faaf 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -269,6 +269,31 @@ "errorWalletLimitDescription": "You currently cannot add any more wallets." }, + "addContact": { + "modalTitle": "Add contact", + "modalTitleEdit": "Edit contact", + + "buttonSubmit": "Add", + "buttonSubmitEdit": "Save", + + "contactLabel": "Label", + "contactLabelPlaceholder": "Contact label (optional)", + "contactLabelMaxLengthError": "No longer than 32 characters", + "contactLabelWhitespaceError": "Must not be only spaces", + + "contactAddressLabel": "Address or name", + + "messageSuccessAdd": "Added contact successfully!", + "messageSuccessEdit": "Saved contact successfully!", + + "errorDuplicateContactTitle": "Contact already exists", + "errorDuplicateContactDescription": "You already have a contact for that address.", + "errorMissingContactTitle": "Contact not found", + "errorMissingContactDescription": "The contact you are trying to edit no longer exists.", + "errorContactLimitTitle": "Contact limit reached", + "errorContactLimitDescription": "You currently cannot add any more contacts." + }, + "dashboard": { "siteTitle": "Dashboard", @@ -804,9 +829,12 @@ "addressPicker": { "placeholder": "Choose a recipient", "placeholderWalletsOnly": "Choose a wallet", + "placeholderNoWallets": "Address or name", + "placeholderNoWalletsNoNames": "Address", "hintCurrentBalance": "Current balance: <1 />", + "errorAddressRequired": "Address is required.", "errorRecipientRequired": "Recipient is required.", "errorWalletRequired": "Wallet is required.", diff --git a/src/components/addresses/picker/AddressPicker.tsx b/src/components/addresses/picker/AddressPicker.tsx index 8961663..bd66f64 100644 --- a/src/components/addresses/picker/AddressPicker.tsx +++ b/src/components/addresses/picker/AddressPicker.tsx @@ -33,6 +33,7 @@ otherPickerValue?: string; walletsOnly?: boolean; + noWallets?: boolean; noNames?: boolean; nameHint?: boolean; @@ -55,6 +56,7 @@ otherPickerValue, walletsOnly, + noWallets, noNames, nameHint, @@ -117,9 +119,9 @@ ? [ ...(exactAddressItem ? [exactAddressItem] : []), ...(exactNameItem ? [exactNameItem] : []), - ...options + ...(!noWallets ? options : []) ] - : options; + : (!noWallets ? options : []); // Fetch an address or name hint if possible const { pickerHints, foundName } = usePickerHints( @@ -131,8 +133,18 @@ form?.validateFields([name]); }, [form, name, foundName, otherPickerValue]); + function getPlaceholder() { + if (walletsOnly) return t("addressPicker.placeholderWalletsOnly"); + if (noWallets) { + if (noNames) return t("addressPicker.placeholderNoWalletsNoNames"); + else return t("addressPicker.placeholderNoWallets"); + } + return t("addressPicker.placeholder"); + } + const classes = classNames("address-picker", className, { "address-picker-wallets-only": walletsOnly, + "address-picker-no-wallets": noWallets, "address-picker-no-names": noNames, "address-picker-has-exact-address": hasExactAddress, "address-picker-has-exact-name": hasExactName, @@ -154,7 +166,9 @@ rules={[ { required: true, message: walletsOnly ? t("addressPicker.errorWalletRequired") - : t("addressPicker.errorRecipientRequired")}, + : (noWallets + ? t("addressPicker.errorAddressRequired") + : t("addressPicker.errorRecipientRequired"))}, // Address/name regexp { @@ -172,8 +186,11 @@ } else { // Validate addresses and names const nameRegexp = getNameRegex(nameSuffix); - if (!addressRegexp.test(value) && !nameRegexp.test(value)) - throw t("addressPicker.errorInvalidRecipient"); + if (!addressRegexp.test(value) && !nameRegexp.test(value)) { + if (noWallets) + throw t("addressPicker.errorInvalidAddress"); + else throw t("addressPicker.errorInvalidRecipient"); + } } } }, @@ -210,9 +227,7 @@ dropdownMatchSelectWidth={false} // Change the placeholder to 'Choose a wallet' if applicable - placeholder={walletsOnly - ? t("addressPicker.placeholderWalletsOnly") - : t("addressPicker.placeholder")} + placeholder={getPlaceholder()} // Show a clear button on the input for convenience allowClear diff --git a/src/krist/contacts/functions/editContact.ts b/src/krist/contacts/functions/editContact.ts index 700fa41..ef7b9e1 100644 --- a/src/krist/contacts/functions/editContact.ts +++ b/src/krist/contacts/functions/editContact.ts @@ -1,8 +1,6 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { v4 as uuid } from "uuid"; - import { store } from "@app"; import * as actions from "@actions/ContactsActions"; @@ -18,7 +16,7 @@ */ export function editContact( contact: Contact, - updated: Contact + updated: ContactNew ): void { const finalContact = { ...contact, diff --git a/src/pages/contacts/AddContactModal.tsx b/src/pages/contacts/AddContactModal.tsx new file mode 100644 index 0000000..0a8d309 --- /dev/null +++ b/src/pages/contacts/AddContactModal.tsx @@ -0,0 +1,158 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { useState } from "react"; +import { Modal, Form, Input, message, notification } from "antd"; + +import { useTFns } from "@utils/i18n"; + +import { ADDRESS_LIST_LIMIT } from "@wallets"; +import { Contact, useContacts, addContact, editContact } from "@contacts"; +import { useAddressPrefix, useNameSuffix, getNameParts } from "@utils/currency"; + +import { AddressPicker } from "@comp/addresses/picker/AddressPicker"; + +interface FormValues { + label?: string; + address: string; +} + +interface Props { + editing?: Contact; + + visible: boolean; + setVisible: React.Dispatch>; +} + +export function AddContactModal({ + editing, + + visible, + setVisible +}: Props): JSX.Element { + const { t, tStr } = useTFns("addContact."); + + const [form] = Form.useForm(); + const [address, setAddress] = useState(""); + + // Required to check for existing contacts + const { contacts, contactAddressMap } = useContacts(); + const addressPrefix = useAddressPrefix(); + const nameSuffix = useNameSuffix(); + + function closeModal() { + setVisible(false); + form.resetFields(); + setAddress(""); + } + + async function onSubmit() { + const values = await form.validateFields(); + if (!values.address) return; + + const { label, address } = values; + const isName = !!getNameParts(nameSuffix, address); + + if (editing) { // Edit contact + // Double check the destination contact exists + if (!contacts[editing.id]) return notification.error({ + message: tStr("errorMissingContactTitle"), + description: tStr("errorMissingContactDescription") + }); + + // If the address changed, check that a contact doesn't already exist + // with this address + if (editing.address !== address && !!contactAddressMap[address]) { + return notification.error({ + message: tStr("errorDuplicateContactTitle"), + description: tStr("errorDuplicateContactDescription") + }); + } + + // Perform the edit + editContact(editing, { label, address, isName }); + message.success(tStr("messageSuccessEdit")); + + closeModal(); + } else { // Add contact + // Check if we reached the contact limit + if (Object.keys(contacts).length >= ADDRESS_LIST_LIMIT) { + return notification.error({ + message: tStr("errorContactLimitTitle"), + description: tStr("errorContactLimitDescription") + }); + } + + // Check if the contact already exists + if (contactAddressMap[address]) { + return notification.error({ + message: tStr("errorDuplicateContactTitle"), + description: tStr("errorDuplicateContactDescription") + }); + } + + // Add the contact + addContact({ label, address, isName }); + message.success(tStr("messageSuccessAdd")); + + closeModal(); + } + } + + function onValuesChange(_: unknown, values: Partial) { + setAddress(values.address || ""); + } + + return +
+ {/* Contact label */} + + + + + {/* Contact address */} + + +
; +} diff --git a/src/pages/contacts/ContactsPage.tsx b/src/pages/contacts/ContactsPage.tsx index d5ab367..5acb1ff 100644 --- a/src/pages/contacts/ContactsPage.tsx +++ b/src/pages/contacts/ContactsPage.tsx @@ -10,6 +10,7 @@ import { PageLayout } from "@layout/PageLayout"; import { useContacts } from "@contacts"; +import { AddContactModal } from "./AddContactModal"; /** Contact count subtitle */ function ContactsPageSubtitle(): JSX.Element { @@ -30,10 +31,15 @@ return <> {/* Add contact */} - - {/* TODO: modal */} + + ; } @@ -43,6 +49,5 @@ subTitle={} extra={} > - ; } diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 935dbe8..3754f00 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -244,7 +244,10 @@ { whitespace: true, message: t("addWallet.walletLabelWhitespaceError") } ]} > - +