From 0028b5ea6c5771d769d39b97b5ffc7d5854dac25 Mon Sep 17 00:00:00 2001 From: DMY <147dmy@gmail.com> Date: Fri, 8 Sep 2023 18:18:37 +0800 Subject: [PATCH] wip --- __tests__/migration/contactMigration.test.ts | 12 + src/background/controller/wallet.ts | 3 + src/background/service/preference.ts | 31 ++ src/ui/assets/address/checked.svg | 5 + src/ui/assets/address/sort-by-alphabet.svg | 11 + src/ui/assets/address/sort-by-type.svg | 23 ++ src/ui/assets/address/sort-by-usd.svg | 17 + src/ui/component/AddressList/style.less | 34 ++ src/ui/models/preference.ts | 21 +- src/ui/views/AddressManagement/SortPopup.tsx | 81 +++++ src/ui/views/AddressManagement/index.tsx | 338 ++++++++++++++----- src/ui/views/AddressManagement/style.less | 12 +- src/ui/views/ManageAddress/hooks.tsx | 4 +- 13 files changed, 506 insertions(+), 86 deletions(-) create mode 100644 src/ui/assets/address/checked.svg create mode 100644 src/ui/assets/address/sort-by-alphabet.svg create mode 100644 src/ui/assets/address/sort-by-type.svg create mode 100644 src/ui/assets/address/sort-by-usd.svg create mode 100644 src/ui/views/AddressManagement/SortPopup.tsx diff --git a/__tests__/migration/contactMigration.test.ts b/__tests__/migration/contactMigration.test.ts index 8d96668dc1b..a4b71a9fccf 100644 --- a/__tests__/migration/contactMigration.test.ts +++ b/__tests__/migration/contactMigration.test.ts @@ -55,6 +55,10 @@ const data: { preference: PreferenceStore; contactBook } = { walletSavedList: [], watchAddressPreference: {}, testnetBalanceMap: {}, + addressSortStore: { + search: '', + sortType: 'usd', + }, }, contactBook: { '0x10b26700b0a2d3f5ef12fa250aba818ee3b43bf4': { @@ -188,6 +192,10 @@ test('should migrate when no alians', () => { walletSavedList: [], watchAddressPreference: {}, testnetBalanceMap: {}, + addressSortStore: { + search: '', + sortType: 'usd', + }, }, contactBook: { '0x10b26700b0a2d3f5ef12fa250aba818ee3b43bf4': { @@ -316,6 +324,10 @@ test('should migrate when no contacts', () => { walletSavedList: [], watchAddressPreference: {}, testnetBalanceMap: {}, + addressSortStore: { + search: '', + sortType: 'usd', + }, }, contactBook: {}, }; diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 6952bfaf557..3de9bbe0884 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -1110,6 +1110,9 @@ export class WalletController extends BaseController { getLastSelectedGasTopUpChain = preferenceService.getLastSelectedGasTopUpChain; setLastSelectedGasTopUpChain = preferenceService.setLastSelectedGasTopUpChain; + getAddressSortStoreValue = preferenceService.getAddressSortStoreValue; + setAddressSortStoreValue = preferenceService.setAddressSortStoreValue; + getLastSelectedSwapChain = swapService.getSelectedChain; setLastSelectedSwapChain = swapService.setSelectedChain; getSwap = swapService.getSwap; diff --git a/src/background/service/preference.ts b/src/background/service/preference.ts index 4f5112a0a46..801b94db766 100644 --- a/src/background/service/preference.ts +++ b/src/background/service/preference.ts @@ -91,8 +91,20 @@ export interface PreferenceStore { autoLockTime?: number; hiddenBalance?: boolean; isShowTestnet?: boolean; + addressSortStore: AddressSortStore; } +export interface AddressSortStore { + search: string; + sortType: 'usd' | 'addressType' | 'alphabet'; + lastCurrent?: string; +} + +const defaultAddressSortStore: AddressSortStore = { + search: '', + sortType: 'usd', +}; + class PreferenceService { store!: PreferenceStore; popupOpen = false; @@ -133,8 +145,14 @@ class PreferenceService { collectionStarred: [], hiddenBalance: false, isShowTestnet: false, + addressSortStore: { + ...defaultAddressSortStore, + }, }, }); + //lifetime in background + this.store.addressSortStore = { ...defaultAddressSortStore }; + if ( !this.store.locale || !LANGS.find((item) => item.code === this.store.locale) @@ -691,6 +709,19 @@ class PreferenceService { resetCurrentCoboSafeAddress = async () => { this.setCurrentAccount(this.currentCoboSafeAddress ?? null); }; + + getAddressSortStoreValue = (key: keyof AddressSortStore) => + this.store.addressSortStore[key]; + + setAddressSortStoreValue = ( + key: K, + value: AddressSortStore[K] + ) => { + this.store.addressSortStore = { + ...this.store.addressSortStore, + [key]: value, + }; + }; } export default new PreferenceService(); diff --git a/src/ui/assets/address/checked.svg b/src/ui/assets/address/checked.svg new file mode 100644 index 00000000000..dcaa34558f3 --- /dev/null +++ b/src/ui/assets/address/checked.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/ui/assets/address/sort-by-alphabet.svg b/src/ui/assets/address/sort-by-alphabet.svg new file mode 100644 index 00000000000..471abc1543c --- /dev/null +++ b/src/ui/assets/address/sort-by-alphabet.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/ui/assets/address/sort-by-type.svg b/src/ui/assets/address/sort-by-type.svg new file mode 100644 index 00000000000..d6b75e2168a --- /dev/null +++ b/src/ui/assets/address/sort-by-type.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/assets/address/sort-by-usd.svg b/src/ui/assets/address/sort-by-usd.svg new file mode 100644 index 00000000000..65d14e3c07b --- /dev/null +++ b/src/ui/assets/address/sort-by-usd.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/component/AddressList/style.less b/src/ui/component/AddressList/style.less index 736399f7177..db0b0846ae7 100644 --- a/src/ui/component/AddressList/style.less +++ b/src/ui/component/AddressList/style.less @@ -124,6 +124,40 @@ } .address-wrap-with-padding { .address-wrap; + margin-bottom: 0; + + &.group:nth-child(n + 2) { + .rabby-address-item-container .rabby-address-item { + position: relative; + &::after { + position: absolute; + top: 0; + left: 0; + content: ''; + width: 100%; + height: 0; + border-top: 0.5px solid var(--r-neutral-line, #d3d8e0); + } + } + } + + &.group:nth-child(1) { + .rabby-address-item-container .rabby-address-item { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } + } + &.group:nth-last-child(1) { + .rabby-address-item-container .rabby-address-item { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + &.group .rabby-address-item-container .rabby-address-item { + border-radius: 0; + } + &:nth-last-child(1) { padding-bottom: 200px; } diff --git a/src/ui/models/preference.ts b/src/ui/models/preference.ts index 025a76737d7..92d43e691f3 100644 --- a/src/ui/models/preference.ts +++ b/src/ui/models/preference.ts @@ -1,7 +1,11 @@ import { createModel } from '@rematch/core'; import { RootModel } from '.'; import { TokenItem } from 'background/service/openapi'; -import { GasCache, addedToken } from 'background/service/preference'; +import { + AddressSortStore, + GasCache, + addedToken, +} from 'background/service/preference'; import { CHAINS_ENUM } from 'consts'; import i18n from '@/i18n'; @@ -22,6 +26,7 @@ interface PreferenceState { autoLockTime: number; hiddenBalance: boolean; isShowTestnet: boolean; + addressSortStore: AddressSortStore; } export const preference = createModel()({ @@ -44,6 +49,7 @@ export const preference = createModel()({ autoLockTime: 0, hiddenBalance: false, isShowTestnet: false, + addressSortStore: {} as AddressSortStore, } as PreferenceState, reducers: { @@ -179,6 +185,19 @@ export const preference = createModel()({ dispatch.preference.getPreference('locale'); }, + async getAddressSortStoreValue(key: keyof AddressSortStore, store?) { + const value = await store.app.wallet.getAddressSortStoreValue(key); + return value; + }, + + async setAddressSortStoreValue( + { key, value }: { key: K; value: AddressSortStore[K] }, + store? + ) { + await store.app.wallet.setAddressSortStoreValue(key, value); + dispatch.preference.getPreference('addressSortStore'); + }, + // async setOpenapiHost(value: string, store?) { // dispatch.preference.setField({ // isShowTestnet: value, diff --git a/src/ui/views/AddressManagement/SortPopup.tsx b/src/ui/views/AddressManagement/SortPopup.tsx new file mode 100644 index 00000000000..dafa9972c42 --- /dev/null +++ b/src/ui/views/AddressManagement/SortPopup.tsx @@ -0,0 +1,81 @@ +import { Item, Popup } from '@/ui/component'; +import React, { useMemo } from 'react'; +import ImgSortByUsd from '@/ui/assets/address/sort-by-usd.svg'; +import ImgSortByType from '@/ui/assets/address/sort-by-type.svg'; +import ImgSortByAlphabet from '@/ui/assets/address/sort-by-alphabet.svg'; +import ImgChecked from '@/ui/assets/address/checked.svg'; + +import { useRabbyDispatch, useRabbySelector } from '@/ui/store'; +import { AddressSortStore } from '@/background/service/preference'; + +export const AddressSortIconMapping: Record< + AddressSortStore['sortType'], + string +> = { + usd: ImgSortByUsd, + addressType: ImgSortByType, + alphabet: ImgSortByAlphabet, +}; + +export const AddressSortPopup = ({ + open, + onCancel, +}: { + open: boolean; + onCancel: () => void; +}) => { + const sortType = useRabbySelector( + (s) => s.preference.addressSortStore.sortType + ); + const dispath = useRabbyDispatch(); + const handleChange = (value: AddressSortStore['sortType']) => () => { + dispath.preference.setAddressSortStoreValue({ key: 'sortType', value }); + onCancel?.(); + }; + + const arr = useMemo( + () => + [ + { + key: 'usd', + label: '按地址金额排序', + }, + { + key: 'addressType', + label: '按地址类型排序', + }, + { + key: 'alphabet', + label: '按地址备注首字母排序', + }, + ] as const, + [] + ); + return ( + +
+ {arr.map((e) => ( + + {e.label} + + ))} +
+
+ ); +}; diff --git a/src/ui/views/AddressManagement/index.tsx b/src/ui/views/AddressManagement/index.tsx index 589d817ce7d..b7dc115978b 100644 --- a/src/ui/views/AddressManagement/index.tsx +++ b/src/ui/views/AddressManagement/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { VariableSizeList as VList } from 'react-window'; @@ -27,6 +27,10 @@ import { LedgerStatusBar } from '@/ui/component/ConnectStatus/LedgerStatusBar'; import { GridPlusStatusBar } from '@/ui/component/ConnectStatus/GridPlusStatusBar'; import useDebounceValue from '@/ui/hooks/useDebounceValue'; import LessPalette from '@/ui/style/var-defs'; +import { AddressSortIconMapping, AddressSortPopup } from './SortPopup'; +import { useSwitch } from '@/ui/utils/switch'; +import { getWalletScore } from '../ManageAddress/hooks'; +import { IDisplayedAccountWithBalance } from '@/ui/models/accountToDisplay'; function NoAddressUI() { const { t } = useTranslation(); @@ -71,6 +75,10 @@ const AddressManagement = () => { const location = useLocation(); const enableSwitch = location.pathname === '/switch-address'; + const addressSortStore = useRabbySelector( + (s) => s.preference.addressSortStore + ); + // todo: store redesign const { accountsList, @@ -110,13 +118,43 @@ const AddressManagement = () => { watchModeHighlightedAccounts ); + const normalAccounts = highlightedAccounts + .concat(data['0'] || []) + .filter((e) => !!e); + const watchModeAccounts = watchModeHighlightedAccounts + .concat(data['1'] || []) + .filter((e) => !!e); + if (addressSortStore.sortType === 'usd') { + return [normalAccounts, watchModeAccounts]; + } + if (addressSortStore.sortType === 'alphabet') { + return [ + normalAccounts.sort((a, b) => a.alianName.localeCompare(b.alianName)), + watchModeAccounts.sort((a, b) => + a.alianName.localeCompare(b.alianName) + ), + ]; + } + + const normalArr = groupBy( + sortAccountsByBalance(normalAccounts), + (e) => e.brandName + ); + return [ - highlightedAccounts.concat(data['0'] || []).filter((e) => !!e), - watchModeHighlightedAccounts.concat(data['1'] || []).filter((e) => !!e), + [ + ...Object.values(normalArr).sort( + (a, b) => getWalletScore(a) - getWalletScore(b) + ), + sortAccountsByBalance(watchModeAccounts), + ], + [], ]; - }, [accountsList, highlightedAddresses]); + }, [accountsList, highlightedAddresses, addressSortStore.sortType]); - const [searchKeyword, setSearchKeyword] = React.useState(''); + const [searchKeyword, setSearchKeyword] = React.useState( + addressSortStore?.search || '' + ); const debouncedSearchKeyword = useDebounceValue(searchKeyword, 250); const { @@ -127,34 +165,64 @@ const AddressManagement = () => { } = useMemo(() => { const result = { accountList: [ - ...(sortedAccountsList || []), + ...(sortedAccountsList?.flat() || []), ...(watchSortedAccountsList || []), ], filteredAccounts: [] as typeof sortedAccountsList, noAnyAccount: false, noAnySearchedAccount: false, }; + result.filteredAccounts = [...result.accountList]; + if (addressSortStore.sortType === 'addressType') { + result.filteredAccounts = sortedAccountsList; + } if (debouncedSearchKeyword) { const lKeyword = debouncedSearchKeyword.toLowerCase(); - result.filteredAccounts = result.accountList.filter((account) => { - const lowerAddress = account.address.toLowerCase(); - const aliasName = account.alianName?.toLowerCase(); - let addrIncludeKw = false; - if (lKeyword.replace(/^0x/, '').length >= 2) { - addrIncludeKw = account.address - .toLowerCase() - .includes(lKeyword.toLowerCase()); - } + if (addressSortStore.sortType === 'addressType') { + result.filteredAccounts = ([ + ...(sortedAccountsList || []), + ...(watchSortedAccountsList || []), + ] as IDisplayedAccountWithBalance[][]) + .map((group) => + group.filter((account) => { + const lowerAddress = account.address.toLowerCase(); + const aliasName = account.alianName?.toLowerCase(); + let addrIncludeKw = false; + if (lKeyword.replace(/^0x/, '').length >= 2) { + addrIncludeKw = account.address + .toLowerCase() + .includes(lKeyword.toLowerCase()); + } + + return ( + lowerAddress === lKeyword || + aliasName?.includes(lKeyword) || + addrIncludeKw + ); + }) + ) + .filter((group) => group.length > 0); + } else { + result.filteredAccounts = result.accountList.filter((account) => { + const lowerAddress = account.address.toLowerCase(); + const aliasName = account.alianName?.toLowerCase(); + let addrIncludeKw = false; + if (lKeyword.replace(/^0x/, '').length >= 2) { + addrIncludeKw = account.address + .toLowerCase() + .includes(lKeyword.toLowerCase()); + } - return ( - lowerAddress === lKeyword || - aliasName?.includes(lKeyword) || - addrIncludeKw - ); - }); + return ( + lowerAddress === lKeyword || + aliasName?.includes(lKeyword) || + addrIncludeKw + ); + }); + } } result.noAnyAccount = result.accountList.length <= 0 && !loadingAccounts; @@ -162,7 +230,12 @@ const AddressManagement = () => { result.filteredAccounts.length <= 0 && !loadingAccounts; return result; - }, [sortedAccountsList, watchSortedAccountsList, debouncedSearchKeyword]); + }, [ + sortedAccountsList, + watchSortedAccountsList, + debouncedSearchKeyword, + addressSortStore.sortType, + ]); const dispatch = useRabbyDispatch(); @@ -203,7 +276,7 @@ const AddressManagement = () => { history.push('/settings/address?back=true'); }; - const switchAccount = async (account: typeof sortedAccountsList[number]) => { + const switchAccount = async (account: typeof accountsList[number]) => { await dispatch.account.changeAccountAsync(account); history.push('/dashboard'); }; @@ -212,62 +285,95 @@ const AddressManagement = () => { dispatch.whitelist.init(); }, []); - const Row = (props) => { + const recordLatestAddress = (value: string) => { + dispatch.preference.setAddressSortStoreValue({ + key: 'lastCurrent', + value, + }); + }; + + const Row = ( + props: any //ListChildComponentProps + ) => { const { data, index, style } = props; const account = data[index]; - const favorited = highlightedAddresses.some( - (highlighted) => - account.address === highlighted.address && - account.brandName === highlighted.brandName - ); - return ( -
- { - e.stopPropagation(); - dispatch.addressManagement.toggleHighlightedAddressAsync({ + const render = (account: typeof accountsList[number], isGroup = false) => { + const favorited = highlightedAddresses.some( + (highlighted) => + account.address === highlighted.address && + account.brandName === highlighted.brandName + ); + + return ( +
{ + recordLatestAddress(`${account.type}-${account.address}`); + }} + > + { + e.stopPropagation(); + dispatch.addressManagement.toggleHighlightedAddressAsync({ + address: account.address, + brandName: account.brandName, + }); + }} + > + +
+ } + onClick={() => { + history.push( + `/settings/address-detail?${obj2query({ address: account.address, + type: account.type, brandName: account.brandName, - }); - }} - > - -
- } - onClick={() => { - history.push( - `/settings/address-detail?${obj2query({ - address: account.address, - type: account.type, - brandName: account.brandName, - byImport: account.byImport || '', - })}` - ); - }} - onSwitchCurrentAccount={() => { - switchAccount(account); - }} - enableSwitch={enableSwitch} - /> - - ); + //@ts-expect-error byImport is boolean + byImport: account.byImport || '', + })}` + ); + }} + onSwitchCurrentAccount={() => { + switchAccount(account); + }} + enableSwitch={enableSwitch} + /> + + ); + }; + + if (addressSortStore.sortType === 'addressType') { + return ( +
+ {(account as typeof accountsList)?.map((e) => render(e, true))} +
+ ); + } + + return render(account as typeof accountsList[number]); }; const isWalletConnect = @@ -278,6 +384,55 @@ const AddressManagement = () => { accountList[currentAccountIndex]?.type === KEYRING_CLASS.HARDWARE.GRIDPLUS; const hasStatusBar = isWalletConnect || isLedger || isGridPlus; + const { on, turnOff, turnOn } = useSwitch(false); + + useEffect(() => { + dispatch.preference.setAddressSortStoreValue({ + key: 'search', + value: searchKeyword, + }); + }, [searchKeyword]); + + const listRef = useRef< + VList + >(null); + + useEffect(() => { + if (addressSortStore.lastCurrent && filteredAccounts?.length) { + let index = -1; + let secondIndex = -1; + let sum = 0; + if (addressSortStore.sortType === 'addressType') { + const accounts = filteredAccounts as IDisplayedAccountWithBalance[][]; + index = accounts.findIndex((arr) => { + const i = arr.findIndex( + (e) => `${e.type}-${e.address}` === addressSortStore.lastCurrent + ); + if (i !== -1) { + secondIndex = i; + return true; + } + return false; + }); + if (index !== -1) { + for (let i = 0; i < index; i++) { + sum += accounts[i].length * 56 + 16; + } + sum += secondIndex * 56; + } + } else { + index = (filteredAccounts as IDisplayedAccountWithBalance[]).findIndex( + (e) => `${e.type}-${e.address}` === addressSortStore.lastCurrent + ); + if (index !== -1) { + sum = index * 64; + } + } + + listRef.current?.scrollTo(sum); + } + }, []); + return (
@@ -363,13 +518,31 @@ const AddressManagement = () => {
} + prefix={ +
+
+ +
+ +
+ } onChange={(e) => setSearchKeyword(e.target.value)} value={searchKeyword} allowClear /> + +
{ ) : (
(i !== sortedAccountsList.length - 1 ? 64 : 78)} + itemSize={(i) => { + if (addressSortStore.sortType === 'addressType') { + return ( + 56 * (filteredAccounts as typeof accountsList[])[i].length + + 16 + ); + } + + return i !== sortedAccountsList.length - 1 ? 64 : 78; + }} className="scroll-container" > {Row} diff --git a/src/ui/views/AddressManagement/style.less b/src/ui/views/AddressManagement/style.less index e710e8b47b4..e1728396e9f 100644 --- a/src/ui/views/AddressManagement/style.less +++ b/src/ui/views/AddressManagement/style.less @@ -219,23 +219,21 @@ } .search-address-wrapper .ant-input-affix-wrapper { + width: 160px; border-radius: 6px; + border: 0.5px solid var(--r-neutral-line, #d3d8e0); } .search-address-wrapper .ant-input-affix-wrapper:hover, .search-address-wrapper .ant-input-affix-wrapper:focus { - background-color: rgba(134, 151, 255, 0.10); - - .ant-input { - background-color: transparent; - } + border: 0.5px solid var(--r-blue-default, #7084ff); } - + .no-matched-address { display: flex; flex-direction: column; align-items: center; - + padding-top: 84px; } } diff --git a/src/ui/views/ManageAddress/hooks.tsx b/src/ui/views/ManageAddress/hooks.tsx index 1fe0e7b75ba..8ed44e413a2 100644 --- a/src/ui/views/ManageAddress/hooks.tsx +++ b/src/ui/views/ManageAddress/hooks.tsx @@ -100,7 +100,9 @@ const sortScore = [ } ); -const getWalletScore = (s: TypeKeyringGroup[]) => { +export const getWalletScore = ( + s: TypeKeyringGroup[] | IDisplayedAccountWithBalance[] +) => { return sortScore[s?.[0]?.brandName || s?.[0]?.type] || DEFAULT_SCORE; };