From d3a1485f1eb08227e0d85f8912246d1b386b9f9f Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 28 Jan 2025 18:30:34 +0100 Subject: [PATCH] add hidden accounts --- packages/ui/src/App.tsx | 27 ++-- .../ui/src/contexts/HiddenAccountsContext.tsx | 127 +++++++++++++++++ .../ui/src/contexts/MultiProxyContext.tsx | 17 ++- .../ui/src/pages/Settings/HiddenAccounts.tsx | 133 ++++++++++++++++++ packages/ui/src/pages/Settings/Settings.tsx | 35 ++++- 5 files changed, 318 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/contexts/HiddenAccountsContext.tsx create mode 100644 packages/ui/src/pages/Settings/HiddenAccounts.tsx diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index a031d73d..394b950c 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -18,6 +18,7 @@ import { ReactiveDotProvider } from '@reactive-dot/react' import { config } from './walletConfigs' import { Suspense } from 'react' import { AssetsContextProvider } from './contexts/AssetsContext' +import { HiddenAccountsContextProvider } from './contexts/HiddenAccountsContext' const App = () => { const queryClient = new QueryClient() @@ -26,7 +27,7 @@ const App = () => { - Loading...}> + Loading...}> @@ -34,17 +35,19 @@ const App = () => { - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/packages/ui/src/contexts/HiddenAccountsContext.tsx b/packages/ui/src/contexts/HiddenAccountsContext.tsx new file mode 100644 index 00000000..6d25f8fe --- /dev/null +++ b/packages/ui/src/contexts/HiddenAccountsContext.tsx @@ -0,0 +1,127 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState +} from 'react' +import { useApi } from './ApiContext' +import { getPubKeyFromAddress } from '../utils/getPubKeyFromAddress' +import { useNetwork } from './NetworkContext' +import { HexString } from 'polkadot-api' +import { useGetEncodedAddress } from '../hooks/useGetEncodedAddress' + +const LOCALSTORAGE_HIDDEN_ACCOUNTS_KEY = 'multix.hiddenAccounts' + +type HiddenAccountsProps = { + children: ReactNode | ReactNode[] +} + +export interface IHiddenAccountsContext { + addHiddenAccount: (address: string) => void + removeHiddenAccount: (address: string) => void + hiddenAccounts: HiddenAccount[] + networkHiddenAccounts: string[] + isInitialized: boolean +} + +export interface HiddenAccount { + pubKey: HexString + network: string +} + +const HiddenAccountsContext = createContext(undefined) + +const HiddenAccountsContextProvider = ({ children }: HiddenAccountsProps) => { + const [hiddenAccounts, setHiddenAccounts] = useState([]) + const [isInitialized, setIsInitialized] = useState(false) + const { chainInfo } = useApi() + const { selectedNetwork } = useNetwork() + const getEncodedAddress = useGetEncodedAddress() + const networkHiddenAccounts = useMemo(() => { + if (!selectedNetwork) return [] + + return hiddenAccounts + .map(({ pubKey, network }) => { + if (network !== selectedNetwork) return null + + return getEncodedAddress(pubKey) + }) + .filter(Boolean) as string[] + }, [getEncodedAddress, hiddenAccounts, selectedNetwork]) + + const addHiddenAccount = useCallback( + (address: string) => { + const pubKey = getPubKeyFromAddress(address) + selectedNetwork && + pubKey && + setHiddenAccounts((prev) => [ + ...prev, + { pubKey, network: selectedNetwork } as HiddenAccount + ]) + }, + [selectedNetwork] + ) + + const removeHiddenAccount = useCallback( + (addressToRemove: string) => { + const pubKeyToRemove = getPubKeyFromAddress(addressToRemove) + const filtered = hiddenAccounts.filter( + ({ pubKey, network }) => pubKey !== pubKeyToRemove && network === selectedNetwork + ) + setHiddenAccounts([...filtered]) + }, + [hiddenAccounts, selectedNetwork] + ) + + const loadHiddenAccounts = useCallback(() => { + if (!chainInfo) { + return + } + + const localStorageHiddenAccount = localStorage.getItem(LOCALSTORAGE_HIDDEN_ACCOUNTS_KEY) + const hiddenArray: HiddenAccount[] = localStorageHiddenAccount + ? JSON.parse(localStorageHiddenAccount) + : [] + + setHiddenAccounts(hiddenArray) + setIsInitialized(true) + }, [chainInfo]) + + useEffect(() => { + !isInitialized && loadHiddenAccounts() + }, [isInitialized, loadHiddenAccounts]) + + // persist the accounts hidden every time there's a change + useEffect(() => { + if (!isInitialized) return + + localStorage.setItem(LOCALSTORAGE_HIDDEN_ACCOUNTS_KEY, JSON.stringify(hiddenAccounts)) + }, [isInitialized, hiddenAccounts]) + + return ( + + {children} + + ) +} + +const useHiddenAccounts = () => { + const context = useContext(HiddenAccountsContext) + if (context === undefined) { + throw new Error('useHiddenAccounts must be used within a HiddenAccountsContextProvider') + } + return context +} + +export { HiddenAccountsContextProvider, useHiddenAccounts } diff --git a/packages/ui/src/contexts/MultiProxyContext.tsx b/packages/ui/src/contexts/MultiProxyContext.tsx index c6bee0c4..ab9e669b 100644 --- a/packages/ui/src/contexts/MultiProxyContext.tsx +++ b/packages/ui/src/contexts/MultiProxyContext.tsx @@ -8,6 +8,7 @@ import { useAccountId } from '../hooks/useAccountId' import { getMultiProxyAddress } from '../utils/getMultiProxyAddress' import { useSearchParams } from 'react-router' import { useNetwork } from './NetworkContext' +import { useHiddenAccounts } from './HiddenAccountsContext' interface MultisigContextProps { children: React.ReactNode | React.ReactNode[] @@ -57,19 +58,22 @@ const getSignatoriesFromAccount = ( } const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { + const { networkHiddenAccounts } = useHiddenAccounts() const [refetchMultisigTimeoutMinutes, setRefetchMultisigTimeoutMinutes] = useState(0) const [shouldPollMultisigs, setShouldPollMultisigs] = useState(false) const [canFindMultiProxyFromUrl, setCanFindMultiProxyFromUrl] = useState(false) const [selectedMultiProxyAddress, setSelectedMultiProxyAddress] = useState('') // if set to null, it means that it hasn't been initialized yet - const [pureProxyList, setPureProxyList] = useState( - null - ) - // if set to null, it means that it hasn't been initialized yet const [multisigList, setMultisigList] = useState(null) const multiProxyList = useMemo(() => { - return [...(pureProxyList || []), ...(multisigList || [])] - }, [multisigList, pureProxyList]) + const filteredMulti = multisigList?.filter(({ proxy, multisigs }) => { + if (proxy) return !networkHiddenAccounts.includes(proxy) + + const firstMultisig = multisigs[0].address + return !!firstMultisig && !networkHiddenAccounts.includes(firstMultisig) + }) + return filteredMulti || [] + }, [multisigList, networkHiddenAccounts]) const { ownAddressList } = useAccounts() const { watchedAddresses } = useWatchedAddresses() const { selectedNetwork } = useNetwork() @@ -127,7 +131,6 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { const resetLists = useCallback(() => { setMultisigList(null) - setPureProxyList(null) }, []) const setAddressInUrl = useCallback( diff --git a/packages/ui/src/pages/Settings/HiddenAccounts.tsx b/packages/ui/src/pages/Settings/HiddenAccounts.tsx new file mode 100644 index 00000000..c8057c0d --- /dev/null +++ b/packages/ui/src/pages/Settings/HiddenAccounts.tsx @@ -0,0 +1,133 @@ +import { styled } from '@mui/material/styles' +import { Box, Grid2 as Grid, IconButton, Paper } from '@mui/material' +import AccountDisplay from '../../components/AccountDisplay/AccountDisplay' +import { HiOutlineXMark } from 'react-icons/hi2' +import AccountSelection from '../../components/select/AccountSelection' +import { useMemo } from 'react' +import { useHiddenAccounts } from '../../contexts/HiddenAccountsContext' +import { useNetwork } from '../../contexts/NetworkContext' + +const HiddenAccounts = () => { + const { addHiddenAccount, networkHiddenAccounts, removeHiddenAccount } = useHiddenAccounts() + const { selectedNetwork } = useNetwork() + + const hasHiddenAddresses = useMemo( + () => networkHiddenAccounts.length > 0, + [networkHiddenAccounts] + ) + + return ( + <> + {hasHiddenAddresses && ( + + Hidden accounts for {selectedNetwork}: + + )} + + {hasHiddenAddresses && ( + + + {networkHiddenAccounts.map((address) => { + return ( + + + removeHiddenAccount(address)} + data-cy="button-delete-hidden-account" + > + + + + ) + })} + + + )} + + + + + + + + ) +} + +const HiddenAccountsHeaderStyled = styled('h3')` + color: ${({ theme }) => theme.custom.gray[800]}; + font-size: 1rem; + font-weight: 400; + margin: 0 0 0.5rem 0; +` + +const PaperStyled = styled(Paper)` + border-radius: ${({ theme }) => theme.custom.borderRadius}; + border: 1px solid ${({ theme }) => theme.custom.text.borderColor}; + box-shadow: none; + + & > :last-of-type { + border-bottom: none; + } +` + +const IconButtonDeleteStyled = styled(IconButton)` + margin-left: 1rem; + height: 2.5rem; + align-self: center; +` + +const AccountDisplayStyled = styled(AccountDisplay)` + flex: 1; +` + +const AccountItemWrapperStyled = styled(Box)` + display: flex; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid ${({ theme }) => theme.custom.text.borderColor}; +` + +const AccountSelectionWrapperStyled = styled(Box)` + display: flex; + margin-bottom: 2rem; + + .MuiAutocomplete-root { + margin-right: 0 !important; + } + + .accountDropdown { + display: flex; + flex-direction: column; + + & > * { + width: 100%; + } + + & > :last-child { + margin-top: 0.5rem; + } + } +` + +export default HiddenAccounts diff --git a/packages/ui/src/pages/Settings/Settings.tsx b/packages/ui/src/pages/Settings/Settings.tsx index 7ea00cfc..4b146c2f 100644 --- a/packages/ui/src/pages/Settings/Settings.tsx +++ b/packages/ui/src/pages/Settings/Settings.tsx @@ -3,17 +3,27 @@ import { css, styled } from '@mui/material/styles' import WatchedAccounts from './WatchedAccounts' import { WalletConnectSession } from '../../components/WalletConnect/WalletConnectSession' import { WalletConnectActiveSessions } from '../../components/WalletConnect/WalletConnectActiveSessions' -import { HiOutlineChevronDown as ExpandMoreIcon, HiOutlineEye } from 'react-icons/hi2' +import { + HiOutlineChevronDown as ExpandMoreIcon, + HiOutlineEye, + HiOutlineEyeSlash +} from 'react-icons/hi2' import { theme } from '../../styles/theme' import { useCallback, useEffect, useState } from 'react' import { useLocation } from 'react-router' import WalletConnectSVG from '../../logos/walletConnectSVG.svg?react' +import HiddenAccounts from './HiddenAccounts' const ACCORDION_WATCHED_ACCOUNTS = 'panel-watched-accounts' const ACCORDION_WALLET_CONNECT = 'panel-wallet-connect' +const ACCORDION_HIDDEN_ACCOUNTS = 'panel-hidden-accounts' export const WATCH_ACCOUNT_ANCHOR = 'watched-accounts' +export const HIDDEN_ACCOUNTS_ANCHOR = 'hidden-accounts' -type AccordionNames = typeof ACCORDION_WATCHED_ACCOUNTS | typeof ACCORDION_WALLET_CONNECT +type AccordionNames = + | typeof ACCORDION_WATCHED_ACCOUNTS + | typeof ACCORDION_WALLET_CONNECT + | typeof ACCORDION_HIDDEN_ACCOUNTS const Settings = () => { const { hash } = useLocation() @@ -29,6 +39,10 @@ const Settings = () => { if (hash === `#${WATCH_ACCOUNT_ANCHOR}`) { onToggle(ACCORDION_WATCHED_ACCOUNTS, true) } + + if (hash === `#${HIDDEN_ACCOUNTS_ANCHOR}`) { + onToggle(ACCORDION_HIDDEN_ACCOUNTS, true) + } }, [hash, onToggle]) return ( @@ -49,6 +63,21 @@ const Settings = () => { + onToggle(ACCORDION_HIDDEN_ACCOUNTS)} + > + } + data-cy="accordion-title-hidden-accounts" + > + + Hidden accounts + + + + + onToggle(ACCORDION_WALLET_CONNECT)} @@ -122,6 +151,8 @@ const WalletConnectSVGStyled = styled(WalletConnectSVG)(commonCssImgs) const HiOutlineEyeStyled = styled(HiOutlineEye)(commonCssImgs) +const HiOutlineEyeSlashStyled = styled(HiOutlineEyeSlash)(commonCssImgs) + const SummaryLabelStyled = styled('div')` color: ${({ theme }) => theme.custom.gray[900]}; font-size: 1.25rem;