diff --git a/locale/de/translation.js b/locale/de/translation.js
index 814ebeb38..4207519c7 100644
--- a/locale/de/translation.js
+++ b/locale/de/translation.js
@@ -94,6 +94,37 @@ export const de_translation = {
paymentRequestMessage: 'Beschreibung (vom Händler)', //Description (from the merchant)
send: 'Senden', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: 'Privater Schlüssel', //Private Key
viewPrivateKey: 'Zeige privaten Schlüssel', //View Private Key?
@@ -318,6 +349,19 @@ export const de_translation = {
MN_COLLAT_NOT_SUITABLE: 'Dies ist keine gültige UTXO für eine Masternode', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Keine Verbindung zum RPC-Knoten möglich!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
PROPOSAL_FINALISED: 'Antrag finalisiert!', //Proposal finalized!
PROPOSAL_UNCONFIRMED: 'Der Antrag wurde noch nicht bestätigt.', //The proposal hasn\'t been confirmed yet.
PROPOSAL_EXPIRED: 'Der Antrag ist ausgelaufen. Erstelle einen neuen.', //The proposal has expired. Create a new one.
diff --git a/locale/en/translation.js b/locale/en/translation.js
index 688a1f5b0..cb363eb30 100644
--- a/locale/en/translation.js
+++ b/locale/en/translation.js
@@ -91,6 +91,42 @@ export const en_translation = {
paymentRequestMessage: 'Description (from the merchant)', //
send: 'Send', //
+ // Contacts System
+ receive: 'Receive', //
+ contacts: 'Contacts', //
+ name: 'Name', //
+ username: 'Username', //
+ addressOrXPub: 'Address or XPub', //
+ back: 'Back', //
+ chooseAContact: 'Choose a Contact', //
+ createContact: 'Create Contact', //
+ encryptFirstForContacts:
+ 'Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!', //
+ shareContactURL: 'Share Contact URL', //
+ setupYourContact: 'Setup your Contact', //
+ receiveWithContact: 'Receive using a simple username-based Contact', //
+ onlyShareContactPrivately:
+ 'Only share your Contact with trusted people (family, friends)', //
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: 'Change to', //
+ contact: 'Contact', //
+ xpub: 'XPub', //
+
+ addContactTitle: 'Add {strName} to Contacts', //
+ addContactSubtext:
+ "Once added you'll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice 'n easy.", //
+ addContactWarning:
+ 'Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!', //
+
+ editContactTitle: 'Change "{strName}" Contact', //
+ newName: 'New Name', //
+
+ removeContactTitle: 'Remove {strName}?', //
+ removeContactSubtext:
+ 'Are you sure you wish to remove {strName} from your Contacts?', //
+ removeContactNote: 'You can add them again any time in the future.', //
+
// Export
privateKey: 'Private Key', //
viewPrivateKey: 'View Private Key?', //
@@ -308,6 +344,25 @@ export const en_translation = {
MN_COLLAT_NOT_SUITABLE: 'This is not a suitable UTXO for a Masternode',
MN_CANT_CONNECT: 'Unable to connect to RPC node!',
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST:
+ 'You need to hit "{button}" before you can use Contacts!',
+ CONTACTS_NAME_REQUIRED: 'A name is required!',
+ CONTACTS_NAME_TOO_LONG: 'That name is too long!',
+ CONTACTS_CANNOT_ADD_YOURSELF: 'You cannot add yourself as a Contact!',
+ CONTACTS_ALREADY_EXISTS:
+ 'Contact already exists! You already saved this contact',
+ CONTACTS_NAME_ALREADY_EXISTS:
+ 'Contact name already exists! This could potentially be a phishing attempt, beware!',
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS:
+ 'Contact already exists! A contact is already called "{strNewName}"!',
+ CONTACTS_KEY_ALREADY_EXISTS:
+ 'Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts',
+ CONTACTS_NOT_A_CONTACT_QR: "This isn't a Contact QR!",
+ CONTACTS_ADDED:
+ 'New Contact added! {strName} has been added, hurray!',
+ CONTACTS_YOU_HAVE_NONE: 'You have no contacts!',
+
PROPOSAL_FINALISED: 'Proposal Launched!',
PROPOSAL_UNCONFIRMED: "The proposal hasn't confirmed yet",
PROPOSAL_EXPIRED: 'The proposal has expired. Create a new one.',
diff --git a/locale/fr/translation.js b/locale/fr/translation.js
index 90e3bb8c7..a583f8903 100644
--- a/locale/fr/translation.js
+++ b/locale/fr/translation.js
@@ -94,6 +94,37 @@ export const fr_translation = {
paymentRequestMessage: "Description (de l'opérateur)", //Description (from the merchant)
send: 'Envoyer', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: 'Clé privée', //Private Key
viewPrivateKey: 'Montrer la clé privée ?', //View Private Key?
@@ -320,6 +351,19 @@ export const fr_translation = {
"Il ne s'agit pas d'une UTXO appropriée pour un Masternode", //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Impossible de se connecter au nœud RPC!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
SWITCHED_EXPLORERS:
'Explorateur échangé! En utilisant maintenant le {explorerName}', //Switched explorer! Now using {explorerName}
SWITCHED_NODE: "Nœud commuté! L'utilisation de la {node}", //Switched node! Now using {node}
diff --git a/locale/ph/translation.js b/locale/ph/translation.js
index 499d03d6f..375aeffb7 100644
--- a/locale/ph/translation.js
+++ b/locale/ph/translation.js
@@ -95,6 +95,37 @@ export const ph_translation = {
paymentRequestMessage: 'Description (galing sa merchant)', //Description (from the merchant)
send: 'Ipadala', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: 'Private Key', //Private Key
viewPrivateKey: 'Tignan ang Private Key?', //View Private Key?
@@ -324,6 +355,19 @@ export const ph_translation = {
'Ang UTXO na ito ay hindi angkop para sa Masternode', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Hindi maka-konekta sa RPC node!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
SWITCHED_EXPLORERS:
'Nagpalit ng explorer!Ang gamit ngayon ay {explorerName}', //Switched explorer! Now using {explorerName}
SWITCHED_NODE: 'Nagpalit ng node!Ang gamit ngayon ay {node}', //Switched node! Now using {node}
diff --git a/locale/pt-br/translation.js b/locale/pt-br/translation.js
index 73f762cbb..8044dea48 100644
--- a/locale/pt-br/translation.js
+++ b/locale/pt-br/translation.js
@@ -94,6 +94,37 @@ export const pt_br_translation = {
paymentRequestMessage: 'Descrição (do comerciante)', //Description (from the merchant)
send: 'Enviar', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: 'Chave privada', //Private Key
viewPrivateKey: 'Mostrar a chave privada?', //View Private Key?
@@ -312,6 +343,19 @@ export const pt_br_translation = {
MN_COLLAT_NOT_SUITABLE: 'Este não é um UTXO adequado para um Masternode', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Não é possível conectar ao nó RPC!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
PROPOSAL_FINALISED: 'Proposta finalizada!', //Proposal finalized!
PROPOSAL_UNCONFIRMED: 'A proposta ainda não foi confirmada.', //The proposal hasn\'t been confirmed yet.
PROPOSAL_EXPIRED: 'A proposta expirou. Crie uma nova.', //The proposal has expired. Create a new one.
diff --git a/locale/pt-pt/translation.js b/locale/pt-pt/translation.js
index 09a25a196..0e1af8620 100644
--- a/locale/pt-pt/translation.js
+++ b/locale/pt-pt/translation.js
@@ -93,6 +93,37 @@ export const pt_pt_translation = {
paymentRequestMessage: 'Descrição (do comerciante)', //Description (from the merchant)
send: 'Enviar', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: 'Chave privada', //Private Key
viewPrivateKey: 'Mostrar a chave privada?', //View Private Key?
@@ -312,6 +343,19 @@ export const pt_pt_translation = {
MN_COLLAT_NOT_SUITABLE: 'Este não é um UTXO adequado para um Masternode', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Não é possível conectar ao nó RPC!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
PROPOSAL_FINALISED: 'Proposta finalizada!', //Proposal finalized!
PROPOSAL_UNCONFIRMED: 'A proposta ainda não foi confirmada.', //The proposal hasn\'t been confirmed yet.
PROPOSAL_EXPIRED: 'A proposta expirou. Crie uma nova.', //The proposal has expired. Create a new one.
diff --git a/locale/template/translation.js b/locale/template/translation.js
index b04ec7f24..6b3e1469a 100644
--- a/locale/template/translation.js
+++ b/locale/template/translation.js
@@ -106,6 +106,37 @@ var translation = {
paymentRequestMessage: '', //Description (from the merchant)
send: '', //Send
+ // Contacts System
+ receive: '', //Receive
+ contacts: '', //Contacts
+ name: '', //Name
+ username: '', //Username
+ addressOrXPub: '', //Address or XPub
+ back: '', //Back
+ chooseAContact: '', //Choose a Contact
+ createContact: '', //Create Contact
+ encryptFirstForContacts: '', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: '', //Share Contact URL
+ setupYourContact: '', //Setup your Contact
+ receiveWithContact: '', //Receive using a simple username-based Contact
+ onlyShareContactPrivately: '', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: '', //Change to
+ contact: '', //Contact
+ xpub: '', //XPub
+
+ addContactTitle: '', //Add {strName} to Contacts
+ addContactSubtext: '', //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning: '', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: '', //Change "{strName}" Contact
+ newName: '', //New Name
+
+ removeContactTitle: '', //Remove {strName}?
+ removeContactSubtext: '', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote: '', //You can add them again any time in the future.
+
// Export
privateKey: '', //Private Key
viewPrivateKey: '', //View Private Key?
@@ -289,6 +320,19 @@ var translation = {
MN_COLLAT_NOT_SUITABLE: '', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: '', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST: '', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: '', //A name is required!
+ CONTACTS_NAME_TOO_LONG: '', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: '', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS: '', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS: '', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS: '', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS: '', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: '', //This isn\'t a Contact QR!
+ CONTACTS_ADDED: '', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: '', //You have no contacts!
+
SWITCHED_EXPLORERS: '', //Switched explorer! Now using {explorerName}
SWITCHED_NODE: '', //Switched node! Now using {node}
SWITCHED_ANALYTICS: '', //Switched analytics level! Now {level}
diff --git a/locale/uwu/translation.js b/locale/uwu/translation.js
index fb391d473..381653445 100644
--- a/locale/uwu/translation.js
+++ b/locale/uwu/translation.js
@@ -93,6 +93,43 @@ export const uwu_translation = {
paymentRequestMessage: 'Deswiption (fwom da Mewrchant)', //Description (from the merchant)
send: 'Send', //Send
+ // Contacts System
+ receive: 'Receive', //Receive
+ contacts: 'Contactz', //Contacts
+ name: 'Name', //Name
+ username: 'Username', //Username
+ addressOrXPub: 'Addwess or XPubby', //Address or XPub
+ back: 'Backu', //Back
+ chooseAContact: 'Chowose a Contact', //Choose a Contact
+ createContact: 'Cweate Contactu', //Create Contact
+ encryptFirstForContacts:
+ 'Once yew hit "{button}" in the Dashboard, yew can cweate a Contact to make receiving PIV easier!', //Once you hit "{button}" in the Dashboard, you can create a Contact to make receiving PIV easier!
+ shareContactURL: 'Share Fren URL', //Share Contact URL
+ setupYourContact: 'Setup ur Contact', //Setup your Contact
+ receiveWithContact: 'Receive using a simp-le username-based Contact', //Receive using a simple username-based Contact
+ onlyShareContactPrivately:
+ 'Only share ur Contact with trusted peeps (family, friends)', //Only share your Contact with trusted people (family, friends)
+
+ /* Context: The "Change to" is used in-app with one of the Three options below it, i.e: "Change to Contact" */
+ changeTo: 'Change tew', //Change to
+ contact: 'Contactu', //Contact
+ xpub: 'XPubby', //XPub
+
+ addContactTitle: 'Add {strName} tew Contacts', //Add {strName} to Contacts
+ addContactSubtext:
+ "Once added you'll be ablwe tew send transactions tew {strName} by their name (either typing, or clicking), no more addwesses, nice 'n OwO.", //Once added you\'ll be able to send transactions to {strName} by their name (either typing, or clicking), no more addresses, nice \'n easy.
+ addContactWarning:
+ 'Ensure dat dis is da real "{strName}", do not accept Contact requests fwom unknown sources!', //Ensure that this is the real "{strName}", do not accept Contact requests from unknown sources!
+
+ editContactTitle: 'Change "{strName}" Contactu', //Change "{strName}" Contact
+ newName: 'Neww Name', //New Name
+
+ removeContactTitle: 'Unfren {strName}?', //Remove {strName}?
+ removeContactSubtext:
+ 'Are u sure u wish to remove {strName} from your Fren list?', //Are you sure you wish to remove {strName} from your Contacts?
+ removeContactNote:
+ 'Yew can add dem again any time in the future, but... :(', //You can add them again any time in the future.
+
// Export
privateKey: 'Pwivate Key', //Private Key
viewPrivateKey: 'View Pwivate Key?', //View Private Key?
@@ -309,6 +346,25 @@ export const uwu_translation = {
MN_COLLAT_NOT_SUITABLE: 'Dis is not a suitable UTXO for a Masternowode', //This is not a suitable UTXO for a Masternode
MN_CANT_CONNECT: 'Unable to connect to RPC nowode!', //Unable to connect to RPC node!
+ /* Contacts System Alerts */
+ CONTACTS_ENCRYPT_FIRST:
+ 'Yew need tew hit "{button}" before yew can use Contacts!', //You need to hit "{button}" before you can use Contacts!
+ CONTACTS_NAME_REQUIRED: 'A name iz required!', //A name is required!
+ CONTACTS_NAME_TOO_LONG: 'That name iz teww long!', //That name is too long!
+ CONTACTS_CANNOT_ADD_YOURSELF: 'Yew cannot add urself as a Contact!', //You cannot add yourself as a Contact!
+ CONTACTS_ALREADY_EXISTS:
+ 'Contact already exists! Yew already saved dis contact', //Contact already exists! You already saved this contact
+ CONTACTS_NAME_ALREADY_EXISTS:
+ 'Contact name already exists! Dis could potentially be a phishing attempt, beware!', //Contact name already exists! This could potentially be a phishing attempt, beware!
+ CONTACTS_EDIT_NAME_ALREADY_EXISTS:
+ 'Contact already exists! A contact iz already cawlled "{strNewName}"!', //Contact already exists! A contact is already called "{strNewName}"!
+ CONTACTS_KEY_ALREADY_EXISTS:
+ 'Contact already exists, buh under a different name! Yew have {newName} saved as {oldName} in ur contacts', //Contact already exists, but under a different name! You have {newName} saved as {oldName} in your contacts
+ CONTACTS_NOT_A_CONTACT_QR: "Dis isn't a Contact QR, baka!", //This isn\'t a Contact QR!
+ CONTACTS_ADDED:
+ 'New Contact added! {strName} haz been added, hurray!', //New Contact added! {strName} has been added, hurray!
+ CONTACTS_YOU_HAVE_NONE: 'Yew have no contacts! Lonely!', //You have no contacts!
+
PROPOSAL_FINALISED: 'Pwoposal finalized!', //Proposal finalized!
PROPOSAL_UNCONFIRMED: "Da pwoposal hasn't been confirmed yet.", //The proposal hasn't been confirmed yet.
PROPOSAL_EXPIRED: 'Da pwoposal has expired. Cweate a new one.', //The proposal has expired. Create a new one.
diff --git a/scripts/contacts-book.js b/scripts/contacts-book.js
new file mode 100644
index 000000000..270efee68
--- /dev/null
+++ b/scripts/contacts-book.js
@@ -0,0 +1,1081 @@
+import { Buffer } from 'buffer';
+import { Database } from './database';
+import { doms, toClipboard } from './global';
+import { ALERTS, translation } from './i18n';
+import {
+ confirmPopup,
+ createAlert,
+ createQR,
+ getImageFile,
+ isStandardAddress,
+ isXPub,
+ sanitizeHTML,
+} from './misc';
+import { scanQRCode } from './scanner';
+import { getDerivationPath, hasEncryptedWallet, masterKey } from './wallet';
+
+/**
+ * Represents an Account contact
+ */
+export class Contact {
+ /**
+ * Creates a new Account contact
+ * @param {Object} options - The contact options
+ * @param {string} options.label - The label of the contact
+ * @param {string} options.icon - The optional icon of the contact (base64)
+ * @param {string} options.pubkey - The Master public key of the contact
+ * @param {number} options.date - The date (unix timestamp) of the contact being saved
+ */
+ constructor({ label, icon, pubkey, date }) {
+ this.label = label;
+ this.icon = icon;
+ this.pubkey = pubkey;
+ this.date = date;
+ }
+
+ /** The label of the Contact
+ * @type {string}
+ */
+ label;
+
+ /** The optional icon of the Contact (base64)
+ * @type {string}
+ */
+ icon;
+
+ /** The Master public key of the Contact
+ * @type {string}
+ */
+ pubkey;
+
+ /** The date (unix timestamp) of the Contact being saved
+ * @type {number}
+ */
+ date;
+}
+
+/**
+ * Add a Contact to an Account's contact list
+ * @param {{publicKey: String, encWif: String?, localProposals: Array, contacts: Array}} account - The account to add the Contact to
+ * @param {Contact} contact - The contact object
+ */
+export async function addContact(account, contact) {
+ // TODO: once multi-account is added, ensure this function adds the contact to the correct account (by pubkey)
+ const cDB = await Database.getInstance();
+
+ // Push contact in to the account
+ const arrContacts = account?.contacts || [];
+ arrContacts.push(contact);
+
+ // Save to the DB
+ await cDB.addAccount({
+ publicKey: account.publicKey,
+ encWif: account.encWif,
+ localProposals: account?.localProposals || [],
+ contacts: arrContacts,
+ name: account?.name || '',
+ });
+}
+
+/**
+ * Search for a Contact in a given Account, by specific properties
+ * @param {{publicKey: String, encWif: String?, localProposals: Array, contacts: Array}} account - The account to search for a Contact
+ * @param {Object} settings
+ * @param {string?} settings.name - The Name of the contact to search for
+ * @param {string?} settings.pubkey - The Pubkey of the contact to search for
+ * @returns {Contact?} - A Contact, if found
+ */
+export function getContactBy(account, { name, pubkey }) {
+ if (!name && !pubkey)
+ throw Error(
+ 'getContactBy(): At least ONE search parameter MUST be set!'
+ );
+ const arrContacts = account?.contacts || [];
+
+ // Get by Name
+ if (name) return arrContacts.find((a) => a.label === name);
+ // Get by Pubkey
+ if (pubkey) return arrContacts.find((a) => a.pubkey === pubkey);
+
+ // Nothing found
+ return null;
+}
+
+/**
+ * Remove a Contact from an Account's contact list
+ * @param {{publicKey: String, encWif: String?, localProposals: Array, contacts: Array}} account - The account to remove the Contact from
+ * @param {string} pubkey - The contact pubkey
+ */
+export async function removeContact(account, pubkey) {
+ // TODO: once multi-account is added, ensure this function adds the contact to the correct account (by pubkey)
+ const cDB = await Database.getInstance();
+
+ // Find the contact by index, if it exists; splice it away
+ const arrContacts = account?.contacts || [];
+ const nIndex = arrContacts.findIndex((a) => a.pubkey === pubkey);
+ if (nIndex > -1) {
+ // Splice out the contact, and save to DB
+ arrContacts.splice(nIndex, 1);
+ await cDB.addAccount({
+ publicKey: account.publicKey,
+ encWif: account.encWif,
+ localProposals: account?.localProposals || [],
+ contacts: account?.contacts || [],
+ name: account?.name || '',
+ });
+ }
+}
+
+/**
+ * Render an Account's contact list
+ * @param {{publicKey: String, encWif: String?, localProposals: Array, contacts: Array}} account
+ * @param {boolean} fPrompt - If this is a Contact Selection prompt
+ */
+export async function renderContacts(account, fPrompt = false) {
+ let strHTML = '';
+ let i = 0;
+
+ // For non-prompts: we allow the user to Add, Edit or Delete their contacts
+ if (!fPrompt) {
+ // Render an editable Contacts Table
+ for (const cContact of account.contacts || []) {
+ const strPubkey = isXPub(cContact.pubkey)
+ ? cContact.pubkey.slice(0, 32) + '…'
+ : cContact.pubkey;
+ strHTML += `
+
+ `;
+ i++;
+ }
+
+ // Lastly, inject the "Add Account" UI to the table
+ strHTML += `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ doms.domContactsTable.innerHTML = strHTML;
+ } else {
+ // For prompts: the user must click an address (or cancel), and cannot add, edit or delete contacts
+ strHTML += `
`;
+
+ // Prepare the Contact list Prompt
+ const cPrompt = getUserContactClick();
+
+ // Hook the Contact Prompt to the Popup UI, which resolves when the user has interacted with the Contact Prompt
+ return await confirmPopup({
+ title: translation.chooseAContact,
+ html: strHTML,
+ resolvePromise: cPrompt(),
+ purpleModal: true,
+ textLeft: true,
+ noPadding: true,
+ maxHeight: 450,
+ });
+ }
+}
+
+/**
+ * Creates and returns a function that returns a promise for a click event.
+ *
+ * The promise will resolve with the Contact Name of whichever button is clicked first.
+ *
+ * Once a button is clicked, all remaining listeners are removed.
+ */
+function getUserContactClick() {
+ // Specify the function to return
+ return function () {
+ // Note that the return type is a Promise, this will wait on the click
+ return new Promise((resolve, _reject) => {
+ // Wait a bit for the DOM to fully render, then setup the handler functions + attach them to the Contact Buttons via Event Listeners
+ setTimeout(() => {
+ // The function to handle the click
+ function handleClick(event) {
+ // If this is the Exit button (a -1 index), just silently quit
+ if (event.target.id.endsWith('-1')) return resolve('');
+
+ // Splice the 'Contact Index' from the button clicked
+ const nIndex = event.target.id.match(/([0-9]+)$/)[0];
+ // Fetch the associated Contact Name from the table
+ // TODO: maybe don't rely on the table, and just fetch the Contact Index from the DB Contacts?
+ const strName = document.getElementById(
+ `contactsName${nIndex}`
+ ).innerText;
+ // Resolve the promise with the Contact Name of the button that was clicked first
+ resolve(strName);
+ // Remove all the remaining click listeners
+ removeRemainingListeners();
+ }
+
+ // The function to iterate over the buttons and remove their listeners
+ function removeRemainingListeners() {
+ let i = -1;
+ let button;
+ // This iteration removes the listener from each button
+ // eslint-disable-next-line no-cond-assign
+ while (
+ (button = document.getElementById(
+ `contactsSelector${i}`
+ ))
+ ) {
+ button.removeEventListener('click', handleClick);
+ i++;
+ }
+ }
+
+ // Attach a click listener to each `contactsSelector` button
+ let i = -1;
+ let button;
+ // eslint-disable-next-line no-cond-assign
+ while (
+ (button = document.getElementById(`contactsSelector${i}`))
+ ) {
+ button.addEventListener('click', handleClick, {
+ once: true,
+ });
+ i++;
+ }
+ }, 500); // Waits 500ms to ensure the all the elements have been added to the DOM (yeah, not the most elegant, but cannot think of a better solution yet)
+ });
+ };
+}
+
+/**
+ * A function that uses the Prompt system to ask the user for a contact
+ */
+export async function promptForContact() {
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+ if (!cAccount || (cAccount.contacts && cAccount.contacts.length === 0))
+ return createAlert('warning', ALERTS.CONTACTS_YOU_HAVE_NONE, [], 2500);
+ return renderContacts(cAccount, true);
+}
+
+/**
+ * A GUI button wrapper that fills an Input with a user-selected Contact
+ * @param {HTMLInputElement} domInput - The input box to fill with a selected Contact Address
+ */
+export async function guiSelectContact(domInput) {
+ // Fill the 'Input box' with a user-chosen Contact
+ domInput.value = (await promptForContact()) || '';
+
+ // Run the validity checker for double-safety
+ await guiCheckRecipientInput({ target: domInput });
+}
+
+/**
+ * A GUI wrapper that renders the current Account's contacts list
+ */
+export async function guiRenderContacts() {
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+
+ if (!cAccount || !cAccount.contacts) {
+ return createAlert(
+ 'warning',
+ ALERTS.CONTACTS_ENCRYPT_FIRST,
+ [{ button: translation.secureYourWallet }],
+ 3500
+ );
+ }
+
+ return renderContacts(cAccount);
+}
+
+/**
+ * Set the current Account's Contact name
+ * @param {{publicKey: String, encWif: String?, localProposals: Array, contacts: Array, name: String?}} account - The account to add the new Name to
+ * @param {String} name - The name to set
+ */
+export async function setAccountContactName(account, name) {
+ const cDB = await Database.getInstance();
+
+ // Save name to the DB
+ await cDB.addAccount({
+ publicKey: account.publicKey,
+ encWif: account.encWif,
+ localProposals: account?.localProposals || [],
+ contacts: account?.contacts || [],
+ name: name,
+ });
+}
+
+/**
+ * Render the Receive Modal with either our Contact or Address
+ * @param {boolean} fContact - `true` to render our Contact, `false` to render our current Address
+ */
+export async function guiRenderReceiveModal(
+ cReceiveType = RECEIVE_TYPES.CONTACT
+) {
+ if (cReceiveType === RECEIVE_TYPES.CONTACT) {
+ // Fetch Contact info from the current Account
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+
+ // Check that a local Contact name was set
+ if (cAccount?.name) {
+ // Derive our Public Key
+ let strPubkey = '';
+
+ // If HD: use xpub, otherwise we'll fallback to our single address
+ if (masterKey.isHD) {
+ // Get our current wallet XPub
+ const derivationPath = getDerivationPath(
+ masterKey.isHardwareWallet
+ )
+ .split('/')
+ .slice(0, 4)
+ .join('/');
+ strPubkey = await masterKey.getxpub(derivationPath);
+ } else {
+ strPubkey = await masterKey.getCurrentAddress();
+ }
+
+ // Construct the Contact Share URI
+ const strContactURI = await localContactToURI(cAccount, strPubkey);
+
+ // Render Copy Button
+ doms.domModalQrLabel.innerHTML = `${translation.shareContactURL}`;
+
+ // We'll render a short informational text, alongside a QR below for Contact scanning
+ doms.domModalQR.innerHTML = `
+
${translation.onlyShareContactPrivately}
+
+ `;
+ const domQR = document.getElementById('receiveModalEmbeddedQR');
+ createQR(strContactURI, domQR, 10);
+ domQR.firstChild.style.width = '100%';
+ domQR.firstChild.style.height = 'auto';
+ domQR.firstChild.classList.add('no-antialias');
+ document.getElementById('clipboard').value = strPubkey;
+ } else {
+ // Get our current wallet address
+ const strAddress = await masterKey.getCurrentAddress();
+
+ // Update the QR Label (we'll show the address here for now, user can set Contact "Name" optionally later)
+ doms.domModalQrLabel.innerHTML =
+ strAddress +
+ ``;
+
+ // Update the QR section
+ if (await hasEncryptedWallet()) {
+ doms.domModalQR.innerHTML = `
+
+ `;
+
+ // Hook the Contact Prompt to the Popup UI, which resolves when the user has interacted with the Contact Prompt
+ const fAdd = await confirmPopup({
+ title: translation.addContactTitle.replace('{strName}', strName),
+ html: strHTML,
+ });
+
+ // If accepted, then we add to contacts!
+ if (fAdd) {
+ // Add the Contact to the current account
+ await addContact(cAccount, {
+ label: strName,
+ pubkey: strPubkey,
+ date: Date.now(),
+ });
+
+ // Notify
+ createAlert(
+ 'success',
+ ALERTS.CONTACTS_ADDED,
+ [{ strName: strName }],
+ 3000
+ );
+ }
+
+ // Return if the user accepted or declined
+ return fAdd;
+}
+
+/**
+ * Prompt the user to edit a contact by it's original name
+ *
+ * The new name will be taken from the internal prompt input
+ * @param {number} nIndex - The DB index of the Contact to edit
+ * @returns {Promise} - `true` if contact was edited, `false` if not
+ */
+export async function guiEditContactNamePrompt(nIndex) {
+ // Fetch the desired Contact to edit
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+ const cContact = cAccount.contacts[nIndex];
+
+ // Render an 'Add to Contacts' UI
+ const strHTML = `
+
+ `;
+
+ // Hook the Contact Prompt to the Popup UI, which resolves when the user has interacted with the Contact Prompt
+ const fContinue = await confirmPopup({
+ title: translation.editContactTitle.replace(
+ '{strName}',
+ sanitizeHTML(cContact.label)
+ ),
+ html: strHTML,
+ });
+ if (!fContinue) return false;
+
+ // Verify the name
+ const strNewName = document.getElementById('contactsNewNameInput').value;
+ if (strNewName.length < 1) {
+ createAlert('warning', ALERTS.CONTACTS_NAME_REQUIRED, [], 2500);
+ return false;
+ }
+ if (strNewName.length > 32) {
+ createAlert('warning', ALERTS.CONTACTS_NAME_TOO_LONG, [], 2500);
+ return false;
+ }
+
+ // Check this new Name isn't already saved
+ const cContactByNewName = getContactBy(cAccount, { name: strNewName });
+ if (cContactByNewName) {
+ createAlert(
+ 'warning',
+ ALERTS.CONTACTS_EDIT_NAME_ALREADY_EXISTS,
+ [{ strNewName: strNewName }],
+ 4500
+ );
+ return false;
+ }
+
+ // Edit it (since it's a pointer to the Account's Contacts)
+ cContact.label = strNewName;
+
+ // Commit to DB
+ await cDB.addAccount({
+ publicKey: cAccount.publicKey,
+ encWif: cAccount.encWif,
+ localProposals: cAccount?.localProposals || [],
+ contacts: cAccount?.contacts || [],
+ name: cAccount?.name,
+ });
+
+ // Re-render the Contacts UI
+ await renderContacts(cAccount);
+
+ // Return if the user accepted or declined
+ return true;
+}
+
+/**
+ * Prompt the user to add an image to a contact by it's DB index
+ *
+ * The new image will be taken from the internal system prompt
+ * @param {number} nIndex - The DB index of the Contact to edit
+ * @returns {Promise} - `true` if contact was edited, `false` if not
+ */
+export async function guiAddContactImage(nIndex) {
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+ const cContact = cAccount.contacts[nIndex];
+
+ // Prompt for the image
+ const strImage = await getImageFile();
+ if (!strImage) return false;
+
+ // Fetch the original contact, edit it (since it's a pointer to the Account's Contacts)
+ cContact.icon = strImage;
+
+ // Commit to DB
+ await cDB.addAccount({
+ publicKey: cAccount.publicKey,
+ encWif: cAccount.encWif,
+ localProposals: cAccount?.localProposals || [],
+ contacts: cAccount?.contacts || [],
+ name: cAccount?.name,
+ });
+
+ // Re-render the Contacts UI
+ await renderContacts(cAccount);
+
+ // Return that the edit was successful
+ return true;
+}
+
+/**
+ * A GUI wrapper to open a QR Scanner prompt for Contact imports
+ * @returns {boolean} - `true` if contact was added, `false` if not
+ */
+export async function guiAddContactQRPrompt() {
+ const cScan = await scanQRCode();
+
+ // Empty (i.e: rejected or no camera) can just silently exit
+ if (!cScan) return false;
+
+ // MPW Contact Request URI
+ if (cScan?.data?.includes('addcontact=')) {
+ // Parse as URL Params
+ const cURL = new URL(cScan.data);
+ const urlParams = new URLSearchParams(cURL.search);
+ const strURI = urlParams.get('addcontact');
+
+ // Sanity check the URI
+ if (strURI?.includes(':')) {
+ // Split to 'name' and 'pubkey'
+ const arrParts = strURI.split(':');
+
+ // Convert Name from HEX to UTF-8
+ const strName = Buffer.from(arrParts[0], 'hex').toString('utf8');
+ const strPubkey = arrParts[1];
+
+ // Prompt the user to add the Contact
+ const fAdded = await guiAddContactPrompt(
+ sanitizeHTML(strName),
+ strPubkey
+ );
+
+ // Re-render the list
+ await guiRenderContacts();
+
+ // Return the status
+ return fAdded;
+ }
+ } else {
+ createAlert('warning', ALERTS.CONTACTS_NOT_A_CONTACT_QR, [], 2500);
+ return false;
+ }
+}
+
+/** A GUI wrapper that removes a contact from the current Account's contacts list */
+export async function guiRemoveContact(index) {
+ // Fetch the current Account
+ const cDB = await Database.getInstance();
+ const cAccount = await cDB.getAccount();
+
+ // Fetch the Contact
+ const cContact = cAccount.contacts[index];
+
+ // Confirm the deletion
+ const fConfirmed = await confirmPopup({
+ title: translation.removeContactTitle.replace(
+ '{strName}',
+ sanitizeHTML(cContact.label)
+ ),
+ html: `
+