From 42c2d56f6bfe8c43349d2d39f76e7a8be68eef39 Mon Sep 17 00:00:00 2001 From: noah Date: Thu, 30 Nov 2023 11:29:12 -0800 Subject: [PATCH] Add a Me DAOs page and improve inactive DAO handling (#1495) --- packages/i18n/locales/en/translation.json | 6 +- packages/state/indexer/search.ts | 8 +- .../components/CommandModalContextView.tsx | 5 +- .../useFollowingAndFilteredDaosSections.ts | 7 +- .../stateful/components/ChainSwitcher.tsx | 5 +- .../stateful/components/dao/LazyDaoCard.tsx | 54 +++++++++ packages/stateful/components/dao/index.tsx | 1 + packages/stateful/components/pages/Me.tsx | 2 + packages/stateful/components/pages/MeDaos.tsx | 10 ++ packages/stateful/components/pages/index.ts | 1 + .../stateful/components/wallet/WalletDaos.tsx | 26 ++++ packages/stateful/components/wallet/index.ts | 1 + packages/stateful/recoil/selectors/wallet.ts | 79 +++++++++++++ packages/stateless/components/Collapsible.tsx | 4 + .../components/inputs/ChainSwitcher.tsx | 108 +++++++++++------ .../components/wallet/WalletDaos.tsx | 111 ++++++++++++++++++ .../stateless/components/wallet/index.tsx | 1 + .../stateless/hooks/useQuerySyncedState.ts | 12 +- packages/stateless/hooks/useSearchFilter.ts | 11 +- packages/stateless/pages/Me.stories.tsx | 1 + packages/stateless/pages/Me.tsx | 6 + packages/types/command.ts | 2 + packages/types/me.ts | 2 + packages/types/stateless/Collapsible.ts | 1 + packages/types/stateless/DaoCard.tsx | 7 ++ packages/types/stateless/WalletDaos.tsx | 5 + packages/types/stateless/index.ts | 1 + packages/utils/constants/other.ts | 3 + 28 files changed, 427 insertions(+), 53 deletions(-) create mode 100644 packages/stateful/components/dao/LazyDaoCard.tsx create mode 100644 packages/stateful/components/pages/MeDaos.tsx create mode 100644 packages/stateful/components/wallet/WalletDaos.tsx create mode 100644 packages/stateless/components/wallet/WalletDaos.tsx create mode 100644 packages/types/stateless/WalletDaos.tsx diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 1812c16ff5..e8090d8eff 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -725,6 +725,7 @@ "addedToDaoFollowPrompt_withoutTimestamp": "You were added as a member to this DAO. Follow it to receive updates.", "advancedVotingConfigWarning": "These are advanced features. If you configure them without fully understanding how they work, you can lock your DAO, making it impossible for proposals to pass.", "allAccountsCreated": "All accounts have been created.", + "allChains": "All chains", "andNumMore": "and {{count}} more", "anyone": "Anyone", "appsProposalDescription": "Add a title and description, and then review the actions generated by the app. Once it all looks good, publish it to the DAO for voting.", @@ -833,6 +834,8 @@ "govTokenAddress": "Governance Token", "groupAddress": "CW4 Group", "historySinceDate": "History since {{date}}", + "inactiveDaoTooltip": "This DAO has no proposals and may be inactive or a duplicate.", + "inactiveDaosTooltip": "These DAOs have no proposals and may be inactive or duplicates.", "inboxConfigPreferencesDescription": "Choose where you want to receive notifications. Website notifications appear here on the Inbox page.", "inboxDescription": "Your notification inbox.", "inboxEmailTooltip": "Receive inbox notifications in your email.", @@ -896,7 +899,6 @@ "noPayrollDescription": "Disable all payroll features and hide them from the DAO's home page.", "noPostsFound": "No posts found.", "noPriceData": "No price data.", - "noProposalsTooltip": "This DAO has no proposals and may be inactive or a duplicate.", "noProposalsYet": "No proposals to vote on yet.", "noPushNotificationSubscriptions": "You do not have any push notification subscriptions.", "noSubDaosYet": "No SubDAOs yet.", @@ -980,6 +982,7 @@ "reviewYourProposal": "Review your proposal...", "reviewYourTransaction": "Review your transaction...", "saveTransactionDescription": "Save this transaction so you can reuse it later. Using an existing name will overwrite the previous save.", + "searchDaosPlaceholder": "Find a DAO...", "searchDraftPlaceholder": "Find a draft...", "searchForChain": "Search for a chain...", "searchForToken": "Search for a token...", @@ -1298,6 +1301,7 @@ "holdings": "Holdings", "home": "Home", "identity": "Identity", + "inactiveDaos": "Inactive DAOs", "inbox": "Inbox", "inboxConfiguration": "Inbox configuration", "inboxWithCount": "Inbox ({{count}})", diff --git a/packages/state/indexer/search.ts b/packages/state/indexer/search.ts index 7d409e5855..b6b025d4ec 100644 --- a/packages/state/indexer/search.ts +++ b/packages/state/indexer/search.ts @@ -2,6 +2,7 @@ import MeiliSearch from 'meilisearch' import { IndexerDumpState, WithChainId } from '@dao-dao/types' import { + INACTIVE_DAO_NAMES, SEARCH_API_KEY, SEARCH_HOST, getSupportedChainConfig, @@ -52,11 +53,8 @@ export const searchDaos = async ({ const results = await index.search>(query, { limit, filter: [ - // Only show DAOs with proposals to reduce clutter/spam. - // - // UPDATE: Commenting this out for now, since many DAOs have trouble - // finding themselves before they've made a proposal. - // `(NOT value.proposalCount EXISTS) OR (value.proposalCount > 0)`, + // Exclude inactive DAOs. + `NOT value.config.name IN ["${INACTIVE_DAO_NAMES.join('", "')}"]`, ...(exclude?.length ? // Exclude DAOs that are in the exclude list. [`NOT contractAddress IN ["${exclude.join('", "')}"]`] diff --git a/packages/stateful/command/components/CommandModalContextView.tsx b/packages/stateful/command/components/CommandModalContextView.tsx index 4d3b8d94c1..2abfde5c3a 100644 --- a/packages/stateful/command/components/CommandModalContextView.tsx +++ b/packages/stateful/command/components/CommandModalContextView.tsx @@ -92,7 +92,10 @@ export const InnerCommandModalContextView = ({ ...section, items: itemsWithSection .filter((optionWithSection) => optionWithSection.section === section) - .map(({ item: option }) => option), + .map(({ item: option }) => option) + .sort((a, b) => + a.sortLast && !b.sortLast ? 1 : !a.sortLast && b.sortLast ? -1 : 0 + ), })) return { diff --git a/packages/stateful/command/hooks/useFollowingAndFilteredDaosSections.ts b/packages/stateful/command/hooks/useFollowingAndFilteredDaosSections.ts index 935471da5b..d0230315bf 100644 --- a/packages/stateful/command/hooks/useFollowingAndFilteredDaosSections.ts +++ b/packages/stateful/command/hooks/useFollowingAndFilteredDaosSections.ts @@ -47,8 +47,8 @@ export const useFollowingAndFilteredDaosSections = ({ chainId: chain.chain_id, query: options.filter, limit, - // Exclude following DAOs from search since they show in a separate - // section. + // Exclude following DAOs from search since they show in a + // separate section. exclude: followingDaosLoading.loading ? undefined : followingDaosLoading.data @@ -85,7 +85,8 @@ export const useFollowingAndFilteredDaosSections = ({ // tooltip to indicate that it may not be active. ...(proposalCount === 0 && { className: 'opacity-50', - tooltip: t('info.noProposalsTooltip'), + tooltip: t('info.inactiveDaoTooltip'), + sortLast: true, }), }) ) diff --git a/packages/stateful/components/ChainSwitcher.tsx b/packages/stateful/components/ChainSwitcher.tsx index 7f3a04274a..244788cafd 100644 --- a/packages/stateful/components/ChainSwitcher.tsx +++ b/packages/stateful/components/ChainSwitcher.tsx @@ -22,8 +22,9 @@ export const ChainSwitcher = ({ - onSelect ? onSelect(chain_id) : setChainId(chain_id) + onSelect={(chain) => + chain && + (onSelect ? onSelect(chain.chain_id) : setChainId(chain.chain_id)) } selected={overrideChainId || chainId} /> diff --git a/packages/stateful/components/dao/LazyDaoCard.tsx b/packages/stateful/components/dao/LazyDaoCard.tsx new file mode 100644 index 0000000000..14a6d8ae88 --- /dev/null +++ b/packages/stateful/components/dao/LazyDaoCard.tsx @@ -0,0 +1,54 @@ +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' + +import { useCachedLoadingWithError } from '@dao-dao/stateless' +import { LazyDaoCardProps } from '@dao-dao/types' +import { processError } from '@dao-dao/utils' + +import { daoCardInfoSelector } from '../../recoil' +import { DaoCard } from './DaoCard' + +export const LazyDaoCard = (props: LazyDaoCardProps) => { + const { t } = useTranslation() + + const daoCardInfo = useCachedLoadingWithError( + daoCardInfoSelector({ + chainId: props.chainId, + coreAddress: props.coreAddress, + }) + ) + + return daoCardInfo.loading ? ( + + ) : daoCardInfo.errored || !daoCardInfo.data ? ( + + ) : ( + + ) +} diff --git a/packages/stateful/components/dao/index.tsx b/packages/stateful/components/dao/index.tsx index 8a42d7c8c2..3cdb265ccf 100644 --- a/packages/stateful/components/dao/index.tsx +++ b/packages/stateful/components/dao/index.tsx @@ -15,4 +15,5 @@ export * from './DaoTreasuryHistory' export * from './DiscordNotifierConfigureModal' export * from './DaoWidgets' export * from './DaoProviders' +export * from './LazyDaoCard' export * from './SdaDaoHome' diff --git a/packages/stateful/components/pages/Me.tsx b/packages/stateful/components/pages/Me.tsx index 7c6d38127b..d893759eb8 100644 --- a/packages/stateful/components/pages/Me.tsx +++ b/packages/stateful/components/pages/Me.tsx @@ -21,6 +21,7 @@ import { ConnectWallet } from '../ConnectWallet' import { ProfileDisconnectedCard, ProfileHomeCard } from '../profile' import { SuspenseLoader } from '../SuspenseLoader' import { MeBalances } from './MeBalances' +import { MeDaos } from './MeDaos' import { MeTransactionBuilder } from './MeTransactionBuilder' export const Me: NextPage = () => { @@ -64,6 +65,7 @@ export const Me: NextPage = () => { } diff --git a/packages/stateful/components/pages/MeDaos.tsx b/packages/stateful/components/pages/MeDaos.tsx new file mode 100644 index 0000000000..38643aa395 --- /dev/null +++ b/packages/stateful/components/pages/MeDaos.tsx @@ -0,0 +1,10 @@ +import { useWallet } from '../../hooks/useWallet' +import { WalletDaos } from '../wallet' + +export const MeDaos = () => { + const { address: walletAddress } = useWallet({ + loadAccount: false, + }) + + return walletAddress ? : null +} diff --git a/packages/stateful/components/pages/index.ts b/packages/stateful/components/pages/index.ts index b6b811edd1..ba5fddc053 100644 --- a/packages/stateful/components/pages/index.ts +++ b/packages/stateful/components/pages/index.ts @@ -1,4 +1,5 @@ export * from './Inbox' export * from './Me' export * from './MeBalances' +export * from './MeDaos' export * from './MeTransactionBuilder' diff --git a/packages/stateful/components/wallet/WalletDaos.tsx b/packages/stateful/components/wallet/WalletDaos.tsx new file mode 100644 index 0000000000..5ff89b35b2 --- /dev/null +++ b/packages/stateful/components/wallet/WalletDaos.tsx @@ -0,0 +1,26 @@ +import { + WalletDaos as StatelessWalletDaos, + useCachedLoadingWithError, +} from '@dao-dao/stateless' +import { StatefulWalletDaosProps } from '@dao-dao/types' + +import { allWalletDaosSelector, walletDaosSelector } from '../../recoil' +import { LazyDaoCard } from '../dao' + +export const WalletDaos = ({ + walletAddress, + chainId, +}: StatefulWalletDaosProps) => { + const walletDaos = useCachedLoadingWithError( + chainId + ? walletDaosSelector({ + walletAddress, + chainId, + }) + : allWalletDaosSelector({ + walletAddress, + }) + ) + + return +} diff --git a/packages/stateful/components/wallet/index.ts b/packages/stateful/components/wallet/index.ts index 444efeb065..c1e9df2500 100644 --- a/packages/stateful/components/wallet/index.ts +++ b/packages/stateful/components/wallet/index.ts @@ -1,4 +1,5 @@ export * from './ConnectedWalletDisplay' export * from './DisconnectWallet' +export * from './WalletDaos' export * from './WalletModals' export * from './WalletUi' diff --git a/packages/stateful/recoil/selectors/wallet.ts b/packages/stateful/recoil/selectors/wallet.ts index b8b7aafa0b..34287d8e8c 100644 --- a/packages/stateful/recoil/selectors/wallet.ts +++ b/packages/stateful/recoil/selectors/wallet.ts @@ -16,17 +16,21 @@ import { refreshWalletBalancesIdAtom, } from '@dao-dao/state/recoil' import { + LazyDaoCardProps, LazyNftCardInfo, MeTransactionSave, TokenCardInfo, TokenType, WithChainId, } from '@dao-dao/types' +import { Config } from '@dao-dao/types/contracts/DaoCore.v2' import { HIDDEN_BALANCE_PREFIX, + INACTIVE_DAO_NAMES, KVPK_API_BASE, ME_SAVED_TX_PREFIX, convertMicroDenomToDenomWithDecimals, + getFallbackImage, getNativeTokenForChainId, getSupportedChains, transformBech32Address, @@ -325,3 +329,78 @@ export const allWalletNftsSelector = selectorFamily< return [...nativeNfts, ...nativeStakedNfts] }, }) + +// Get DAOs this wallet is a member of. +export const walletDaosSelector = selectorFamily< + LazyDaoCardProps[], + // Can be any wallet address. + WithChainId<{ walletAddress: string }> +>({ + key: 'walletDaos', + get: + ({ chainId, walletAddress }) => + ({ get }) => { + const daos: { + dao: string + config: Config + proposalCount: number + }[] = get( + queryWalletIndexerSelector({ + chainId, + walletAddress, + formula: 'daos/memberOf', + }) + ) + if (!daos || !Array.isArray(daos)) { + return [] + } + + const lazyDaoCards = daos + .map( + ({ dao, config, proposalCount }): LazyDaoCardProps => ({ + chainId, + coreAddress: dao, + name: config.name, + description: config.description, + imageUrl: config.image_url || getFallbackImage(dao), + isInactive: + INACTIVE_DAO_NAMES.includes(config.name) || proposalCount === 0, + }) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + + return lazyDaoCards + }, +}) + +// Get DAOs across all DAO DAO-supported chains. +export const allWalletDaosSelector = selectorFamily< + LazyDaoCardProps[], + // Can be any wallet address. + { walletAddress: string } +>({ + key: 'allWalletDaos', + get: + ({ walletAddress }) => + ({ get }) => { + const chains = getSupportedChains() + + const allLazyDaoCards = get( + waitForAll( + chains.map(({ chain }) => + walletDaosSelector({ + chainId: chain.chain_id, + walletAddress: transformBech32Address( + walletAddress, + chain.chain_id + ), + }) + ) + ) + ) + .flat() + .sort((a, b) => a.name.localeCompare(b.name)) + + return allLazyDaoCards + }, +}) diff --git a/packages/stateless/components/Collapsible.tsx b/packages/stateless/components/Collapsible.tsx index 297f17c077..7592e006f1 100644 --- a/packages/stateless/components/Collapsible.tsx +++ b/packages/stateless/components/Collapsible.tsx @@ -5,12 +5,14 @@ import { CollapsibleProps } from '@dao-dao/types/stateless/Collapsible' import { toAccessibleImageUrl } from '@dao-dao/utils' import { DropdownIconButton } from './icon_buttons/DropdownIconButton' +import { TooltipInfoIcon } from './tooltip' const titleClassName = 'flex grow flex-row items-center gap-2 overflow-hidden py-2 transition-opacity hover:opacity-70 active:opacity-60 cursor-pointer' export const Collapsible = ({ label, + tooltip, imageUrl, link, defaultCollapsed = false, @@ -41,6 +43,8 @@ export const Collapsible = ({ )}

{label}

+ + {tooltip && } ) diff --git a/packages/stateless/components/inputs/ChainSwitcher.tsx b/packages/stateless/components/inputs/ChainSwitcher.tsx index 9352d407ed..9a79996807 100644 --- a/packages/stateless/components/inputs/ChainSwitcher.tsx +++ b/packages/stateless/components/inputs/ChainSwitcher.tsx @@ -1,7 +1,8 @@ import { Chain } from '@chain-registry/types' import { ArrowDropDown } from '@mui/icons-material' import clsx from 'clsx' -import { useMemo, useRef } from 'react' +import { ComponentType, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' import { ButtonPopupProps, ButtonPopupSectionButton } from '@dao-dao/types' import { @@ -22,8 +23,33 @@ export type ChainSwitcherProps = Omit< loading?: boolean excludeChainIds?: string[] - onSelect: (chain: Chain) => void - selected: string + /** + * Called when a chain is selected. If the none button is selected, it will be + * called with undefined. + */ + onSelect: (chain?: Chain) => void + /** + * When defined, this will be the selected chain. When undefined, selects the + * none button. + */ + selected: string | undefined + + /** + * If true, a button will be shown at the top that represents none. + */ + showNone?: boolean + /** + * If defined, this will be the label of the none button. + */ + noneLabel?: string + /** + * If defined, this will be the icon of the none button. + */ + noneIcon?: ComponentType<{ className?: string }> +} + +type ChainSwitcherButton = Omit & { + chain?: Chain } export const ChainSwitcher = ({ @@ -32,9 +58,15 @@ export const ChainSwitcher = ({ loading, wrapperClassName, excludeChainIds, + showNone, + noneLabel, + noneIcon, ...props }: ChainSwitcherProps) => { - const chain = getChainForChainId(selected) + const { t } = useTranslation() + + noneLabel ||= t('info.none') + const chain = selected ? getChainForChainId(selected) : undefined const excludeChainIdsRef = useRef(excludeChainIds) excludeChainIdsRef.current = excludeChainIds @@ -45,53 +77,55 @@ export const ChainSwitcher = ({ return } - const ChainIcon = makeChainIcon(chain.chain_id) + const Icon = chain ? makeChainIcon(chain.chain_id) : noneIcon const chainSwitcherTriggerContent = ( <> - -

{getDisplayNameForChainId(chain.chain_id)}

+ {Icon && } + +

{chain ? getDisplayNameForChainId(chain.chain_id) : noneLabel}

+ ) const chainSwitcherSections = [ { - buttons: getSupportedChains() - .filter( - ({ chain: { chain_id: chainId } }) => - !excludeChainIdsRef.current?.includes(chainId) - ) - .map( - ({ - chain, - }): Omit & { - chain: Chain - } => ({ - chain, - label: getDisplayNameForChainId(chain.chain_id), - pressed: selected === chain.chain_id, - Icon: makeChainIcon(chain.chain_id), - }) - ) - .sort((a, b) => { - // Sort selected to the top. - if (a.pressed && !b.pressed) { - return -1 - } else if (!a.pressed && b.pressed) { - return 1 - } - - // Sort alphabetically by label. - return a.label.localeCompare(b.label) - }), + buttons: [ + ...(showNone + ? ([ + { + label: noneLabel, + pressed: !chain, + Icon: noneIcon, + }, + ] as ChainSwitcherButton[]) + : []), + ...getSupportedChains() + .filter( + ({ chain: { chain_id: chainId } }) => + !excludeChainIdsRef.current?.includes(chainId) + ) + .map( + ({ chain }): ChainSwitcherButton => ({ + chain, + label: getDisplayNameForChainId(chain.chain_id), + pressed: selected === chain.chain_id, + Icon: makeChainIcon(chain.chain_id), + }) + ) + .sort((a, b) => + // Sort alphabetically by label. + a.label.localeCompare(b.label) + ), + ], }, ] return { - ChainIcon, + ChainIcon: Icon, chainSwitcherTriggerContent, chainSwitcherSections, } - }, [chain.chain_id, selected]) + }, [chain, noneIcon, noneLabel, selected, showNone]) return ( + LazyDaoCard: ComponentType +} + +export const WalletDaos = ({ daos, LazyDaoCard }: WalletDaosProps) => { + const { t } = useTranslation() + + const allDaos = daos.loading || daos.errored || !daos.data ? [] : daos.data + const { searchBarProps, filteredData } = useSearchFilter( + allDaos, + FILTERABLE_KEYS, + undefined, + 'dq' + ) + + const [chainId, setChainId] = useQuerySyncedState({ + param: 'dqc', + defaultValue: undefined, + }) + + const [showingInactive, setShowingInactive] = useState(false) + + const filteredDaos = chainId + ? filteredData.filter(({ item }) => item.chainId === chainId) + : filteredData + + const activeDaos = filteredDaos.filter(({ item }) => !item.isInactive) + const inactiveDaos = filteredDaos.filter(({ item }) => item.isInactive) + + return daos.loading ? ( + + ) : daos.errored ? ( + +
+        {daos.error instanceof Error ? daos.error.message : `${daos.error}`}
+      
+
+ ) : ( +
+
+ + + setChainId(chain?.chain_id)} + selected={chainId} + showNone + wrapperClassName="flex flex-row items-stretch" + /> +
+ + {activeDaos.length > 0 || inactiveDaos.length > 0 ? ( + <> + {activeDaos.length > 0 && ( + + {activeDaos.map(({ item: dao }) => ( + + ))} + + )} + + {inactiveDaos.length > 0 && ( + setShowingInactive(expanded)} + tooltip={t('info.inactiveDaosTooltip')} + > + + {inactiveDaos.map(({ item: dao }) => ( + + ))} + + + )} + + ) : ( + + )} +
+ ) +} + +const FILTERABLE_KEYS: Fuse.FuseOptionKey[] = [ + 'chainId', + 'coreAddress', + 'name', + 'description', +] diff --git a/packages/stateless/components/wallet/index.tsx b/packages/stateless/components/wallet/index.tsx index d3863aeb29..df3d39ad57 100644 --- a/packages/stateless/components/wallet/index.tsx +++ b/packages/stateless/components/wallet/index.tsx @@ -1,4 +1,5 @@ export * from './ConnectedWallet' export * from './ConnectWallet' export * from './DisconnectWallet' +export * from './WalletDaos' export * from './WalletLogo' diff --git a/packages/stateless/hooks/useQuerySyncedState.ts b/packages/stateless/hooks/useQuerySyncedState.ts index 4ffe94cdc7..1cd06a049f 100644 --- a/packages/stateless/hooks/useQuerySyncedState.ts +++ b/packages/stateless/hooks/useQuerySyncedState.ts @@ -2,7 +2,11 @@ import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' export type UseQuerySyncedStateOptions = { - param: string + /** + * The query parameter. When undefined, the state will not be synced, making + * this hook act exactly like `useState`. + */ + param?: string defaultValue: T } @@ -40,6 +44,10 @@ export const useQuerySyncedState = ({ } pageInitialized.current = true + if (!param) { + return + } + const initialValue = router.query[param] if (typeof initialValue === 'string') { setValue( @@ -54,7 +62,7 @@ export const useQuerySyncedState = ({ // On value change, store in query parameter. If default, remove. useEffect(() => { - if (!pageInitialized.current) { + if (!pageInitialized.current || !param) { return } diff --git a/packages/stateless/hooks/useSearchFilter.ts b/packages/stateless/hooks/useSearchFilter.ts index 0077072f09..86db7a8306 100644 --- a/packages/stateless/hooks/useSearchFilter.ts +++ b/packages/stateless/hooks/useSearchFilter.ts @@ -9,6 +9,7 @@ import { } from 'react' import { SearchBarProps } from '../components' +import { useQuerySyncedState } from './useQuerySyncedState' interface UseSearchFilterReturn { searchBarProps: SearchBarProps @@ -39,7 +40,9 @@ interface UseSearchFilterReturn { export const useSearchFilter = ( data: T[], filterableKeys: Fuse.FuseOptionKey[], - options?: Fuse.IFuseOptions + options?: Fuse.IFuseOptions, + // If defined, store the search param in a query param. + querySyncedParam?: string ): UseSearchFilterReturn => { // Store latest data in a ref so we can compare it to the current data. const dataRef = useRef(data) @@ -54,7 +57,11 @@ export const useSearchFilter = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterableKeys, options]) - const [filter, setFilter] = useState('') + const [filter, setFilter] = useQuerySyncedState({ + // When param is undefined, this is just like a normal `useState` hook. + param: querySyncedParam, + defaultValue: '', + }) // When collection or filter is updated, re-filter. const filteredData: UseSearchFilterReturn['filteredData'] = useMemo(() => { diff --git a/packages/stateless/pages/Me.stories.tsx b/packages/stateless/pages/Me.stories.tsx index c9de4dfddb..916149d766 100644 --- a/packages/stateless/pages/Me.stories.tsx +++ b/packages/stateless/pages/Me.stories.tsx @@ -53,6 +53,7 @@ Default.args = { {...(MeTransactionBuilderStory.args as MeTransactionBuilderProps)} /> ), + MeDaos: () =>
, profileData: WALLET_PROFILE_DATA, ChainSwitcher, } diff --git a/packages/stateless/pages/Me.tsx b/packages/stateless/pages/Me.tsx index 633a1710d8..d2e27b78b2 100644 --- a/packages/stateless/pages/Me.tsx +++ b/packages/stateless/pages/Me.tsx @@ -24,6 +24,7 @@ export const Me = ({ rightSidebarContent, MeBalances, MeTransactionBuilder, + MeDaos, profileData, updateProfileName, ChainSwitcher, @@ -37,6 +38,11 @@ export const Me = ({ label: t('title.balances'), Component: MeBalances, }, + { + id: MeTabId.Daos, + label: t('title.daos'), + Component: MeDaos, + }, { id: MeTabId.TransactionBuilder, label: t('title.transactionBuilder'), diff --git a/packages/types/command.ts b/packages/types/command.ts index 1cf6b130ae..8827fd6c7d 100644 --- a/packages/types/command.ts +++ b/packages/types/command.ts @@ -18,6 +18,8 @@ export type CommandModalContextSectionItem< className?: string disabled?: boolean loading?: boolean + // If true, sorts last even if it matches search. + sortLast?: boolean } & ( | { imageUrl: string diff --git a/packages/types/me.ts b/packages/types/me.ts index 4281ab8212..e16e46e4a2 100644 --- a/packages/types/me.ts +++ b/packages/types/me.ts @@ -24,6 +24,7 @@ export type MeTransactionSave = MeTransactionForm & { // Value goes in URL hash. export enum MeTabId { Balances = 'balances', + Daos = 'daos', TransactionBuilder = 'tx', } @@ -37,6 +38,7 @@ export type MeProps = { rightSidebarContent: ReactNode MeBalances: ComponentType MeTransactionBuilder: ComponentType + MeDaos: ComponentType profileData: WalletProfileData updateProfileName: (name: string | null) => Promise ChainSwitcher: ComponentType diff --git a/packages/types/stateless/Collapsible.ts b/packages/types/stateless/Collapsible.ts index ea8317cbba..002398c443 100644 --- a/packages/types/stateless/Collapsible.ts +++ b/packages/types/stateless/Collapsible.ts @@ -4,6 +4,7 @@ import { LinkWrapperProps } from './LinkWrapper' export interface CollapsibleProps { label: string + tooltip?: string imageUrl?: string link?: { href: string diff --git a/packages/types/stateless/DaoCard.tsx b/packages/types/stateless/DaoCard.tsx index 676998ee7c..aa60a5de8e 100644 --- a/packages/types/stateless/DaoCard.tsx +++ b/packages/types/stateless/DaoCard.tsx @@ -43,3 +43,10 @@ export interface DaoCardProps extends DaoCardInfo { IconButtonLink: ComponentType follow: { hide: true } | ({ hide?: false } & FollowState) } + +export type LazyDaoCardProps = Pick< + DaoCardProps, + 'chainId' | 'coreAddress' | 'name' | 'description' | 'imageUrl' | 'className' +> & { + isInactive?: boolean +} diff --git a/packages/types/stateless/WalletDaos.tsx b/packages/types/stateless/WalletDaos.tsx new file mode 100644 index 0000000000..d500387060 --- /dev/null +++ b/packages/types/stateless/WalletDaos.tsx @@ -0,0 +1,5 @@ +export type StatefulWalletDaosProps = { + walletAddress: string + // If defined, filter by this chain. + chainId?: string +} diff --git a/packages/types/stateless/index.ts b/packages/types/stateless/index.ts index 434e45bdcd..440e766148 100644 --- a/packages/types/stateless/index.ts +++ b/packages/types/stateless/index.ts @@ -44,3 +44,4 @@ export * from './TokenAmountDisplay' export * from './TokenSwapStatus' export * from './Trans' export * from './ValidatorPicker' +export * from './WalletDaos' diff --git a/packages/utils/constants/other.ts b/packages/utils/constants/other.ts index 49e09ef501..17ea57cec3 100644 --- a/packages/utils/constants/other.ts +++ b/packages/utils/constants/other.ts @@ -62,3 +62,6 @@ export const NUM_FEATURED_DAOS = 10 // Neutron governance DAO. export const NEUTRON_GOVERNANCE_DAO = 'neutron1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrstdxvff' + +// DAOs with these names will be excluded from search. +export const INACTIVE_DAO_NAMES = ['[archived]', '[deleted]']