diff --git a/src/composables/accountSelector.ts b/src/composables/accountSelector.ts index 14a88f479..019d30da4 100644 --- a/src/composables/accountSelector.ts +++ b/src/composables/accountSelector.ts @@ -1,34 +1,201 @@ import { useAddressBook } from '@/composables/addressBook'; -import { ref } from 'vue'; -import { ACCOUNT_SELECT_TYPE_FILTER, AccountSelectTypeFilter } from '@/constants'; -import { createCustomScopedComposable } from '@/utils'; +import { computed, ref, watch } from 'vue'; + +import { + IAddressBookEntry, + type ICommonTransaction, + type ITransaction, +} from '@/types'; +import { + ACCOUNT_SELECT_TYPE_FILTER, + AccountSelectTypeFilter, + PROTOCOLS, + TX_DIRECTION, +} from '@/constants'; +import { createCustomScopedComposable, getDefaultAccountLabel, pipe } from '@/utils'; +import { useAccounts, useLatestTransactionList } from '@/composables'; + +import { useAeMiddleware } from '@/protocols/aeternity/composables'; +import { useAeNames } from '@/protocols/aeternity/composables/aeNames'; +import { + getInnerTransaction, + getOwnershipStatus, + getTxDirection, + getTxOwnerAddress, +} from '@/protocols/aeternity/helpers'; +import { AE_TRANSACTION_OWNERSHIP_STATUS } from '@/protocols/aeternity/config'; export const useAccountSelector = createCustomScopedComposable(() => { + const { getName } = useAeNames(); + const { getMiddleware } = useAeMiddleware(); + const latestTransactions = ref([]); + const searchQuery = ref(''); const { addressBookFiltered, addressBookFilteredByProtocol, protocolFilter, - searchQuery, showBookmarked, + getAddressBookEntryByAddress, setProtocolFilter, setShowBookmarked, clearFilters: addressBookClearFilters, } = useAddressBook(); + const { + accounts, + activeAccount, + accountsGroupedByProtocol, + getAccountByAddress, + } = useAccounts(); + const { accountsTransactionsLatest } = useLatestTransactionList(); const accountSelectType = ref(ACCOUNT_SELECT_TYPE_FILTER.addressBook); + const prevAccountSelectType = ref(accountSelectType.value); + const ownAddresses = computed(() => { + if (protocolFilter.value) { + return accountsGroupedByProtocol.value[protocolFilter.value]?.map((account) => ( + { + name: getName(account.address).value || getDefaultAccountLabel(account), + address: account.address, + isBookmarked: false, + protocol: protocolFilter.value ?? PROTOCOLS.aeternity, + isOwnAddress: true, + type: account.type, + } + )); + } + return []; + }); + const accountsFilteredByType = computed( + () => { + switch (accountSelectType.value) { + case ACCOUNT_SELECT_TYPE_FILTER.bookmarked: + return addressBookFiltered.value; + case ACCOUNT_SELECT_TYPE_FILTER.addressBook: + return addressBookFiltered.value; + case ACCOUNT_SELECT_TYPE_FILTER.recent: + return latestTransactions.value; + case ACCOUNT_SELECT_TYPE_FILTER.owned: + return ownAddresses.value; + case ACCOUNT_SELECT_TYPE_FILTER.all: + return [...(addressBookFiltered.value ?? []), ...(ownAddresses.value ?? [])]; + default: + return []; + } + }, + ); + function filterAccountsBookBySearchPhrase(entries: IAddressBookEntry[]) { + const searchQueryLower = searchQuery.value.toLowerCase(); + return entries.filter(({ name, address }) => ( + [name, address].some((val) => val.toLowerCase().includes(searchQueryLower)) + )); + } + function sortAccounts(entries: IAddressBookEntry[]) { + return entries.sort((a, b) => { + if (a === b) return 0; + return a ? 1 : -1; + }); + } + function removeDuplicates(entries: IAddressBookEntry[]) { + return entries.filter( + (addr1, i, addresses) => addresses.findIndex( + (addr2) => addr2.address === addr1.address, + ) === i, + ); + } + const accountsFiltered = computed( + () => pipe([ + filterAccountsBookBySearchPhrase, + sortAccounts, + removeDuplicates, + ])(accountsFilteredByType.value ?? []), + ); function setAccountSelectType(type: AccountSelectTypeFilter, resetProtocolFilter = false) { accountSelectType.value = type; setShowBookmarked(type === ACCOUNT_SELECT_TYPE_FILTER.bookmarked, resetProtocolFilter); } - function clearFilters(resetProtocolFilter = false) { accountSelectType.value = ACCOUNT_SELECT_TYPE_FILTER.addressBook; addressBookClearFilters(resetProtocolFilter); } + watch( + () => [accountsTransactionsLatest.value, activeAccount.value.address, protocolFilter.value], + async () => { + const filteredTransactions = accountsTransactionsLatest + .value[activeAccount.value.address].filter( + (transaction: ICommonTransaction) => { + const outerTx = transaction.tx!; + const innerTx = transaction.tx ? getInnerTransaction(transaction.tx) : null; + const txOwnerAddress = getTxOwnerAddress(innerTx); + + const direction = getTxDirection( + outerTx?.payerId ? outerTx : innerTx, + (transaction as ITransaction).transactionOwner + || ( + ( + getOwnershipStatus(activeAccount.value, accounts.value, innerTx) + !== AE_TRANSACTION_OWNERSHIP_STATUS.current + ) + && txOwnerAddress + ) + || activeAccount.value.address, + ); + + return ( + direction === TX_DIRECTION.sent + && (outerTx?.payerId ? outerTx : innerTx).recipientId + ); + }, + ); + + latestTransactions.value = await Promise.all( + filteredTransactions + .map(async (transaction: ICommonTransaction): Promise => { + const outerTx = transaction.tx!; + const innerTx = transaction.tx ? getInnerTransaction(transaction.tx) : null; + const { recipientId } = outerTx?.payerId ? outerTx : innerTx; + const middleware = await getMiddleware(); + + let address = recipientId; + const accountFound = getAccountByAddress(recipientId!); + let name = getName(accountFound?.address).value + || getDefaultAccountLabel(accountFound); + if (recipientId?.startsWith('nm_')) { + address = (await middleware.getName(recipientId)).name; + name = address; + } + const addressBookEntryByAddress = getAddressBookEntryByAddress(address); + if (addressBookEntryByAddress) { + return addressBookEntryByAddress; + } + return { + name, + address, + isBookmarked: false, + protocol: protocolFilter.value ?? PROTOCOLS.aeternity, + }; + }), + ); + }, + { immediate: true }, // Run immediately on initialization + ); + + // Storing the previous type in order to revert to it when the input is cleared + let savedPrevAccountSelectType = false; + watch(searchQuery, (newSearch) => { + if (newSearch !== '' && !savedPrevAccountSelectType) { + savedPrevAccountSelectType = true; + prevAccountSelectType.value = accountSelectType.value; + accountSelectType.value = ACCOUNT_SELECT_TYPE_FILTER.all; + } else if (newSearch === '') { + savedPrevAccountSelectType = false; + accountSelectType.value = prevAccountSelectType.value; + } + }); + return { accountSelectType, - addressBookFiltered, + accountsFiltered, addressBookFilteredByProtocol, protocolFilter, showBookmarked, diff --git a/src/popup/components/AddressBook/AddressBookFilters.vue b/src/popup/components/AddressBook/AddressBookFilters.vue index d6720f8e2..f7700fa6d 100644 --- a/src/popup/components/AddressBook/AddressBookFilters.vue +++ b/src/popup/components/AddressBook/AddressBookFilters.vue @@ -13,6 +13,7 @@ /> @@ -25,7 +27,13 @@ class="list" > + -

+ @@ -64,38 +88,33 @@ import { IonHeader, IonContent } from '@ionic/vue'; import { useRoute } from 'vue-router'; import { tg } from '@/popup/plugins/i18n'; -import { - ComponentRef, - IAddressBookEntry, - type ICommonTransaction, - type ITransaction, -} from '@/types'; +import { ComponentRef, type AccountType } from '@/types'; import { ROUTE_ADDRESS_BOOK, ROUTE_ADDRESS_BOOK_EDIT } from '@/popup/router/routeNames'; import { ProtocolAdapterFactory } from '@/lib/ProtocolAdapterFactory'; import { - useAccounts, useAccountSelector, - useLatestTransactionList, } from '@/composables'; import InputSearch from '@/popup/components/InputSearch.vue'; import AccountInfo from '@/popup/components/AccountInfo.vue'; import AddressBookFilters from '@/popup/components/AddressBook/AddressBookFilters.vue'; import PanelItem from '@/popup/components/PanelItem.vue'; -import { - getInnerTransaction, - getOwnershipStatus, - getTxDirection, - getTxOwnerAddress, -} from '@/protocols/aeternity/helpers'; -import { AE_TRANSACTION_OWNERSHIP_STATUS } from '@/protocols/aeternity/config'; -import { ACCOUNT_SELECT_TYPE_FILTER, PROTOCOLS, TX_DIRECTION } from '@/constants'; -import { useAeNames } from '@/protocols/aeternity/composables/aeNames'; -import { useAeMiddleware } from '@/protocols/aeternity/composables'; -import { getDefaultAccountLabel } from '@/utils'; +import { ACCOUNT_SELECT_TYPE_FILTER, ACCOUNT_TYPES } from '@/constants'; +import { getAddressColor } from '@/utils'; + +import BtnBase from '@/popup/components/buttons/BtnBase.vue'; +import BtnMain from '@/popup/components/buttons/BtnMain.vue'; +import IconWrapper from '@/popup/components/IconWrapper.vue'; + +import AirGapIcon from '@/icons/air-gap.svg?vue-component'; +import PrivateKeyIcon from '@/icons/private-key.svg?vue-component'; +import PlusCircleIcon from '@/icons/plus-circle.svg?vue-component'; export default defineComponent({ components: { + IconWrapper, + BtnMain, + BtnBase, IonHeader, IonContent, AccountInfo, @@ -112,38 +131,17 @@ export default defineComponent({ setup(props, { emit }) { const scrollWrapperEl = ref(); const isScrolled = ref(false); - const latestTransactions = ref([]); - const { getName } = useAeNames(); - const { getMiddleware } = useAeMiddleware(); - const { - accounts, - activeAccount, - accountsGroupedByProtocol, - } = useAccounts(); const { accountSelectType, - addressBookFiltered, + accountsFiltered, addressBookFilteredByProtocol, protocolFilter, searchQuery, showBookmarked, setAccountSelectType, } = useAccountSelector(); - const ownAddresses = computed(() => { - if (protocolFilter.value) { - return accountsGroupedByProtocol.value[protocolFilter.value]?.map((account) => ( - { - name: getName(account.address).value || getDefaultAccountLabel(account), - address: account.address, - isBookmarked: false, - protocol: protocolFilter.value ?? PROTOCOLS.aeternity, - } - )); - } - return []; - }); - const { accountsTransactionsLatest } = useLatestTransactionList(); + const route = useRoute(); const noRecordsMessage = computed(() => { @@ -154,6 +152,8 @@ export default defineComponent({ return tg('pages.addressBook.noRecords.addressBook'); case showBookmarked.value: return tg('pages.addressBook.noRecords.bookmarked'); + case accountSelectType.value === ACCOUNT_SELECT_TYPE_FILTER.recent: + return tg('pages.addressBook.noRecords.recent'); default: return tg('pages.addressBook.noRecords.blockchain'); } @@ -163,25 +163,15 @@ export default defineComponent({ ? ProtocolAdapterFactory.getAdapter(protocolFilter.value)?.protocolName : ''); const hideOuterButtons = computed(() => isScrolled.value || !!searchQuery.value); + const isSearchVisible = computed(() => ( + ( + (!protocolFilter.value || showBookmarked.value) + && Object.keys(addressBookFilteredByProtocol.value).length > 9 + ) || props.isSelector + ) || hideOuterButtons.value); const hasBookmarkedEntries = computed( () => addressBookFilteredByProtocol.value.some((entry) => entry.isBookmarked), ); - const accountsFiltered = computed( - () => { - switch (accountSelectType.value) { - case ACCOUNT_SELECT_TYPE_FILTER.bookmarked: - return addressBookFiltered.value; - case ACCOUNT_SELECT_TYPE_FILTER.addressBook: - return addressBookFiltered.value; - case ACCOUNT_SELECT_TYPE_FILTER.recent: - return latestTransactions.value; - case ACCOUNT_SELECT_TYPE_FILTER.owned: - return ownAddresses.value; - default: - return []; - } - }, - ); const handleScroll = throttle(() => { if (!scrollWrapper.value) return; @@ -189,10 +179,28 @@ export default defineComponent({ emit('update:hideButtons', hideOuterButtons.value); }, 100); + function accountIcon(type: AccountType) { + switch (type) { + case ACCOUNT_TYPES.airGap: + return AirGapIcon; + case ACCOUNT_TYPES.privateKey: + return PrivateKeyIcon; + default: + return null; + } + } + function bgColorStyle(isOwnAddress: boolean, address: String) { + return isOwnAddress ? { '--bg-color': getAddressColor(address) } : {}; + } + onMounted(() => { scrollWrapper.value?.addEventListener('scroll', handleScroll); - if (props.isSelector && hasBookmarkedEntries.value) { - setAccountSelectType(ACCOUNT_SELECT_TYPE_FILTER.bookmarked); + if (props.isSelector) { + if (hasBookmarkedEntries.value) { + setAccountSelectType(ACCOUNT_SELECT_TYPE_FILTER.bookmarked); + } else { + setAccountSelectType(ACCOUNT_SELECT_TYPE_FILTER.addressBook); + } } }); @@ -205,77 +213,20 @@ export default defineComponent({ } }, { deep: true }); - watch( - () => [accountsTransactionsLatest.value, activeAccount.value.address, protocolFilter.value], - async () => { - // Step 1: Filter transactions synchronously - const filteredTransactions = accountsTransactionsLatest - .value[activeAccount.value.address].filter( - (transaction: ICommonTransaction) => { - const outerTx = transaction.tx!; - const innerTx = transaction.tx ? getInnerTransaction(transaction.tx) : null; - const txOwnerAddress = getTxOwnerAddress(innerTx); - - const direction = getTxDirection( - outerTx?.payerId ? outerTx : innerTx, - (transaction as ITransaction).transactionOwner - || ( - ( - getOwnershipStatus(activeAccount.value, accounts.value, innerTx) - !== AE_TRANSACTION_OWNERSHIP_STATUS.current - ) - && txOwnerAddress - ) - || activeAccount.value.address, - ); - - return ( - direction === TX_DIRECTION.sent - && (outerTx?.payerId ? outerTx : innerTx).recipientId - ); - }, - ); - - const addressEntries = await Promise.all( - filteredTransactions - .map(async (transaction: ICommonTransaction): Promise => { - const outerTx = transaction.tx!; - const innerTx = transaction.tx ? getInnerTransaction(transaction.tx) : null; - const { recipientId } = outerTx?.payerId ? outerTx : innerTx; - const middleware = await getMiddleware(); - - let address = recipientId; - if (recipientId?.startsWith('nm_')) { - address = (await middleware.getName(recipientId)).name; - } - return { - name: protocolFilter.value === PROTOCOLS.aeternity ? getName(recipientId).value : '', - address, - isBookmarked: false, - protocol: protocolFilter.value ?? PROTOCOLS.aeternity, - }; - }), - ); - - // Removing duplicates - latestTransactions.value = addressEntries.filter( - (addr1, i, addresses) => addresses.findIndex( - (addr2) => addr2.address === addr1.address, - ) === i, - ); - }, - { immediate: true }, // Run immediately on initialization - ); - return { ROUTE_ADDRESS_BOOK_EDIT, + ROUTE_ADDRESS_BOOK, scrollWrapperEl, + isSearchVisible, hasBookmarkedEntries, accountSelectType, searchQuery, accountsFiltered, noRecordsMessage, protocolName, + bgColorStyle, + accountIcon, + PlusCircleIcon, }; }, }); @@ -297,6 +248,11 @@ export default defineComponent({ box-shadow: none; } + .address-book-item { + background-color: var(--bg-color); + border: var(--border-width) solid var(--bg-color); + } + .search-field { margin-bottom: 16px; } @@ -311,5 +267,16 @@ export default defineComponent({ text-align: center; padding: 40px 8px; } + + .account-type-icon { + width: 20px; + height: 20px; + opacity: 0.85; + } + + .add-record-button { + width: 100%; + gap: 4px; + } } diff --git a/src/popup/components/PanelItem.vue b/src/popup/components/PanelItem.vue index f0630c26b..85e2f0384 100644 --- a/src/popup/components/PanelItem.vue +++ b/src/popup/components/PanelItem.vue @@ -35,6 +35,11 @@ /> +

+ +
@@ -135,5 +140,11 @@ export default defineComponent({ transition: $transition-interactive; } } + + .panel-item-bottom-right { + display: inline-flex; + align-self: flex-end; + margin-right: 4px; + } } diff --git a/src/popup/locales/en.json b/src/popup/locales/en.json index 657ee8ce9..7df77a824 100644 --- a/src/popup/locales/en.json +++ b/src/popup/locales/en.json @@ -634,10 +634,12 @@ "searchPlaceholder": "Search for name or {0}address", "scanTitle": "Scan the address you want to add", "selectAddress": "Select recipient’s address", + "addAddressRecord": "Add new address records", "noRecords": { "addressBook": "There are no records in your address book.", "blockchain": "Currently there are no address records for the blockchain selected.", "bookmarked": "There are no bookmarked address records.", + "recent": "There are no recent recipients.", "search": "No address records found for your search query." }, "entry": {