diff --git a/src/assets/icons/add-circle-outline.svg b/src/assets/icons/add-circle-outline.svg new file mode 100644 index 0000000..6ec8c38 --- /dev/null +++ b/src/assets/icons/add-circle-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-up-circle-outline.svg b/src/assets/icons/arrow-up-circle-outline.svg new file mode 100644 index 0000000..9c55bec --- /dev/null +++ b/src/assets/icons/arrow-up-circle-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/download-outline.svg b/src/assets/icons/download-outline.svg new file mode 100644 index 0000000..f9af237 --- /dev/null +++ b/src/assets/icons/download-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 7483db2..54d0902 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -3,12 +3,14 @@ import React, { useRef, useEffect, useState } from 'react'; interface ModalProps { show: boolean; + className?: string; onClose: () => {}; } export function Modal({ show, onClose, + className, children }: React.PropsWithChildren) { const modalRef = useRef(null); @@ -34,7 +36,7 @@ export function Modal({ // }, [show]); return show ? ( -
+
{children}
diff --git a/src/options.tsx b/src/options.tsx index c6a5bfc..b7df781 100644 --- a/src/options.tsx +++ b/src/options.tsx @@ -15,11 +15,14 @@ import { import * as Storage from './storage'; import { getPermissionsString, isHexadecimal, isValidRelayURL } from './common'; import logotype from './assets/logo/logotype.png'; -import WarningIcon from './assets/icons/warning-outline.svg'; +import AddCircleIcon from './assets/icons/add-circle-outline.svg'; +import ArrowUpCircleIcon from './assets/icons/arrow-up-circle-outline.svg'; import CopyIcon from './assets/icons/copy-outline.svg'; import DiceIcon from './assets/icons/dice-outline.svg'; +import DownloadIcon from './assets/icons/download-outline.svg'; import RadioIcon from './assets/icons/radio-outline.svg'; import TrashIcon from './assets/icons/trash-outline.svg'; +import WarningIcon from './assets/icons/warning-outline.svg'; type RelayConfig = { url: string; @@ -30,8 +33,10 @@ function Options() { let [selectedProfilePubKey, setSelectedProfilePubKey] = useState(''); let [profiles, setProfiles] = useState({}); let [isLoadingProfile, setLoadingProfile] = useState(false); - let [profileJson, setProfileJson] = useState(''); - let [isModalShown, setModalShown] = useState(false); + let [profileExportJson, setProfileExportJson] = useState(''); + let [profileImportJson, setProfileImportJson] = useState(''); + let [isExportModalShown, setExportModalShown] = useState(false); + let [isImportModalShown, setImportModalShown] = useState(false); let [privateKey, setPrivateKey] = useState(''); let [isKeyHidden, setKeyHidden] = useState(true); @@ -94,7 +99,11 @@ function Options() { loadAndSelectProfile(selectedProfilePubKey); }, [selectedProfilePubKey]); - const showMessage = useCallback((msg, type = 'info', timeout = 3000) => { + const showMessage: ( + msg: string, + type?: 'info' | 'success' | 'warning', + timeout?: number + ) => {} = useCallback((msg, type = 'info', timeout = 3000) => { setMessageType(type); setMessage(msg); if (timeout > 0) { @@ -160,20 +169,82 @@ function Options() { function handleExportProfileClick() { const profile = getSelectedProfile(); const profileJson = JSON.stringify(profile); - setProfileJson(profileJson); - setModalShown(true); + setProfileExportJson(profileJson); + setExportModalShown(true); } function handleExportProfileCopyClick() { - navigator.clipboard.writeText(profileJson); + navigator.clipboard.writeText(profileExportJson); + } + + function handleExportModalClose() { + setExportModalShown(false); + } + + function handleImportProfileClick() { + setImportModalShown(true); + } + + function handleChangeProfileImportJson(e) { + setProfileImportJson(e.target.value); + } + + async function handleImportProfileImportClick() { + let newProfile: ProfileConfig; + // validations + try { + newProfile = JSON.parse(profileImportJson); + } catch (error) { + console.warn(`Error parsing the entered JSON`, error); + showMessage( + `There was an error parsing the JSON. ${error.message}`, + 'warning' + ); + return; + } + if (!newProfile) { + console.warn(`The imported profile is empty.`); + showMessage(`The imported profile is invalid.`, 'warning'); + } + + // store the new profile + await Storage.addProfile(newProfile); + + const newPubKey = getPublicKey(newProfile.privateKey); + setProfiles({ ...profiles, ...{ [newPubKey]: newProfile } }); + + // now load in the component + if (newProfile.privateKey) { + setPrivateKey(nip19.nsecEncode(newProfile.privateKey)); + } else { + setPrivateKey(''); + } + setSelectedProfilePubKey(newPubKey); + + setImportModalShown(false); } - function handleModalClose() { - setModalShown(false); + function handleImportModalClose() { + setImportModalShown(false); } - function saveProfiles() { - Storage.updateProfiles(profiles); + async function handleDeleteProfileClick(e) { + e.preventDefault(); + if ( + window.confirm( + `Delete the profile "${nip19.npubEncode(selectedProfilePubKey)}"?` + ) + ) { + const updateProfiles = profiles; + delete updateProfiles[selectedProfilePubKey]; + console.debug('updated profiles', updateProfiles); + setProfiles(updateProfiles); + await saveProfiles(); + } + } + + async function saveProfiles() { + await Storage.updateProfiles(profiles); } //#endregion Profiles @@ -205,7 +276,7 @@ function Options() { delete profiles[selectedProfilePubKey]; setSelectedProfilePubKey(newPubKey); // this re-loads the profile in the screen - saveProfiles(); + await saveProfiles(); } else { console.warn('Saving and empty private key'); } @@ -403,9 +474,24 @@ function Options() { disabled={isNewProfilePending()} onClick={handleNewProfileClick} > + New profile - + + +
@@ -551,16 +637,33 @@ function Options() {
version {version}
- +

This is the JSON that represents your profile (WARNING: it contains your private key):

- {profileJson} + {profileExportJson}
+ + +

Paste the profile JSON in the following box:

+ + +
); } diff --git a/src/storage.ts b/src/storage.ts index 074757f..f8fd8c1 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -28,6 +28,10 @@ export async function readActiveRelays(): Promise { export async function updateRelays(profilePublicKey: string, newRelays) { if (newRelays) { const profile = await getProfile(profilePublicKey); + if (!profile) { + console.warn(`There is no profile with the key '${profilePublicKey}'`); + return; + } profile.relays = newRelays; return updateProfile(profile); } @@ -130,7 +134,19 @@ export async function getProfile(publicKey: string): Promise { export async function updateProfiles( profiles: ProfilesConfig ): Promise { - browser.storage.local.set({ + await browser.storage.local.set({ + [ConfigurationKeys.PROFILES]: profiles + }); + + return profiles; +} +export async function addProfile( + profile: ProfileConfig +): Promise { + const profiles = await readProfiles(); + profiles[getPublicKey(profile.privateKey)] = profile; + + await browser.storage.local.set({ [ConfigurationKeys.PROFILES]: profiles }); @@ -142,7 +158,7 @@ export async function updateProfile( const profiles = await readProfiles(); profiles[getPublicKey(profile.privateKey)] = profile; - browser.storage.local.set({ + await browser.storage.local.set({ [ConfigurationKeys.PROFILES]: profiles }); diff --git a/src/style.scss b/src/style.scss index 61413fb..0fb7386 100644 --- a/src/style.scss +++ b/src/style.scss @@ -467,11 +467,21 @@ input, .modal { max-width: 95%; + } + .export-modal { code { display: block; overflow: scroll; } } + .import-modal { + textarea { + display: block; + width: 100%; + overflow: scroll; + font-family: 'Courier New', Courier, monospace; + } + } } /* ---- Prompt ---- */