Skip to content

Commit

Permalink
add hidden accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaut committed Jan 28, 2025
1 parent 3ba1dca commit d3a1485
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 21 deletions.
27 changes: 15 additions & 12 deletions packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -26,25 +27,27 @@ const App = () => {
<MuiThemeProvider theme={theme}>
<CssBaseline />
<ReactiveDotProvider config={config}>
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<div> Loading...</div>}>
<ToastContextProvider>
<NetworkContextProvider>
<QueryClientProvider client={queryClient}>
<ApiContextProvider>
<PplApiContextProvider>
<AssetsContextProvider>
<WatchedAddressesContextProvider>
<AccountContextProvider>
<AccountNamesContextProvider>
<MultiProxyContextProvider>
<WalletConnectContextProvider>
<ModalsContextProvider>
<MainLayout />
</ModalsContextProvider>
</WalletConnectContextProvider>
</MultiProxyContextProvider>
</AccountNamesContextProvider>
</AccountContextProvider>
<HiddenAccountsContextProvider>
<AccountContextProvider>
<AccountNamesContextProvider>
<MultiProxyContextProvider>
<WalletConnectContextProvider>
<ModalsContextProvider>
<MainLayout />
</ModalsContextProvider>
</WalletConnectContextProvider>
</MultiProxyContextProvider>
</AccountNamesContextProvider>
</AccountContextProvider>
</HiddenAccountsContextProvider>
</WatchedAddressesContextProvider>
</AssetsContextProvider>
</PplApiContextProvider>
Expand Down
127 changes: 127 additions & 0 deletions packages/ui/src/contexts/HiddenAccountsContext.tsx
Original file line number Diff line number Diff line change
@@ -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<IHiddenAccountsContext | undefined>(undefined)

const HiddenAccountsContextProvider = ({ children }: HiddenAccountsProps) => {
const [hiddenAccounts, setHiddenAccounts] = useState<HiddenAccount[]>([])
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 (
<HiddenAccountsContext.Provider
value={{
addHiddenAccount,
removeHiddenAccount,
hiddenAccounts,
isInitialized,
networkHiddenAccounts
}}
>
{children}
</HiddenAccountsContext.Provider>
)
}

const useHiddenAccounts = () => {
const context = useContext(HiddenAccountsContext)
if (context === undefined) {
throw new Error('useHiddenAccounts must be used within a HiddenAccountsContextProvider')
}
return context
}

export { HiddenAccountsContextProvider, useHiddenAccounts }
17 changes: 10 additions & 7 deletions packages/ui/src/contexts/MultiProxyContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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<IMultisigContext['multiProxyList'] | null>(
null
)
// if set to null, it means that it hasn't been initialized yet
const [multisigList, setMultisigList] = useState<IMultisigContext['multiProxyList'] | null>(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()
Expand Down Expand Up @@ -127,7 +131,6 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => {

const resetLists = useCallback(() => {
setMultisigList(null)
setPureProxyList(null)
}, [])

const setAddressInUrl = useCallback(
Expand Down
133 changes: 133 additions & 0 deletions packages/ui/src/pages/Settings/HiddenAccounts.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<HiddenAccountsHeaderStyled>
Hidden accounts for {selectedNetwork}:
</HiddenAccountsHeaderStyled>
)}
<Grid
container
spacing={2}
>
{hasHiddenAddresses && (
<Grid size={{ xs: 12, md: 8 }}>
<PaperStyled>
{networkHiddenAccounts.map((address) => {
return (
<AccountItemWrapperStyled
key={address}
data-cy="container-hidden-account-details"
>
<AccountDisplayStyled
address={address}
canEdit
canCopy
/>
<IconButtonDeleteStyled
aria-label="delete"
onClick={() => removeHiddenAccount(address)}
data-cy="button-delete-hidden-account"
>
<HiOutlineXMark />
</IconButtonDeleteStyled>
</AccountItemWrapperStyled>
)
})}
</PaperStyled>
</Grid>
)}
<Grid size={{ xs: 12, md: 8 }}>
<AccountSelectionWrapperStyled>
<AccountSelection
className="accountDropdown"
currentSelection={networkHiddenAccounts}
addAccount={addHiddenAccount}
actionButtonLabel="Hide"
actionButtonVariant="primary"
withAddButton
withPreselection={false}
//make sure the first state is empty
value=""
/>
</AccountSelectionWrapperStyled>
</Grid>
</Grid>
</>
)
}

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
Loading

0 comments on commit d3a1485

Please sign in to comment.