diff --git a/common_knowledge/App-Initialization-Flow.md b/common_knowledge/App-Initialization-Flow.md index 6811d3a6808..40d1719ec36 100644 --- a/common_knowledge/App-Initialization-Flow.md +++ b/common_knowledge/App-Initialization-Flow.md @@ -31,9 +31,9 @@ The goal of this document is to describe the current state of the app initializa 2. This `Layout` component includes a nested `Suspense` component (via the `react` library) and `ErrorBoundary` component (via the `react-error-boundary` library). 3. The `LayoutComponent` operates as follows: 1. If, _at render time_, `app.loadingError` is set, display an application error. - 2. If, _at render time_, `app.config.chains.getById(scope)` does not return successfully, render a `PageNotFound` view. - 3. If, _at render time_, there is no `selectedScope`, and no custom domain, then whatever chain is currently being loaded is deinitialized by `deinitChainOrCommunity`, and the `ScopeToLoad` state variable set to `null`. A loading spinner is shown, triggered by `shouldShowLoadingState`. - 4. If, _at render time_, the `selectedScope` differs from `app.activeChainId()`, the `isLoading` and `scopeToLoad` global state variables are updated (to `true` and `selectedScope`, respectively). `selectChain()` is fired, receiving new scope's chain as argument; on completion, the `isLoading` state variable is set to `false`. + 2. If, _at render time_, `useGetCommunityByIdQuery(scope)` does not return successfully, render a `PageNotFound` view. + 3. If, _at render time_, there is no `providedCommunityScope`, and no custom domain, then whatever chain is currently being loaded is deinitialized by `deinitChainOrCommunity`, and the `communityToLoad` state variable set to `null`. A loading spinner is shown, triggered by `shouldShowLoadingState`. + 4. If, _at render time_, the `providedCommunityScope` differs from `app.activeChainId()`, the `isLoading` and `communityToLoad` global state variables are updated (to `true` and `providedCommunityScope`, respectively). `selectChain()` is fired, receiving new scope's chain as argument; on completion, the `isLoading` state variable is set to `false`. 5. If none of these conditions apply, the routed-to page is rendered. 9. If `selectChain()` (`/helpers/chain.ts`) is fired, per step #8: 1. If no `chain` argument is passed, the function defaults to a `chain` value set my `app.user.selectedChain`, or else 'edgeware'. @@ -66,4 +66,4 @@ The goal of this document is to describe the current state of the app initializa ## Change Log -- 230810: Authored by Graham Johnson (#4763). +- 230810: Authored by Graham Johnson (#4763). \ No newline at end of file diff --git a/libs/model/src/community/GetCommunity.query.ts b/libs/model/src/community/GetCommunity.query.ts index 431be654578..433b42dd8e8 100644 --- a/libs/model/src/community/GetCommunity.query.ts +++ b/libs/model/src/community/GetCommunity.query.ts @@ -24,6 +24,7 @@ export function GetCommunity(): Query { ], }, ]; + if (payload.include_node_info) { include.push({ model: models.ChainNode, diff --git a/libs/model/src/community/GetStakeTransaction.query.ts b/libs/model/src/community/GetStakeTransaction.query.ts index 0d524239e27..bab8da7acf1 100644 --- a/libs/model/src/community/GetStakeTransaction.query.ts +++ b/libs/model/src/community/GetStakeTransaction.query.ts @@ -33,10 +33,12 @@ export function GetStakeTransaction(): Query< 'default_symbol', c.default_symbol, 'icon_url', c.icon_url, 'name', c.name, - 'chain_node_id', c.chain_node_id + 'chain_node_id', c.chain_node_id, + 'chain_node_name', cn.name ) AS community FROM "StakeTransactions" AS t LEFT JOIN "Communities" AS c ON c.id = t.community_id + LEFT JOIN "ChainNodes" AS cn ON cn.id = c.chain_node_id LEFT JOIN "CommunityStakes" AS cs ON cs.community_id = t.community_id ${addressFilter} `, diff --git a/libs/schemas/src/entities/community.schemas.ts b/libs/schemas/src/entities/community.schemas.ts index 8799e33c681..c4c03aededa 100644 --- a/libs/schemas/src/entities/community.schemas.ts +++ b/libs/schemas/src/entities/community.schemas.ts @@ -20,7 +20,7 @@ export const Community = z.object({ name: z.string(), chain_node_id: PG_INT, default_symbol: z.string().default(''), - network: z.nativeEnum(ChainNetwork).default(ChainNetwork.Ethereum), + network: z.string().default(ChainNetwork.Ethereum), base: z.nativeEnum(ChainBase), icon_url: z.string().nullish(), active: z.boolean(), @@ -68,5 +68,17 @@ export const Community = z.object({ contest_managers: z.array(ContestManager).optional(), }); +export const ExtendedCommunity = Community.extend({ + numVotingThreads: PG_INT, + numTotalThreads: PG_INT, + adminsAndMods: z.array( + z.object({ + address: z.string(), + role: z.enum(['admin', 'moderator']), + }), + ), + communityBanner: z.string().nullish(), +}); + // aliases export const Chain = Community; diff --git a/libs/schemas/src/queries/community.schemas.ts b/libs/schemas/src/queries/community.schemas.ts index d89dd35e9cc..4632c6e385f 100644 --- a/libs/schemas/src/queries/community.schemas.ts +++ b/libs/schemas/src/queries/community.schemas.ts @@ -4,7 +4,12 @@ import { MIN_SCHEMA_INT, } from '@hicommonwealth/shared'; import { z } from 'zod'; -import { Community, CommunityMember, CommunityStake } from '../entities'; +import { + Community, + CommunityMember, + CommunityStake, + ExtendedCommunity, +} from '../entities'; import { PG_INT } from '../utils'; import { PaginatedResultSchema, PaginationParamsSchema } from './pagination'; @@ -36,17 +41,7 @@ export const GetCommunity = { id: z.string(), include_node_info: z.boolean().optional(), }), - output: Community.extend({ - numVotingThreads: PG_INT, - numTotalThreads: PG_INT, - adminsAndMods: z.array( - z.object({ - address: z.string(), - role: z.enum(['admin', 'moderator']), - }), - ), - communityBanner: z.string().optional(), - }).optional(), + output: ExtendedCommunity, }; export const GetCommunityStake = { @@ -106,6 +101,7 @@ export const GetStakeTransaction = { icon_url: z.string().nullish(), name: z.string(), chain_node_id: PG_INT.nullish(), + chain_node_name: z.string().nullish(), }), }) .array(), diff --git a/libs/shared/src/types/protocol.ts b/libs/shared/src/types/protocol.ts index f48e1f52cc7..58727ecc12c 100644 --- a/libs/shared/src/types/protocol.ts +++ b/libs/shared/src/types/protocol.ts @@ -127,51 +127,26 @@ export enum CommunityCategoryType { DAO = 'DAO', } -// TODO: remove many of these chain networks, esp substrate (make them all "Substrate"), -// and just use id to identify specific chains for conditionals +// This enum represents known values for the "network" field on Community, which can be used for +// switched functionality against specific groups of communities (so we could e.g. group together all +// communities on a specific testnet, or all ERC20s). In practice this field is deprecated, and should be +// removed, but these following values remain as either defaults or for custom integration support. export enum ChainNetwork { + Ethereum = 'ethereum', + ERC20 = 'erc20', + ERC721 = 'erc721', + ERC1155 = 'erc1155', Edgeware = 'edgeware', - EdgewareTestnet = 'edgeware-testnet', - Kusama = 'kusama', - Kulupu = 'kulupu', - Polkadot = 'polkadot', - Plasm = 'plasm', - Stafi = 'stafi', - Darwinia = 'darwinia', - Phala = 'phala', - Centrifuge = 'centrifuge', - Straightedge = 'straightedge', Osmosis = 'osmosis', Injective = 'injective', - InjectiveTestnet = 'injective-testnet', + Solana = 'solana', Terra = 'terra', - Ethereum = 'ethereum', NEAR = 'near', - NEARTestnet = 'near-testnet', + Stargaze = 'stargaze', Compound = 'compound', - Aave = 'aave', - AaveLocal = 'aave-local', - dYdX = 'dydx', - Metacartel = 'metacartel', - ALEX = 'alex', - ERC20 = 'erc20', - ERC721 = 'erc721', - ERC1155 = 'erc1155', - CW20 = 'cw20', - CW721 = 'cw721', - Clover = 'clover', - HydraDX = 'hydradx', - Crust = 'crust', - Sputnik = 'sputnik', - SolanaDevnet = 'solana-devnet', - SolanaTestnet = 'solana-testnet', - Solana = 'solana', - SPL = 'spl', // solana token Evmos = 'evmos', Kava = 'kava', Kyve = 'kyve', - Stargaze = 'stargaze', - Cosmos = 'cosmos', } /** diff --git a/packages/commonwealth/client/scripts/controllers/app/login.ts b/packages/commonwealth/client/scripts/controllers/app/login.ts index 329ee209a5b..d02c0d51636 100644 --- a/packages/commonwealth/client/scripts/controllers/app/login.ts +++ b/packages/commonwealth/client/scripts/controllers/app/login.ts @@ -21,8 +21,10 @@ import { CosmosExtension } from '@magic-ext/cosmos'; import { OAuthExtension } from '@magic-ext/oauth'; import { Magic } from 'magic-sdk'; +import { ExtendedCommunity } from '@hicommonwealth/schemas'; import axios from 'axios'; import app from 'state'; +import { EXCEPTION_CASE_VANILLA_getCommunityById } from 'state/api/communities/getCommuityById'; import { SERVER_URL } from 'state/api/config'; import { fetchProfilesByAddress } from 'state/api/profiles/fetchProfilesByAddress'; import { @@ -33,10 +35,11 @@ import { import { authModal } from 'state/ui/modals/authModal'; import { welcomeOnboardModal } from 'state/ui/modals/welcomeOnboardModal'; import { userStore } from 'state/ui/user'; +import { z } from 'zod'; import Account from '../../models/Account'; import AddressInfo from '../../models/AddressInfo'; import type BlockInfo from '../../models/BlockInfo'; -import type ChainInfo from '../../models/ChainInfo'; +import ChainInfo from '../../models/ChainInfo'; function storeActiveAccount(account: Account) { const user = userStore.getState(); @@ -112,14 +115,13 @@ export async function completeClientLogin(account: Account) { } } -export async function updateActiveAddresses({ chain }: { chain?: ChainInfo }) { +export async function updateActiveAddresses(chainId: string) { // update addresses for a chain (if provided) or for communities (if null) // for communities, addresses on all chains are available by default userStore.getState().setData({ accounts: userStore .getState() - .addresses // @ts-expect-error StrictNullChecks - .filter((a) => a.community.id === chain.id) + .addresses.filter((a) => a.community.id === chainId) .map((addr) => { const tempAddr = app.chain?.accounts.get(addr.address, false); tempAddr.profile = addr.profile; @@ -131,8 +133,7 @@ export async function updateActiveAddresses({ chain }: { chain?: ChainInfo }) { // select the address that the new chain should be initialized with const memberAddresses = userStore.getState().accounts.filter((account) => { - // @ts-expect-error StrictNullChecks - return account.community.id === chain.id; + return account.community.id === chainId; }); if (memberAddresses.length === 1) { @@ -145,7 +146,7 @@ export async function updateActiveAddresses({ chain }: { chain?: ChainInfo }) { const communityAddressesSortedByLastUsed = [ ...(userStore .getState() - .addresses.filter((a) => a.community.id === chain?.id) || []), + .addresses.filter((a) => a.community.id === chainId) || []), ].sort((a, b) => { if (b.lastActive && a.lastActive) return b.lastActive.diff(a.lastActive); if (!b.lastActive && !a.lastActive) return 0; // no change @@ -285,7 +286,15 @@ export async function createUserWithAddress( }); const id = response.data.result.id; - const chainInfo = app.config.chains.getById(chain); + + const communityInfo = await EXCEPTION_CASE_VANILLA_getCommunityById( + chain || '', + true, + ); + const chainInfo = ChainInfo.fromTRPCResponse( + communityInfo as z.infer, + ); + const account = new Account({ addressId: id, address, @@ -359,7 +368,7 @@ export async function startLoginWithMagicLink({ redirectTo ? encodeURIComponent(redirectTo) : '' }&chain=${chain || ''}&sso=${provider}`; await magic.oauth.loginWithRedirect({ - provider: provider as any, + provider, redirectURI: new URL( '/finishsociallogin' + params, window.location.origin, @@ -418,8 +427,16 @@ export async function handleSocialLoginCallback({ }): Promise<{ address: string; isAddressNew: boolean }> { // desiredChain may be empty if social login was initialized from // a page without a chain, in which case we default to an eth login - // @ts-expect-error StrictNullChecks - const desiredChain = app.chain?.meta || app.config.chains.getById(chain); + let desiredChain = app.chain?.meta; + if (!desiredChain && chain) { + const communityInfo = await EXCEPTION_CASE_VANILLA_getCommunityById( + chain || '', + true, + ); + desiredChain = ChainInfo.fromTRPCResponse( + communityInfo as z.infer, + ); + } const isCosmos = desiredChain?.base === ChainBase.CosmosSDK; const magic = await constructMagic(isCosmos, desiredChain?.id); const isEmail = walletSsoSource === WalletSsoSource.Email; @@ -554,10 +571,19 @@ export async function handleSocialLoginCallback({ // This is code from before desiredChain was implemented, and // may not be necessary anymore: if (app.chain) { - const c = - userStore.getState().activeCommunity || - app.config.chains.getById(app.activeChainId()); - await updateActiveAddresses({ chain: c }); + let chainInfo = userStore.getState().activeCommunity; + + if (!chainInfo && chain) { + const communityInfo = await EXCEPTION_CASE_VANILLA_getCommunityById( + chain || '', + true, + ); + chainInfo = ChainInfo.fromTRPCResponse( + communityInfo as z.infer, + ); + } + + chainInfo && (await updateActiveAddresses(chainInfo.id)); } const { Profiles: profiles, email: ssoEmail } = response.data.result; diff --git a/packages/commonwealth/client/scripts/controllers/app/webWallets/coinbase_web_wallet.ts b/packages/commonwealth/client/scripts/controllers/app/webWallets/coinbase_web_wallet.ts index d3e0684872c..a00e8e8e321 100644 --- a/packages/commonwealth/client/scripts/controllers/app/webWallets/coinbase_web_wallet.ts +++ b/packages/commonwealth/client/scripts/controllers/app/webWallets/coinbase_web_wallet.ts @@ -132,9 +132,9 @@ class CoinbaseWebWalletController implements IWebWallet { } catch (switchError) { // This error code indicates that the chain has not been added to coinbase. if (switchError.code === 4902) { - const wsRpcUrl = new URL(app.chain.meta.node.url); + const wsRpcUrl = new URL(app.chain?.meta?.node?.url); const rpcUrl = - app.chain.meta.node.altWalletUrl || `https://${wsRpcUrl.host}`; + app.chain?.meta?.node?.altWalletUrl || `https://${wsRpcUrl.host}`; const chains = await axios.get('https://chainid.network/chains.json'); const baseChain = chains.data.find((c) => c.chainId === chainId); diff --git a/packages/commonwealth/client/scripts/controllers/app/webWallets/walletconnect_web_wallet.ts b/packages/commonwealth/client/scripts/controllers/app/webWallets/walletconnect_web_wallet.ts index d24901222d7..c8e53dc9d98 100644 --- a/packages/commonwealth/client/scripts/controllers/app/webWallets/walletconnect_web_wallet.ts +++ b/packages/commonwealth/client/scripts/controllers/app/webWallets/walletconnect_web_wallet.ts @@ -4,8 +4,11 @@ import app from 'state'; import type Web3 from 'web3'; import { SIWESigner } from '@canvas-js/chain-ethereum'; +import { ExtendedCommunity } from '@hicommonwealth/schemas'; +import { EXCEPTION_CASE_VANILLA_getCommunityById } from 'state/api/communities/getCommuityById'; import { userStore } from 'state/ui/user'; import { hexToNumber } from 'web3-utils'; +import { z } from 'zod'; import BlockInfo from '../../../models/BlockInfo'; import ChainInfo from '../../../models/ChainInfo'; import IWebWallet from '../../../models/IWebWallet'; @@ -97,10 +100,21 @@ class WalletConnectWebWalletController implements IWebWallet { public async enable() { console.log('Attempting to enable WalletConnect'); this._enabling = true; - // try { - // Create WalletConnect Provider - this._chainInfo = - app.chain?.meta || app.config.chains.getById(this.defaultNetwork); + this._chainInfo = app?.chain?.meta; + + if (!this._chainInfo && app.activeChainId()) { + const communityInfo = await EXCEPTION_CASE_VANILLA_getCommunityById( + app.activeChainId() || '', + true, + ); + + const chainInfo = ChainInfo.fromTRPCResponse( + communityInfo as z.infer, + ); + + this._chainInfo = chainInfo; + } + const chainId = this._chainInfo.node?.ethChainId || 1; const EthereumProvider = (await import('@walletconnect/ethereum-provider')) .default; diff --git a/packages/commonwealth/client/scripts/helpers/chain.ts b/packages/commonwealth/client/scripts/helpers/chain.ts index 292cdc42535..1f59efb65c4 100644 --- a/packages/commonwealth/client/scripts/helpers/chain.ts +++ b/packages/commonwealth/client/scripts/helpers/chain.ts @@ -1,8 +1,11 @@ +import { ExtendedCommunity } from '@hicommonwealth/schemas'; import { ChainBase } from '@hicommonwealth/shared'; import { updateActiveAddresses } from 'controllers/app/login'; import { DEFAULT_CHAIN } from 'helpers/constants'; import app, { ApiStatus } from 'state'; +import { z } from 'zod'; import ChainInfo from '../models/ChainInfo'; +import { EXCEPTION_CASE_VANILLA_getCommunityById } from '../state/api/communities/getCommuityById'; import { userStore } from '../state/ui/user'; export const deinitChainOrCommunity = async () => { @@ -39,7 +42,13 @@ export const loadCommunityChainInfo = async ( if (activeCommunity) { tempChain = activeCommunity; } else { - tempChain = app.config.chains.getById(DEFAULT_CHAIN); + const communityInfo = await EXCEPTION_CASE_VANILLA_getCommunityById( + DEFAULT_CHAIN, + true, + ); + tempChain = ChainInfo.fromTRPCResponse( + communityInfo as z.infer, + ); } if (!tempChain) { @@ -112,7 +121,7 @@ export const loadCommunityChainInfo = async ( app.chainPreloading = false; // Instantiate active addresses before chain fully loads - await updateActiveAddresses({ chain }); + await updateActiveAddresses(chain?.id || ''); return true; }; @@ -120,7 +129,7 @@ export const loadCommunityChainInfo = async ( // Initializes a selected chain. Requires `app.chain` to be defined and valid // and not already initialized. export const initChain = async (): Promise => { - if (!app.chain || !app.chain.meta || app.chain.loaded) { + if (!app.chain || !app.chain?.meta || app.chain.loaded) { return; } @@ -140,5 +149,5 @@ export const initChain = async (): Promise => { console.log(`${chain.network.toUpperCase()} started.`); // Instantiate (again) to create chain-specific Account<> objects - await updateActiveAddresses({ chain }); + await updateActiveAddresses(chain.id || ''); }; diff --git a/packages/commonwealth/client/scripts/helpers/index.tsx b/packages/commonwealth/client/scripts/helpers/index.tsx index 1b90c733ff4..3385df9cda2 100644 --- a/packages/commonwealth/client/scripts/helpers/index.tsx +++ b/packages/commonwealth/client/scripts/helpers/index.tsx @@ -31,7 +31,8 @@ export function threadStageToLabel(stage: string) { export function isDefaultStage(stage: string, customStages?: string[]) { return ( stage === ThreadStage.Discussion || - stage === parseCustomStages(customStages || app.chain.meta.customStages)[0] + stage === + parseCustomStages(customStages || app?.chain?.meta?.customStages)[0] ); } diff --git a/packages/commonwealth/client/scripts/helpers/link.ts b/packages/commonwealth/client/scripts/helpers/link.ts new file mode 100644 index 00000000000..6ab26c7c2b5 --- /dev/null +++ b/packages/commonwealth/client/scripts/helpers/link.ts @@ -0,0 +1,76 @@ +export const isLinkValid = (link = '') => + /^https?:\/\/[^\s/$.?#].[^\s]*$/.test(link); + +export type LinkType = + | 'discord' + | 'slack' + | 'telegram' + | 'x (twitter)' + | 'github' + | 'matrix' + | ''; + +export const getLinkType = (link: string): LinkType => { + if (!isLinkValid(link)) return ''; + if (link.includes('discord.com') || link.includes('discord.gg')) + return 'discord'; + if (link.includes('slack.com')) return 'slack'; + if (link.includes('telegram.com')) return 'telegram'; + if (link.includes('t.me')) return 'telegram'; + if (link.includes('twitter.com')) return 'x (twitter)'; + if (link.includes('github.com')) return 'github'; + if (link.includes('matrix.to')) return 'matrix'; + return ''; +}; + +export type CategorizedSocialLinks = { + discords: string[]; + githubs: string[]; + telegrams: string[]; + twitters: string[]; + elements: string[]; + slacks: string[]; + remainingLinks: string[]; +}; + +export const categorizeSocialLinks = ( + links: string[] = [], +): CategorizedSocialLinks => { + const categorized: CategorizedSocialLinks = { + discords: [], + githubs: [], + telegrams: [], + twitters: [], + elements: [], + slacks: [], + remainingLinks: [], + }; + + links.forEach((link) => { + const linkType = getLinkType(link); + switch (linkType) { + case 'discord': + categorized.discords.push(link); + break; + case 'github': + categorized.githubs.push(link); + break; + case 'slack': + categorized.slacks.push(link); + break; + case 'telegram': + categorized.telegrams.push(link); + break; + case 'x (twitter)': + categorized.twitters.push(link); + break; + case 'matrix': + categorized.elements.push(link); + break; + default: + categorized.remainingLinks.push(link); + } + }); + + return categorized; +}; diff --git a/packages/commonwealth/client/scripts/helpers/linkType.ts b/packages/commonwealth/client/scripts/helpers/linkType.ts deleted file mode 100644 index 669df646ef5..00000000000 --- a/packages/commonwealth/client/scripts/helpers/linkType.ts +++ /dev/null @@ -1,14 +0,0 @@ -const getLinkType = (link: string, defaultValueWhenValidLink = '') => { - if (link.includes('discord.com') || link.includes('discord.gg')) - return 'discord'; - if (link.includes('slack.com')) return 'slack'; - if (link.includes('telegram.com')) return 'telegram'; - if (link.includes('t.me')) return 'telegram'; - if (link.includes('twitter.com')) return 'x (twitter)'; - if (link.includes('github.com')) return 'github'; - if (/^https?:\/\/[^\s/$.?#].[^\s]*$/.test(link)) - return defaultValueWhenValidLink; - return ''; -}; - -export default getLinkType; diff --git a/packages/commonwealth/client/scripts/hooks/useCommunityCardPrice.ts b/packages/commonwealth/client/scripts/hooks/useCommunityCardPrice.ts index 99cca18cc30..ba2ea7b2e64 100644 --- a/packages/commonwealth/client/scripts/hooks/useCommunityCardPrice.ts +++ b/packages/commonwealth/client/scripts/hooks/useCommunityCardPrice.ts @@ -1,5 +1,4 @@ -import ChainInfo from '../models/ChainInfo'; -import StakeInfo from '../models/StakeInfo'; +import { useGetCommunityByIdQuery } from '../state/api/communities'; import { useGetBuyPriceQuery } from '../state/api/communityStake/index'; import { convertEthToUsd } from '../views/modals/ManageCommunityStakeModal/utils'; @@ -12,32 +11,36 @@ function fromWei(value: string | null): number | null { } export const useCommunityCardPrice = ({ - community, + communityId, stakeId, ethUsdRate, historicalPrice, }: { - community: ChainInfo; + communityId: string; stakeId: number; ethUsdRate: string; historicalPrice: string; }) => { - // @ts-expect-error StrictNullChecks - const communityStake: StakeInfo = community?.CommunityStakes?.find( - (s) => s.stakeId === stakeId, + const { data: community, isLoading: isCommunityLoading } = + useGetCommunityByIdQuery({ + id: communityId, + enabled: !!communityId, + includeNodeInfo: true, + }); + + const communityStake = (community?.CommunityStakes || [])?.find( + (s) => s.stake_id === stakeId, ); - const stakeEnabled = !!communityStake?.stakeEnabled; + const stakeEnabled = !!communityStake?.stake_enabled; // The price of buying one stake const { data: buyPriceData, isLoading } = useGetBuyPriceQuery({ - // @ts-expect-error StrictNullChecks - namespace: community.namespace, + namespace: community?.namespace || '', stakeId: stakeId, amount: 1, - apiEnabled: stakeEnabled, - chainRpc: community.ChainNode?.url, - // @ts-expect-error StrictNullChecks - ethChainId: community.ChainNode?.ethChainId, + apiEnabled: stakeEnabled && !isCommunityLoading, + chainRpc: community?.ChainNode?.url || '', + ethChainId: community?.ChainNode?.eth_chain_id || 0, }); if (!stakeEnabled || isLoading) { diff --git a/packages/commonwealth/client/scripts/hooks/useManageDocumentTitle.ts b/packages/commonwealth/client/scripts/hooks/useManageDocumentTitle.ts index ce6959c1d82..9d8f9beb842 100644 --- a/packages/commonwealth/client/scripts/hooks/useManageDocumentTitle.ts +++ b/packages/commonwealth/client/scripts/hooks/useManageDocumentTitle.ts @@ -4,7 +4,9 @@ import app from '../state'; const useManageDocumentTitle = (title: string, details?: string) => { useEffect(() => { const displayTitle = details || title; - document.title = `${app.chain.meta.name} – ${displayTitle}`; + document.title = app?.chain?.meta?.name + ? `${app?.chain?.meta?.name} - ${displayTitle}` + : displayTitle; }, [details, title]); }; diff --git a/packages/commonwealth/client/scripts/hooks/useRunOnceOnCondition.ts b/packages/commonwealth/client/scripts/hooks/useRunOnceOnCondition.ts new file mode 100644 index 00000000000..ab1e5aa78f3 --- /dev/null +++ b/packages/commonwealth/client/scripts/hooks/useRunOnceOnCondition.ts @@ -0,0 +1,25 @@ +import { useRef } from 'react'; +import useNecessaryEffect from './useNecessaryEffect'; + +type UseRunOnceOnConditionProps = { + callback: () => void | Promise<() => void>; + shouldRun: boolean; +}; + +const useRunOnceOnCondition = ({ + callback, + shouldRun, +}: UseRunOnceOnConditionProps) => { + const isRunComplete = useRef(false); + + useNecessaryEffect(() => { + if (shouldRun && !isRunComplete.current) { + callback?.()?.catch?.(console.error); + isRunComplete.current = true; + } + }, [callback, shouldRun]); + + return {}; +}; + +export default useRunOnceOnCondition; diff --git a/packages/commonwealth/client/scripts/hooks/useTransactionHistory.ts b/packages/commonwealth/client/scripts/hooks/useTransactionHistory.ts index 415449fdbf8..3e55628f181 100644 --- a/packages/commonwealth/client/scripts/hooks/useTransactionHistory.ts +++ b/packages/commonwealth/client/scripts/hooks/useTransactionHistory.ts @@ -39,7 +39,6 @@ const useTransactionHistory = ({ // @ts-expect-error StrictNullChecks t.community?.chain_node_id, ), - chain: '', })); // filter by community name and symbol diff --git a/packages/commonwealth/client/scripts/models/AddressInfo.ts b/packages/commonwealth/client/scripts/models/AddressInfo.ts index bcf1c088260..644eb6ede66 100644 --- a/packages/commonwealth/client/scripts/models/AddressInfo.ts +++ b/packages/commonwealth/client/scripts/models/AddressInfo.ts @@ -25,7 +25,8 @@ class AddressInfo extends Account { ghostAddress?: boolean; lastActive?: string | moment.Moment; }) { - const chain = app.config.chains.getById(communityId); + // TODO: cleanup this with #2617 + const chain = app.config.chains.getAll().find((c) => c.id === communityId); if (!chain) throw new Error(`Failed to locate chain: ${communityId}`); super({ address, diff --git a/packages/commonwealth/client/scripts/models/ChainInfo.ts b/packages/commonwealth/client/scripts/models/ChainInfo.ts index d1d842e4023..c0fee378abb 100644 --- a/packages/commonwealth/client/scripts/models/ChainInfo.ts +++ b/packages/commonwealth/client/scripts/models/ChainInfo.ts @@ -1,15 +1,9 @@ -import type { - AddressRole, - ChainNetwork, - DefaultPage, -} from '@hicommonwealth/shared'; +import { ExtendedCommunity } from '@hicommonwealth/schemas'; +import type { AddressRole, DefaultPage } from '@hicommonwealth/shared'; import { ChainBase } from '@hicommonwealth/shared'; -import axios from 'axios'; -import app from 'state'; -import { SERVER_URL } from 'state/api/config'; +import { z } from 'zod'; import { getCosmosChains } from '../controllers/app/webWallets/utils'; -import { userStore } from '../state/ui/user'; -import type NodeInfo from './NodeInfo'; +import NodeInfo from './NodeInfo'; import StakeInfo from './StakeInfo'; import Tag from './Tag'; @@ -24,7 +18,7 @@ class ChainInfo { public readonly profileCount: number; public readonly default_symbol: string; public name: string; - public readonly network: ChainNetwork; + public readonly network: string; public readonly base: ChainBase; public iconUrl: string; public description: string; @@ -53,6 +47,8 @@ class ChainInfo { public directoryPageChainNodeId?: number; public namespace?: string; public redirect?: string; + public numTotalThreads?: number; + public numVotingThreads?: number; public get node() { return this.ChainNode; @@ -96,6 +92,7 @@ class ChainInfo { thread_count, profile_count, snapshot_spaces, + communityBanner, }) { this.id = id; this.network = network; @@ -125,8 +122,7 @@ class ChainInfo { this.CommunityTags = CommunityTags; this.tokenName = tokenName; this.adminOnlyPolling = adminOnlyPolling; - // @ts-expect-error StrictNullChecks - this.communityBanner = null; + this.communityBanner = communityBanner; this.discordConfigId = discord_config_id; this.discordBotWebhooksEnabled = discordBotWebhooksEnabled; this.directoryPageEnabled = directoryPageEnabled; @@ -176,6 +172,8 @@ class ChainInfo { CommunityTags, snapshot_spaces, Addresses, + adminsAndMods, + communityBanner, }) { let blockExplorerIdsParsed; try { @@ -231,157 +229,70 @@ class ChainInfo { thread_count, profile_count, snapshot_spaces, - adminsAndMods: Addresses, + adminsAndMods: adminsAndMods || Addresses, + communityBanner, }); } - public setAdminsAndMods(roles: AddressRole[]) { - this.adminsAndMods = roles; - } - - public setBanner(banner_text: string) { - this.communityBanner = banner_text; - } - - public async updateChainData({ - name, - description, - social_links, - stagesEnabled, - customStages, - customDomain, - terms, - snapshot, - iconUrl, - defaultOverview, - defaultPage, - hasHomepage, - cosmos_gov_version, - chain_node_id, - discord_bot_webhooks_enabled, - directory_page_enabled, - directory_page_chain_node_id, - type, - isPWA, - }: { - name?: string; - description?: string; - social_links?: string[]; - discord?: string; - stagesEnabled?: boolean; - customStages?: string[]; - customDomain?: string; - terms?: string; - snapshot?: string[]; - iconUrl?: string; - defaultOverview?: boolean; - defaultPage?: DefaultPage; - hasHomepage?: boolean; - cosmos_gov_version?: string; - chain_node_id?: string; - discord_bot_webhooks_enabled?: boolean; - directory_page_enabled?: boolean; - directory_page_chain_node_id?: number; - type?: string; - isPWA?: boolean; - }) { - const id = app.activeChainId() ?? this.id; - const r = await axios.patch( - `${SERVER_URL}/communities/${id}`, - { - id, - name, - description, - social_links, - stages_enabled: stagesEnabled, - custom_stages: customStages, - custom_domain: customDomain, - snapshot, - terms, - icon_url: iconUrl, - default_summary_view: defaultOverview, - default_page: defaultPage, - has_homepage: hasHomepage, - chain_node_id, - cosmos_gov_version, - discord_bot_webhooks_enabled, - directory_page_enabled, - directory_page_chain_node_id, - type, - jwt: userStore.getState().jwt, - }, - { - headers: { - isPWA: isPWA?.toString(), - }, - }, - ); - - const updatedChain = r.data.result; - this.name = updatedChain.name; - this.description = updatedChain.description; - this.socialLinks = updatedChain.social_links; - this.stagesEnabled = updatedChain.stages_enabled; - this.customStages = updatedChain.custom_stages; - this.customDomain = updatedChain.custom_domain; - this.snapshot = updatedChain.snapshot; - this.terms = updatedChain.terms; - this.iconUrl = updatedChain.icon_url; - this.defaultOverview = updatedChain.default_summary_view; - this.defaultPage = updatedChain.default_page; - this.hasHomepage = updatedChain.has_homepage; - this.discordBotWebhooksEnabled = updatedChain.discord_bot_webhooks_enabled; - this.directoryPageEnabled = updatedChain.directory_page_enabled; - this.directoryPageChainNodeId = updatedChain.directory_page_chain_node_id; - this.type = updatedChain.type; - } - - public categorizeSocialLinks(): CategorizedSocialLinks { - const categorizedLinks: CategorizedSocialLinks = { - discords: [], - githubs: [], - telegrams: [], - twitters: [], - elements: [], - remainingLinks: [], - }; - - this.socialLinks - .filter((link) => !!link) - .forEach((link) => { - if (link.includes('://discord.com') || link.includes('://discord.gg')) { - categorizedLinks.discords.push(link); - } else if (link.includes('://github.com')) { - categorizedLinks.githubs.push(link); - } else if (link.includes('://t.me')) { - categorizedLinks.telegrams.push(link); - } else if (link.includes('://matrix.to')) { - categorizedLinks.elements.push(link); - } else if ( - link.includes('://twitter.com') || - link.includes('://x.com') - ) { - categorizedLinks.twitters.push(link); - } else { - categorizedLinks.remainingLinks.push(link); - } - }); - - return categorizedLinks; - } - - public updateTags(tags: Tag[]) { - this.CommunityTags = tags; + // TODO: 8811 cleanup `ChainInfo` + public static fromTRPCResponse( + community: z.infer, + ): ChainInfo { + return ChainInfo.fromJSON({ + Addresses: community.Addresses, + admin_only_polling: community.admin_only_polling, + base: community.base, + bech32_prefix: community.bech32_prefix, + block_explorer_ids: community.block_explorer_ids, + chain_node_id: community.chain_node_id, + ChainNode: new NodeInfo({ + alt_wallet_url: community?.ChainNode?.alt_wallet_url, + balance_type: community?.ChainNode?.balance_type, + bech32: community?.ChainNode?.bech32, + block_explorer: community?.ChainNode?.block_explorer, + cosmos_chain_id: community?.ChainNode?.cosmos_chain_id, + cosmos_gov_version: community?.ChainNode?.cosmos_gov_version, + eth_chain_id: community?.ChainNode?.eth_chain_id, + id: community?.ChainNode?.id, + name: community?.ChainNode?.name, + slip44: community?.ChainNode?.slip44, + url: community?.ChainNode?.url, + }), + collapsed_on_homepage: community.collapsed_on_homepage, + CommunityStakes: community.CommunityStakes, + CommunityTags: community.CommunityTags, + custom_domain: community.custom_domain, + custom_stages: community.custom_stages, + default_page: community.default_page, + default_summary_view: community.default_summary_view, + default_symbol: community.default_symbol, + description: community.description, + directory_page_chain_node_id: community.directory_page_chain_node_id, + directory_page_enabled: community.directory_page_enabled, + discord_bot_webhooks_enabled: community.discord_bot_webhooks_enabled, + token_name: community.token_name, + has_homepage: community.has_homepage, + discord_config_id: community.discord_config_id, + icon_url: community.icon_url, + name: community.name, + id: community.id, + redirect: community.redirect, + namespace: community.namespace, + network: community.network, + snapshot_spaces: community.snapshot_spaces, + stages_enabled: community.stages_enabled, + terms: community.terms, + thread_count: community.numTotalThreads, + social_links: community.social_links, + ss58_prefix: community.ss58_prefix, + type: community.type, + adminsAndMods: community?.adminsAndMods || [], + communityBanner: community?.communityBanner || '', + // these don't come from /communities/:id response and need to be added in + // api response when needed + Contracts: [], + profile_count: 0, + }); } } - -export type CategorizedSocialLinks = { - discords: string[]; - githubs: string[]; - telegrams: string[]; - twitters: string[]; - elements: string[]; - remainingLinks: string[]; -}; - export default ChainInfo; diff --git a/packages/commonwealth/client/scripts/models/IChainAdapter.ts b/packages/commonwealth/client/scripts/models/IChainAdapter.ts index dde771667d5..bee1c5b0bee 100644 --- a/packages/commonwealth/client/scripts/models/IChainAdapter.ts +++ b/packages/commonwealth/client/scripts/models/IChainAdapter.ts @@ -1,15 +1,11 @@ import type { ChainBase } from '@hicommonwealth/shared'; import type { Coin } from 'adapters/currency'; - -import axios from 'axios'; import moment from 'moment'; import type { IApp } from 'state'; import { ApiStatus } from 'state'; -import { SERVER_URL } from 'state/api/config'; import { clearLocalStorage } from 'stores/PersistentStore'; import { setDarkMode } from '../helpers/darkMode'; import { EXCEPTION_CASE_threadCountersStore } from '../state/ui/thread'; -import { userStore } from '../state/ui/user'; import Account from './Account'; import type ChainInfo from './ChainInfo'; import type { IAccountsModule, IBlockInfo, IChainModule } from './interfaces'; @@ -44,16 +40,8 @@ abstract class IChainAdapter { public async initServer(): Promise { clearLocalStorage(); console.log(`Starting ${this.meta.name}`); - const [response] = await Promise.all([ - axios.get(`${SERVER_URL}/bulkOffchain`, { - params: { - chain: this.id, - community: null, - jwt: userStore.getState().jwt, - }, - }), - ]); + // only on `1inch`, force enable dark mode const darkModePreferenceSet = localStorage.getItem('user-dark-mode-state'); if (this.meta.id === '1inch') { darkModePreferenceSet @@ -61,20 +49,11 @@ abstract class IChainAdapter { : setDarkMode(true); } - const { - adminsAndMods, - numVotingThreads, - numTotalThreads, - communityBanner, - } = response.data.result; - // Update community level thread counters variables (Store in state instead of react query here is an - // exception case, view the threadCountersStore code for more details) + // TODO: cleanup EXCEPTION_CASE_threadCountersStore in 8812 EXCEPTION_CASE_threadCountersStore.setState({ - totalThreadsInCommunity: numTotalThreads, - totalThreadsInCommunityForVoting: numVotingThreads, + totalThreadsInCommunity: this.meta.numTotalThreads, + totalThreadsInCommunityForVoting: this.meta.numVotingThreads, }); - this.meta.setAdminsAndMods(adminsAndMods); - this.meta.setBanner(communityBanner); this._serverLoaded = true; return true; diff --git a/packages/commonwealth/client/scripts/state/api/communities/editCommunityBanner.ts b/packages/commonwealth/client/scripts/state/api/communities/editCommunityBanner.ts index 529361cb56f..9cb0bf1a3ca 100644 --- a/packages/commonwealth/client/scripts/state/api/communities/editCommunityBanner.ts +++ b/packages/commonwealth/client/scripts/state/api/communities/editCommunityBanner.ts @@ -26,9 +26,7 @@ const editCommunityBanner = async ({ const useEditCommunityBannerMutation = () => { return useMutation({ mutationFn: editCommunityBanner, - onSuccess: async (communityBanner) => { - app.chain.meta.setBanner(communityBanner.banner_text); - + onSuccess: () => { const communityBannerKey = `${app.activeChainId()}-banner`; if (localStorage.getItem(communityBannerKey) === 'off') { localStorage.setItem(communityBannerKey, 'on'); diff --git a/packages/commonwealth/client/scripts/state/api/communities/editCommunityTags.ts b/packages/commonwealth/client/scripts/state/api/communities/editCommunityTags.ts index 0e35acc7ca0..6725333fa96 100644 --- a/packages/commonwealth/client/scripts/state/api/communities/editCommunityTags.ts +++ b/packages/commonwealth/client/scripts/state/api/communities/editCommunityTags.ts @@ -1,10 +1,10 @@ import { useMutation } from '@tanstack/react-query'; import axios from 'axios'; -import app from 'state'; import { SERVER_URL } from 'state/api/config'; import { userStore } from '../../ui/user'; import { ApiEndpoints, queryClient } from '../config'; import { FetchActiveCommunitiesResponse } from './fetchActiveCommunities'; +import { invalidateAllQueriesForCommunity } from './getCommuityById'; interface EditCommunityTagsProps { communityId: string; @@ -28,9 +28,8 @@ const editCommunityTags = async ({ const useEditCommunityTagsMutation = () => { return useMutation({ mutationFn: editCommunityTags, - onSuccess: ({ CommunityTags = [], community_id }) => { - const community = app.config.chains.getById(community_id); - if (community) community.updateTags(CommunityTags); + onSuccess: async ({ CommunityTags = [], community_id }) => { + await invalidateAllQueriesForCommunity(community_id); // update active communities cache const key = [ApiEndpoints.FETCH_ACTIVE_COMMUNITIES]; diff --git a/packages/commonwealth/client/scripts/state/api/communities/getCommuityById.ts b/packages/commonwealth/client/scripts/state/api/communities/getCommuityById.ts new file mode 100644 index 00000000000..97ad8b8f36d --- /dev/null +++ b/packages/commonwealth/client/scripts/state/api/communities/getCommuityById.ts @@ -0,0 +1,92 @@ +import { ExtendedCommunity } from '@hicommonwealth/schemas'; +import axios from 'axios'; +import { trpc } from 'utils/trpcClient'; +import { z } from 'zod'; +import { queryClient, SERVER_URL } from '../config'; + +const COMMUNITIY_STALE_TIME = 60 * 3_000; // 3 mins + +type UseGetCommunityByIdProps = { + id: string; + includeNodeInfo?: boolean; + enabled?: boolean; +}; + +export const invalidateAllQueriesForCommunity = async (communityId: string) => { + // get all the query keys for this community + const queryKeys = [ + trpc.community.getCommunity.getQueryKey({ + id: communityId, + include_node_info: true, + }), + trpc.community.getCommunity.getQueryKey({ + id: communityId, + include_node_info: false, + }), + ]; + + // invalidate all the query keys for this community + if (queryKeys.length > 0) { + await Promise.all( + queryKeys.map(async (key) => { + const params = { queryKey: key }; + await queryClient.cancelQueries(params); + await queryClient.invalidateQueries(params); + }), + ); + } +}; + +export const EXCEPTION_CASE_VANILLA_getCommunityById = async ( + communityId: string, + includeNodeInfo = false, +): Promise | undefined> => { + // make trpc query key for this request + const queryKey = trpc.community.getCommunity.getQueryKey({ + id: communityId, + include_node_info: includeNodeInfo, + }); + + // if community already exists in cache, return that + const cachedCommunity = queryClient.getQueryData< + z.infer | undefined + >(queryKey); + if (cachedCommunity) { + return cachedCommunity; + } + + // HACK: with @trpc/react-query v10.x, we can't directly call an endpoint outside of 'react-context' + // with this way the api can be used in non-react files. This should be cleaned up when we migrate + // to @trpc/react-query v11.x + const response = await axios.get( + // eslint-disable-next-line max-len + `${SERVER_URL}/v1/community.getCommunity?batch=1&input=%7B%220%22%3A%7B%22id%22%3A%22${communityId}%22%2C%22include_node_info%22%3A${includeNodeInfo}%7D%7D`, + ); + const fetchedCommunity = response?.data[0]?.result?.data as z.infer< + typeof ExtendedCommunity + >; + + // add response in cache + queryClient.setQueryData(queryKey, fetchedCommunity); + + return fetchedCommunity; +}; + +const useGetCommunityByIdQuery = ({ + id, + includeNodeInfo = false, + enabled, +}: UseGetCommunityByIdProps) => { + return trpc.community.getCommunity.useQuery( + { + id, + include_node_info: includeNodeInfo, + }, + { + staleTime: COMMUNITIY_STALE_TIME, + enabled, + }, + ); +}; + +export default useGetCommunityByIdQuery; diff --git a/packages/commonwealth/client/scripts/state/api/communities/index.ts b/packages/commonwealth/client/scripts/state/api/communities/index.ts index 8a1325f01d6..1addfdcb79f 100644 --- a/packages/commonwealth/client/scripts/state/api/communities/index.ts +++ b/packages/commonwealth/client/scripts/state/api/communities/index.ts @@ -1,6 +1,7 @@ import useEditCommunityBannerMutation from './editCommunityBanner'; import useEditCommunityTagsMutation from './editCommunityTags'; import useFetchRelatedCommunitiesQuery from './fetchRelatedCommunities'; +import useGetCommunityByIdQuery from './getCommuityById'; import useToggleCommunityStarMutation from './toggleCommunityStar'; import useUpdateCommunityMutation from './updateCommunity'; @@ -8,6 +9,7 @@ export { useEditCommunityBannerMutation, useEditCommunityTagsMutation, useFetchRelatedCommunitiesQuery, + useGetCommunityByIdQuery, useToggleCommunityStarMutation, useUpdateCommunityMutation, }; diff --git a/packages/commonwealth/client/scripts/state/api/communities/updateCommunity.ts b/packages/commonwealth/client/scripts/state/api/communities/updateCommunity.ts index 89c3725e756..e8675fdefd0 100644 --- a/packages/commonwealth/client/scripts/state/api/communities/updateCommunity.ts +++ b/packages/commonwealth/client/scripts/state/api/communities/updateCommunity.ts @@ -1,14 +1,32 @@ +import { ChainType } from '@hicommonwealth/shared'; import { useMutation } from '@tanstack/react-query'; import axios from 'axios'; import { initAppState } from 'state'; import { SERVER_URL } from 'state/api/config'; import { userStore } from '../../ui/user'; +import { invalidateAllQueriesForCommunity } from './getCommuityById'; interface UpdateCommunityProps { communityId: string; - namespace: string; - symbol: string; - transactionHash: string; + namespace?: string; + symbol?: string; + transactionHash?: string; + directoryPageEnabled?: boolean; + directoryPageChainNodeId?: number; + discordBotWebhooksEnabled?: boolean; + snapshot?: string[]; + terms?: string; + name?: string; + description?: string; + socialLinks?: string[]; + stagesEnabled?: boolean; + customStages?: string[]; + customDomain?: string; + iconUrl?: string; + defaultOverview?: boolean; + chainNodeId?: string; + type?: ChainType; + isPWA?: boolean; } const updateCommunity = async ({ @@ -16,21 +34,116 @@ const updateCommunity = async ({ namespace, symbol, transactionHash, + directoryPageEnabled, + directoryPageChainNodeId, + discordBotWebhooksEnabled, + snapshot, + terms, + name, + description, + socialLinks, + stagesEnabled, + customStages, + customDomain, + iconUrl, + defaultOverview, + chainNodeId, + type, + isPWA, }: UpdateCommunityProps) => { - return await axios.patch(`${SERVER_URL}/communities/${communityId}`, { - jwt: userStore.getState().jwt, - id: communityId, - namespace, - default_symbol: symbol, - transactionHash, - }); + return await axios.patch( + `${SERVER_URL}/communities/${communityId}`, + { + jwt: userStore.getState().jwt, + id: communityId, + ...(namespace && { + namespace, + }), + ...(typeof symbol !== 'undefined' && { + default_symbol: symbol, + }), + ...(typeof transactionHash !== 'undefined' && { + transactionHash, + }), + ...(typeof directoryPageEnabled === 'boolean' && { + directory_page_enabled: directoryPageEnabled, + }), + ...(directoryPageChainNodeId && { + directory_page_chain_node_id: directoryPageChainNodeId, + }), + ...(typeof snapshot !== 'undefined' && { + snapshot, + }), + ...(typeof snapshot !== 'undefined' && { + snapshot, + }), + ...(typeof terms !== 'undefined' && { + terms, + }), + ...(typeof discordBotWebhooksEnabled === 'boolean' && { + discord_bot_webhooks_enabled: discordBotWebhooksEnabled, + }), + ...(typeof name !== 'undefined' && { + name, + }), + ...(typeof name !== 'undefined' && { + name, + }), + ...(typeof description !== 'undefined' && { + description, + }), + ...(typeof socialLinks !== 'undefined' && { + social_links: socialLinks, + }), + ...(typeof stagesEnabled !== 'undefined' && { + stages_enabled: stagesEnabled, + }), + ...(typeof customStages !== 'undefined' && { + custom_stages: customStages, + }), + ...(typeof customDomain !== 'undefined' && { + custom_domain: customDomain, + }), + ...(typeof iconUrl !== 'undefined' && { + icon_url: iconUrl, + }), + ...(typeof defaultOverview !== 'undefined' && { + default_summary_view: defaultOverview, + }), + ...(typeof chainNodeId !== 'undefined' && { + chain_node_id: chainNodeId, + }), + ...(typeof type !== 'undefined' && { + type: type, + }), + }, + { + headers: { + isPWA: isPWA?.toString(), + }, + }, + ); +}; + +type UseUpdateCommunityMutationProps = { + communityId: string; + reInitAppOnSuccess?: boolean; }; -const useUpdateCommunityMutation = () => { +const useUpdateCommunityMutation = ({ + communityId, + reInitAppOnSuccess, +}: UseUpdateCommunityMutationProps) => { return useMutation({ mutationFn: updateCommunity, onSuccess: async () => { - await initAppState(false); + // since this is the main chain/community object affecting + // some other features, better to re-fetch on update. + await invalidateAllQueriesForCommunity(communityId); + + if (reInitAppOnSuccess) { + await initAppState(false); + } }, }); }; diff --git a/packages/commonwealth/client/scripts/state/api/user/updateActiveCommunity.ts b/packages/commonwealth/client/scripts/state/api/user/updateActiveCommunity.ts index 2aad198bd4c..3784ec921e9 100644 --- a/packages/commonwealth/client/scripts/state/api/user/updateActiveCommunity.ts +++ b/packages/commonwealth/client/scripts/state/api/user/updateActiveCommunity.ts @@ -1,6 +1,5 @@ import { useMutation } from '@tanstack/react-query'; import axios from 'axios'; -import ChainInfo from 'models/ChainInfo'; import app from 'state'; import { SERVER_URL } from 'state/api/config'; import useUserStore, { userStore } from '../../ui/user'; @@ -13,8 +12,6 @@ type UpdateActiveCommunityProps = { export const updateActiveCommunity = async ({ communityId, // assuming this is a valid community id }: UpdateActiveCommunityProps) => { - const community = app.config.chains.getById(communityId); - const res = await axios.post( `${SERVER_URL}/${ApiEndpoints.UPDATE_USER_ACTIVE_COMMUNTY}`, { @@ -26,7 +23,7 @@ export const updateActiveCommunity = async ({ if (res.data.status !== 'Success') throw new Error(res.data.status); - return community; + return communityId; }; const useUpdateUserActiveCommunityMutation = () => { @@ -34,10 +31,18 @@ const useUpdateUserActiveCommunityMutation = () => { return useMutation({ mutationFn: updateActiveCommunity, - onSuccess: (community: ChainInfo) => - user.setData({ - activeCommunity: community, - }), + onSuccess: (communityId: string) => { + // TODO: cleanup this with #2617 + const foundChain = app.config.chains + .getAll() + .find((c) => c.id == communityId); + + if (foundChain) { + user.setData({ + activeCommunity: foundChain, + }); + } + }, onError: (error) => console.error(`Failed to update active community: ${error}`), }); diff --git a/packages/commonwealth/client/scripts/state/ui/modals/manageCommunityStakeModal.ts b/packages/commonwealth/client/scripts/state/ui/modals/manageCommunityStakeModal.ts index 7756a9a6546..4ba5e23ddfe 100644 --- a/packages/commonwealth/client/scripts/state/ui/modals/manageCommunityStakeModal.ts +++ b/packages/commonwealth/client/scripts/state/ui/modals/manageCommunityStakeModal.ts @@ -1,14 +1,23 @@ -import ChainInfo from 'models/ChainInfo'; +import { ChainBase } from '@hicommonwealth/shared'; import { createBoundedUseStore } from 'state/ui/utils'; import { ManageCommunityStakeModalMode } from 'views/modals/ManageCommunityStakeModal/types'; import { devtools } from 'zustand/middleware'; import { createStore } from 'zustand/vanilla'; +type SelectedCommunity = { + id: string; + base: ChainBase; + namespace: string; + ChainNode: { + url: string; + ethChainId: number; + }; +}; interface ManageCommunityStakeModalStore { selectedAddress?: Readonly; setSelectedAddress: (address: string) => void; - selectedCommunity?: Readonly; - setSelectedCommunity: (community: ChainInfo) => void; + selectedCommunity?: Readonly; + setSelectedCommunity: (community: SelectedCommunity) => void; modeOfManageCommunityStakeModal: ManageCommunityStakeModalMode; setModeOfManageCommunityStakeModal: ( modalType: ManageCommunityStakeModalMode, diff --git a/packages/commonwealth/client/scripts/utils/Permissions.ts b/packages/commonwealth/client/scripts/utils/Permissions.ts index 01c8bf45d8a..433eaecf74e 100644 --- a/packages/commonwealth/client/scripts/utils/Permissions.ts +++ b/packages/commonwealth/client/scripts/utils/Permissions.ts @@ -1,8 +1,15 @@ +import { ExtendedCommunity } from '@hicommonwealth/schemas'; import { Role } from '@hicommonwealth/shared'; import app from 'state'; +import { z } from 'zod'; import Thread from '../models/Thread'; import { userStore } from '../state/ui/user'; +type SelectedCommunity = Pick< + z.infer, + 'adminsAndMods' | 'id' +>; + const ROLES = { ADMIN: 'admin', MODERATOR: 'moderator', @@ -25,27 +32,30 @@ const isCommunityMember = (communityId = app.activeChainId()) => { .addresses.some(({ community }) => community.id === communityId); }; -const isCommunityRole = (adminOrMod: Role, communityId?: string) => { - const chainInfo = communityId - ? app.config.chains.getById(communityId) - : app.chain?.meta; - if (!chainInfo) return false; +const isCommunityRole = ( + adminOrMod: Role, + selectedCommunity?: SelectedCommunity, +) => { + const adminAndMods = + selectedCommunity?.adminsAndMods || app.chain?.meta?.adminsAndMods; // selected or active community mods + const communityId = selectedCommunity?.id || app.chain?.meta?.id; // selected or active community id + if (!adminAndMods || !communityId) return false; return userStore.getState().addresses.some(({ community, address }) => { return ( - community.id === chainInfo.id && - chainInfo.adminsAndMods.some( + community.id === communityId && + (adminAndMods || []).some( (role) => role.address === address && role.role === adminOrMod, ) ); }); }; -const isCommunityAdmin = (communityId?: string) => { - return isCommunityRole('admin', communityId); +const isCommunityAdmin = (selectedCommunity?: SelectedCommunity) => { + return isCommunityRole('admin', selectedCommunity); }; -const isCommunityModerator = (communityId?: string) => { - return isCommunityRole('moderator', communityId); +const isCommunityModerator = (selectedCommunity?: SelectedCommunity) => { + return isCommunityRole('moderator', selectedCommunity); }; const isThreadCollaborator = (thread: Thread) => { diff --git a/packages/commonwealth/client/scripts/views/Layout.tsx b/packages/commonwealth/client/scripts/views/Layout.tsx index b7b545a9cc2..a8adb5a92ac 100644 --- a/packages/commonwealth/client/scripts/views/Layout.tsx +++ b/packages/commonwealth/client/scripts/views/Layout.tsx @@ -1,3 +1,4 @@ +import { ExtendedCommunity } from '@hicommonwealth/schemas'; import 'Layout.scss'; import { deinitChainOrCommunity, loadCommunityChainInfo } from 'helpers/chain'; import withRouter, { useCommonNavigate } from 'navigation/helpers'; @@ -9,7 +10,10 @@ import { useFetchConfigurationQuery } from 'state/api/configuration'; import useUserStore from 'state/ui/user'; import { PageNotFound } from 'views/pages/404'; import ErrorPage from 'views/pages/error'; +import { z } from 'zod'; import useNecessaryEffect from '../hooks/useNecessaryEffect'; +import ChainInfo from '../models/ChainInfo'; +import { useGetCommunityByIdQuery } from '../state/api/communities'; import { useUpdateUserActiveCommunityMutation } from '../state/api/user'; import SubLayout from './Sublayout'; import MetaTags from './components/MetaTags'; @@ -37,10 +41,10 @@ const LayoutComponent = ({ const navigate = useCommonNavigate(); const routerParams = useParams(); const pathScope = routerParams?.scope?.toString() || app.customDomainId(); - const selectedScope = scoped ? pathScope : null; + const providedCommunityScope = scoped ? pathScope : null; const user = useUserStore(); - const [scopeToLoad, setScopeToLoad] = useState(); + const [communityToLoad, setCommunityToLoad] = useState(); const [isLoading, setIsLoading] = useState(); const { mutateAsync: updateActiveCommunity } = @@ -51,38 +55,49 @@ const LayoutComponent = ({ // redirect to new community id ex: `commonwealth.im/{new-community-id}/**/*` useNecessaryEffect(() => { // @ts-expect-error - const redirectTo = configurationData?.redirects?.[selectedScope]; + const redirectTo = configurationData?.redirects?.[providedCommunityScope]; // @ts-expect-error - if (redirectTo && redirectTo !== selectedScope.toLowerCase()) { + if (redirectTo && redirectTo !== providedCommunityScope.toLowerCase()) { // @ts-expect-error - const path = window.location.href.split(selectedScope); + const path = window.location.href.split(providedCommunityScope); navigate(`/${redirectTo}${path.length > 1 ? path[1] : ''}`); return; } - }, [selectedScope]); + }, [providedCommunityScope]); - // @ts-expect-error - const scopeMatchesCommunity = app.config.chains.getById(selectedScope); + const { data: community, isLoading: isVerifyingCommunityExistance } = + useGetCommunityByIdQuery({ + id: providedCommunityScope || '', + includeNodeInfo: true, + enabled: !!providedCommunityScope, + }); // If the navigated-to community scope differs from the active chain id at render time, // and we have not begun loading the new navigated-to community data, shouldSelectChain is // set to true, and the navigated-to scope is loaded. const shouldSelectChain = - selectedScope && - selectedScope !== app.activeChainId() && - selectedScope !== scopeToLoad && - scopeMatchesCommunity; + providedCommunityScope && + providedCommunityScope !== app.activeChainId() && + providedCommunityScope !== communityToLoad && + community && + !isVerifyingCommunityExistance; useNecessaryEffect(() => { (async () => { if (shouldSelectChain) { setIsLoading(true); - setScopeToLoad(selectedScope); - if (await loadCommunityChainInfo(scopeMatchesCommunity)) { + setCommunityToLoad(providedCommunityScope); + if ( + await loadCommunityChainInfo( + ChainInfo.fromTRPCResponse( + community as z.infer, + ), + ) + ) { // Update default community on server if logged in if (user.isLoggedIn) { await updateActiveCommunity({ - communityId: scopeMatchesCommunity.id, + communityId: community?.id || '', }); } } @@ -94,15 +109,17 @@ const LayoutComponent = ({ // If scope is not defined (and we are not on a custom domain), deinitialize the loaded chain // with deinitChainOrCommunity(), then set loadingScope to null and render a LoadingLayout. const shouldDeInitChain = - !selectedScope && !app.isCustomDomain() && app.chain && app.chain.network; + !providedCommunityScope && + !app.isCustomDomain() && + app.chain && + app.chain.network; useNecessaryEffect(() => { (async () => { if (shouldDeInitChain) { setIsLoading(true); await deinitChainOrCommunity(); - // @ts-expect-error - setScopeToLoad(null); + setCommunityToLoad(undefined); setIsLoading(false); } })(); @@ -137,7 +154,8 @@ const LayoutComponent = ({ if (shouldShowLoadingState) return Bobber; // If attempting to navigate to a community not fetched by the /status query, return a 404 - const pageNotFound = selectedScope && !scopeMatchesCommunity; + const pageNotFound = + providedCommunityScope && !community && !isVerifyingCommunityExistance; return ( {pageNotFound ? : } diff --git a/packages/commonwealth/client/scripts/views/Sublayout.tsx b/packages/commonwealth/client/scripts/views/Sublayout.tsx index f5400b38453..2c602191f8b 100644 --- a/packages/commonwealth/client/scripts/views/Sublayout.tsx +++ b/packages/commonwealth/client/scripts/views/Sublayout.tsx @@ -117,10 +117,8 @@ const Sublayout = ({ children, isInsideCommunity }: SublayoutProps) => { }, [resizing]); const chain = app.chain ? app.chain.meta : null; - // @ts-expect-error StrictNullChecks - const terms = app.chain ? chain.terms : null; - // @ts-expect-error StrictNullChecks - const banner = app.chain ? chain.communityBanner : null; + const terms = app.chain ? chain?.terms : null; + const banner = app.chain ? chain?.communityBanner : null; return (
@@ -164,8 +162,7 @@ const Sublayout = ({ children, isInsideCommunity }: SublayoutProps) => { resizing, )} > - {/* @ts-expect-error StrictNullChecks */} - +
{ const navigate = useCommonNavigate(); - const community = app.config.chains.getById(app.activeChainId()); + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + const integrations = { - snapshot: community?.snapshot?.length > 0, - discordBot: community?.discordConfigId !== null, - discordBotWebhooksEnabled: community?.discordBotWebhooksEnabled, + snapshot: (community?.snapshot_spaces || [])?.length > 0, + discordBot: community?.discord_config_id !== null, + discordBotWebhooksEnabled: community?.discord_bot_webhooks_enabled, }; - const hasAnyIntegration = + const hasAnyIntegration = !!( integrations.snapshot || integrations.discordBot || - integrations.discordBotWebhooksEnabled; + integrations.discordBotWebhooksEnabled + ); const { setIsVisible, @@ -113,15 +120,17 @@ export const AdminOnboardingSlider = () => { const isCommunitySupported = community?.base === ChainBase.Ethereum && + community?.ChainNode?.eth_chain_id && [ commonProtocol.ValidChains.Base, commonProtocol.ValidChains.SepoliaBase, - ].includes(community?.ChainNode?.ethChainId as number); + ].includes(community?.ChainNode?.eth_chain_id); const isContestActionCompleted = contestEnabled && isCommunitySupported && contestsData?.length > 0; const isSliderHidden = !app.activeChainId() || + isLoadingCommunity || isContestDataLoading || isLoadingTopics || isLoadingGroups || diff --git a/packages/commonwealth/client/scripts/views/components/CommunityStake/useCommunityStake.ts b/packages/commonwealth/client/scripts/views/components/CommunityStake/useCommunityStake.ts index ef0f7a6ff62..3014dff14fa 100644 --- a/packages/commonwealth/client/scripts/views/components/CommunityStake/useCommunityStake.ts +++ b/packages/commonwealth/client/scripts/views/components/CommunityStake/useCommunityStake.ts @@ -1,5 +1,4 @@ import { commonProtocol } from '@hicommonwealth/shared'; -import ChainInfo from 'models/ChainInfo'; import app from 'state'; import { useFetchCommunityStakeQuery, @@ -7,10 +6,16 @@ import { useGetUserStakeBalanceQuery, } from 'state/api/communityStake'; import useUserStore from 'state/ui/user'; -import { CommunityData } from '../../pages/DirectoryPage/DirectoryPageContent'; interface UseCommunityStakeProps { - community?: ChainInfo | CommunityData; + community?: { + id?: string; + namespace?: string; + ChainNode?: { + url: string; + ethChainId: number; + }; + }; stakeId?: number; walletAddress?: string; } diff --git a/packages/commonwealth/client/scripts/views/components/EditProfile/EditProfile.tsx b/packages/commonwealth/client/scripts/views/components/EditProfile/EditProfile.tsx index 7a7553fe999..d91278b15d2 100644 --- a/packages/commonwealth/client/scripts/views/components/EditProfile/EditProfile.tsx +++ b/packages/commonwealth/client/scripts/views/components/EditProfile/EditProfile.tsx @@ -1,7 +1,7 @@ import { useUpdateUserMutation } from 'client/scripts/state/api/user'; import { notifyError } from 'controllers/app/notifications'; import { linkValidationSchema } from 'helpers/formValidations/common'; -import getLinkType from 'helpers/linkType'; +import { getLinkType, isLinkValid } from 'helpers/link'; import Account from 'models/Account'; import AddressInfo from 'models/AddressInfo'; import MinimumProfile from 'models/MinimumProfile'; @@ -349,9 +349,9 @@ const EditProfile = () => { links={links.map((link) => ({ ...link, customElementAfterLink: - link.value && getLinkType(link.value, 'website') ? ( + link.value && isLinkValid(link.value) ? ( diff --git a/packages/commonwealth/client/scripts/views/components/NewThreadForm/NewThreadForm.tsx b/packages/commonwealth/client/scripts/views/components/NewThreadForm/NewThreadForm.tsx index 2cdaa169276..e86f940bc4a 100644 --- a/packages/commonwealth/client/scripts/views/components/NewThreadForm/NewThreadForm.tsx +++ b/packages/commonwealth/client/scripts/views/components/NewThreadForm/NewThreadForm.tsx @@ -175,8 +175,8 @@ export const NewThreadForm = () => { const thread = await createThread({ address: user.activeAccount?.address || '', kind: threadKind, - stage: app.chain.meta.customStages - ? parseCustomStages(app.chain.meta.customStages)[0] + stage: app.chain.meta?.customStages + ? parseCustomStages(app.chain.meta?.customStages)[0] : ThreadStage.Discussion, communityId: app.activeChainId(), title: threadTitle, diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityRow.tsx b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityRow.tsx index 8bd296d6ef8..cb66b0c2524 100644 --- a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityRow.tsx +++ b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityRow.tsx @@ -8,7 +8,7 @@ import withRouter, { navigateToCommunity, useCommonNavigate, } from 'navigation/helpers'; -import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { PopoverMenu } from 'views/components/component_kit/CWPopoverMenu'; import { CWIconButton } from '../component_kit/cw_icon_button'; import { CWText } from '../component_kit/cw_text'; @@ -30,7 +30,10 @@ const ProfileActivityRow = ({ activity }: ProfileActivityRowProps) => { } const isThread = !!(activity as Thread).kind; const comment = activity as CommentWithAssociatedThread; - const { iconUrl } = app.config.chains.getById(communityId); + const { data: community } = useGetCommunityByIdQuery({ + id: communityId, + enabled: !!communityId, + }); const domain = document.location.origin; let decodedTitle: string; @@ -70,7 +73,7 @@ const ProfileActivityRow = ({ activity }: ProfileActivityRowProps) => { return (
- chain-logo + chain-logo { diff --git a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useJoinCommunity.tsx b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useJoinCommunity.tsx index 87275c31bea..49c6e36696e 100644 --- a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useJoinCommunity.tsx +++ b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useJoinCommunity.tsx @@ -35,8 +35,7 @@ const useJoinCommunity = () => { } // add all items on same base as active chain - const addressChainInfo = app.config.chains.getById(a.community.id); - if (addressChainInfo?.base !== activeBase) { + if (a?.community?.base !== activeBase) { return false; } @@ -44,8 +43,7 @@ const useJoinCommunity = () => { const addressExists = !!user.addresses.slice(idx + 1).find( (prev) => activeBase === ChainBase.Substrate && - (app.config.chains.getById(prev.community.id)?.base === - ChainBase.Substrate + (prev.community?.base === ChainBase.Substrate ? addressSwapper({ address: prev.address, currentPrefix: 42, diff --git a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx index 05a69a38664..2a993f0f8f9 100644 --- a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx +++ b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx @@ -1,11 +1,11 @@ import { ChainBase, WalletId } from '@hicommonwealth/shared'; import axios from 'axios'; -import { getUniqueUserAddresses } from 'client/scripts/helpers/user'; import { setActiveAccount } from 'controllers/app/login'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import WebWalletController from 'controllers/app/web_wallets'; import { SessionKeyError } from 'controllers/server/sessions'; import { setDarkMode } from 'helpers/darkMode'; +import { getUniqueUserAddresses } from 'helpers/user'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useEffect, useState } from 'react'; import app, { initAppState } from 'state'; diff --git a/packages/commonwealth/client/scripts/views/components/community_label.tsx b/packages/commonwealth/client/scripts/views/components/community_label.tsx index a06dc9542b4..a35ff095878 100644 --- a/packages/commonwealth/client/scripts/views/components/community_label.tsx +++ b/packages/commonwealth/client/scripts/views/components/community_label.tsx @@ -4,22 +4,30 @@ import 'components/community_label.scss'; import { CWCommunityAvatar } from './component_kit/cw_community_avatar'; import type { IconSize } from './component_kit/cw_icons/types'; -import ChainInfo from 'models/ChainInfo'; import { CWText } from './component_kit/cw_text'; type CommunityLabelProps = { - community: ChainInfo; + iconUrl: string; + name: string; size?: IconSize; }; -export const CommunityLabel = (props: CommunityLabelProps) => { - const { community, size = 'small' } = props; - +export const CommunityLabel = ({ + name, + iconUrl, + size = 'small', +}: CommunityLabelProps) => { return (
- - - {community.name} + + + {name}
); diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/AccountSelector/AccountSelector.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/AccountSelector/AccountSelector.tsx index 3e74c0487af..2821637a098 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/AccountSelector/AccountSelector.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/AccountSelector/AccountSelector.tsx @@ -1,4 +1,3 @@ -import type { ChainNetwork } from '@hicommonwealth/shared'; import { ChainBase } from '@hicommonwealth/shared'; import type Substrate from 'controllers/chain/substrate/adapter'; import React from 'react'; @@ -13,7 +12,7 @@ type LinkAccountItemProps = { idx: number; onSelect: (idx: number) => void; walletChain: ChainBase; - walletNetwork: ChainNetwork; + walletNetwork: string; }; const LinkAccountItem = ({ @@ -78,7 +77,7 @@ type AccountSelectorProps = { onModalClose: () => void; onSelect: (idx: number) => void; walletChain: ChainBase; - walletNetwork: ChainNetwork; + walletNetwork: string; }; // eslint-disable-next-line react/no-multi-comp diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/CWSidebarHeader/CWSidebarHeader.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/CWSidebarHeader/CWSidebarHeader.tsx index ed2be973e93..91befb92a38 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/CWSidebarHeader/CWSidebarHeader.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/CWSidebarHeader/CWSidebarHeader.tsx @@ -21,7 +21,10 @@ const SidebarHeader = ({
navigateToCommunity({ navigate, path: '', chain: app.chain.id }) } diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/CommunityInfo/CommunityInfo.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/CommunityInfo/CommunityInfo.tsx index c97ceda7391..1355c43d31a 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/CommunityInfo/CommunityInfo.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/CommunityInfo/CommunityInfo.tsx @@ -1,4 +1,3 @@ -import ChainInfo from 'models/ChainInfo'; import React from 'react'; import { Link } from 'react-router-dom'; import { CWCommunityAvatar } from 'views/components/component_kit/cw_community_avatar'; @@ -23,10 +22,7 @@ const CommunityInfo = ({ }: CommunityInfoProps) => { return ( - +
{symbol && ( diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/cw_sidebar_menu.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/cw_sidebar_menu.tsx index 3dba5842461..a523e975dca 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/cw_sidebar_menu.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/cw_sidebar_menu.tsx @@ -121,8 +121,9 @@ export const CWSidebarMenuItem = (props: CWSidebarMenuItemProps) => { }); }} > - {/*// @ts-expect-error */} - + {item && ( + + )} {user.isLoggedIn && account && (
{ - const { address, communityInfo } = props; - +export const CWTruncatedAddress = ({ + address, + communityInfo, +}: TruncatedAddressProps) => { return (
{ } > {communityInfo && ( - + )} {formatAddressShort(address)}
diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWRelatedCommunityCard/CWRelatedCommunityCard.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWRelatedCommunityCard/CWRelatedCommunityCard.tsx index 8d8b1fb1fa7..a92d0b74d78 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWRelatedCommunityCard/CWRelatedCommunityCard.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWRelatedCommunityCard/CWRelatedCommunityCard.tsx @@ -1,8 +1,8 @@ +import { ChainBase } from '@hicommonwealth/shared'; import clsx from 'clsx'; import { isCommandClick, pluralizeWithoutNumberPrefix } from 'helpers'; import { disabledStakeButtonTooltipText } from 'helpers/tooltipTexts'; import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; -import type ChainInfo from 'models/ChainInfo'; import { navigateToCommunity, useCommonNavigate } from 'navigation/helpers'; import React, { useCallback } from 'react'; import { useManageCommunityStakeModalStore } from 'state/ui/modals'; @@ -23,7 +23,18 @@ import './CWRelatedCommunityCard.scss'; import { addPeriodToText } from './utils'; type CWRelatedCommunityCardProps = { - community: ChainInfo; + community: { + id: string; + name: string; + description: string; + base: ChainBase; + iconUrl: string; + namespace: string; + ChainNode: { + url: string; + ethChainId: number; + }; + }; memberCount: string | number; threadCount: string | number; canBuyStake?: boolean; @@ -48,7 +59,7 @@ export const CWRelatedCommunityCard = ({ const user = useUserStore(); const { stakeEnabled, stakeValue, stakeChange } = useCommunityCardPrice({ - community: community, + communityId: community?.id, // @ts-expect-error ethUsdRate, stakeId: 2, @@ -83,7 +94,12 @@ export const CWRelatedCommunityCard = ({ const handleBuyStakeClick = () => { onStakeBtnClick?.(); setModeOfManageCommunityStakeModal('buy'); - setSelectedCommunity(community); + setSelectedCommunity({ + id: community.id, + base: community.base, + namespace: community.namespace, + ChainNode: community.ChainNode, + }); }; const disableStakeButton = !user.isLoggedIn || !canBuyStake; @@ -115,7 +131,13 @@ export const CWRelatedCommunityCard = ({
- + {community.name} diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/CWSearchBar.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/CWSearchBar.tsx index a8af924452c..645f6960a6d 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/CWSearchBar.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/CWSearchBar.tsx @@ -13,6 +13,7 @@ import { ComponentType } from '../../types'; import { CWTag } from '../CWTag'; import { SearchBarDropdown } from './SearchBarDropdown'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import './CWSearchBar.scss'; type BaseSearchBarProps = { @@ -59,7 +60,12 @@ export const CWSearchBar: FC = ({ const communityId = showTag ? app.activeChainId() || 'all_communities' : 'all_communities'; - const community = app.config.chains.getById(communityId); + + const { data: community } = useGetCommunityByIdQuery({ + id: communityId, + enabled: communityId === app.activeChainId(), // don't run on communityId = 'all_communities' + }); + const [searchTerm, setSearchTerm] = useState(''); const { searchResults } = useSearchResults(searchTerm, [ 'threads', @@ -144,7 +150,10 @@ export const CWSearchBar: FC = ({ setShowTag(false)} /> )} diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/SearchBarCommunityPreviewRow.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/SearchBarCommunityPreviewRow.tsx index e7da19b14f4..ae2cfba75f5 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/SearchBarCommunityPreviewRow.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWSearchBar/SearchBarCommunityPreviewRow.tsx @@ -28,7 +28,10 @@ export const SearchBarCommunityPreviewRow: FC< return (
- +
); }; diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWTag.tsx b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWTag.tsx index 85fc2c3336b..fb81c249654 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWTag.tsx +++ b/packages/commonwealth/client/scripts/views/components/component_kit/new_designs/CWTag.tsx @@ -36,7 +36,7 @@ export type TagProps = { onClick?: (e?: React.MouseEvent) => void; trimAt?: number; classNames?: string; - community?: ChainInfo; + community?: Pick; onMouseEnter?: (e?: React.MouseEvent) => void; onMouseLeave?: (e: React.MouseEvent) => void; }; @@ -78,8 +78,13 @@ export const CWTag = ({ onMouseLeave={onMouseLeave} > {type === 'input' && ( - // @ts-expect-error - + )} {type === 'contest' && } {!!iconName && ( diff --git a/packages/commonwealth/client/scripts/views/components/feed.tsx b/packages/commonwealth/client/scripts/views/components/feed.tsx index 22b9e1fec17..b29c7cdfbad 100644 --- a/packages/commonwealth/client/scripts/views/components/feed.tsx +++ b/packages/commonwealth/client/scripts/views/components/feed.tsx @@ -12,6 +12,7 @@ import { getProposalUrlPath } from 'identifiers'; import Thread from 'models/Thread'; import { useCommonNavigate } from 'navigation/helpers'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { useFetchGlobalActivityQuery, useFetchUserActivityQuery, @@ -42,11 +43,13 @@ const FeedThread = ({ thread }: { thread: Thread }) => { thread.communityId, ); - const chain = app.config.chains.getById(thread.communityId); + const { data: community } = useGetCommunityByIdQuery({ + id: thread.communityId, + enabled: !!thread.communityId, + }); const isAdmin = - Permissions.isSiteAdmin() || - Permissions.isCommunityAdmin(thread.communityId); + Permissions.isSiteAdmin() || Permissions.isCommunityAdmin(community); const account = user.addresses?.find( (a) => a?.community?.id === thread?.communityId, @@ -79,7 +82,12 @@ const FeedThread = ({ thread }: { thread: Thread }) => { }); // edge case for deleted communities with orphaned posts - if (!chain) return; + if (!community) { + return ( + + ); + } + return ( { threadHref={discussionLink} onCommentBtnClick={() => navigate(`${discussionLink}?focusComments=true`)} disabledActionsTooltipText={disabledActionsTooltipText} - customStages={chain?.customStages} + customStages={community.custom_stages} hideReactionButton hideUpvotesDrawer layoutType="community-first" diff --git a/packages/commonwealth/client/scripts/views/components/linked_addresses.tsx b/packages/commonwealth/client/scripts/views/components/linked_addresses.tsx index 56c3c314a00..c8f0109191c 100644 --- a/packages/commonwealth/client/scripts/views/components/linked_addresses.tsx +++ b/packages/commonwealth/client/scripts/views/components/linked_addresses.tsx @@ -29,7 +29,13 @@ const Address = (props: AddressProps) => { return (
- + { // TODO: move this marshalling into controller const formatCurrency = (n: BN) => { - const decimals = new BN(10).pow(new BN(app.chain.meta.decimals || 6)); - const denom = app.chain.meta.default_symbol; + const decimals = new BN(10).pow(new BN(app.chain.meta?.decimals || 6)); + const denom = app.chain.meta?.default_symbol; const coin = new Coin(denom, n, false, decimals); return coin.format(); }; diff --git a/packages/commonwealth/client/scripts/views/components/sidebar/CommunitySection/CommunitySection.tsx b/packages/commonwealth/client/scripts/views/components/sidebar/CommunitySection/CommunitySection.tsx index da58000f69d..db3e11d4d71 100644 --- a/packages/commonwealth/client/scripts/views/components/sidebar/CommunitySection/CommunitySection.tsx +++ b/packages/commonwealth/client/scripts/views/components/sidebar/CommunitySection/CommunitySection.tsx @@ -32,7 +32,6 @@ export const CommunitySection = ({ showSkeleton }: CommunitySectionProps) => { const user = useUserStore(); const { selectedAddress, - selectedCommunity, modeOfManageCommunityStakeModal, setModeOfManageCommunityStakeModal, } = useManageCommunityStakeModalStore(); @@ -57,9 +56,10 @@ export const CommunitySection = ({ showSkeleton }: CommunitySectionProps) => { if (showSkeleton || isLoading || isContestDataLoading) return ; - const isAdmin = Permissions.isSiteAdmin() || Permissions.isCommunityAdmin(); - const isMod = Permissions.isCommunityModerator(); - const showAdmin = isAdmin || isMod; + const isAdmin = + Permissions.isSiteAdmin() || + Permissions.isCommunityAdmin() || + Permissions.isCommunityModerator(); return ( <> @@ -85,7 +85,7 @@ export const CommunitySection = ({ showSkeleton }: CommunitySectionProps) => { - {showAdmin && ( + {isAdmin && ( <> @@ -129,7 +129,6 @@ export const CommunitySection = ({ showSkeleton }: CommunitySectionProps) => { // @ts-expect-error onModalClose={() => setModeOfManageCommunityStakeModal(null)} denomination={findDenominationString(activeChainId) || 'ETH'} - {...(selectedCommunity && { community: selectedCommunity })} /> } // @ts-expect-error diff --git a/packages/commonwealth/client/scripts/views/components/sidebar/DirectoryMenuItem/DirectoryMenuItem.tsx b/packages/commonwealth/client/scripts/views/components/sidebar/DirectoryMenuItem/DirectoryMenuItem.tsx index 6f9b090f777..9f7a3482cc6 100644 --- a/packages/commonwealth/client/scripts/views/components/sidebar/DirectoryMenuItem/DirectoryMenuItem.tsx +++ b/packages/commonwealth/client/scripts/views/components/sidebar/DirectoryMenuItem/DirectoryMenuItem.tsx @@ -6,6 +6,7 @@ import app from 'state'; import { CWTag } from 'views/components/component_kit/new_designs/CWTag'; import { SubSectionGroup } from '../sidebar_section'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import './DirectoryMenuItem.scss'; const DirectoryMenuItem = () => { @@ -14,16 +15,19 @@ const DirectoryMenuItem = () => { const matchesDirectoryRoute = matchRoutes( [{ path: '/directory' }, { path: ':scope/directory' }], - location + location, ); - const directoryPageEnabled = app.config.chains.getById( - app.activeChainId() - )?.directoryPageEnabled; + const { data: community, isLoading } = useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + + if (isLoading || !community) return; return ( { + ...(joinedCommunities.map((c) => { return { community: c, type: 'community', diff --git a/packages/commonwealth/client/scripts/views/components/sidebar/external_links_module.tsx b/packages/commonwealth/client/scripts/views/components/sidebar/external_links_module.tsx index a808f659778..4e374b6995c 100644 --- a/packages/commonwealth/client/scripts/views/components/sidebar/external_links_module.tsx +++ b/packages/commonwealth/client/scripts/views/components/sidebar/external_links_module.tsx @@ -1,15 +1,30 @@ -import React from 'react'; - import 'components/sidebar/external_links_module.scss'; - +import { categorizeSocialLinks } from 'helpers/link'; +import React from 'react'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { CWIcon } from '../component_kit/cw_icons/cw_icon'; +import CWCircleMultiplySpinner from '../component_kit/new_designs/CWCircleMultiplySpinner'; export const ExternalLinksModule = () => { - if (!app.chain) return; - const meta = app.chain.meta; - const { remainingLinks, discords, elements, telegrams, githubs, twitters } = - meta.categorizeSocialLinks(); + const { data: community, isLoading } = useGetCommunityByIdQuery({ + id: app.activeChainId() || '', + enabled: !!app.activeChainId(), + }); + + if (!app.chain || !community) return; + + if (isLoading) return ; + + const { + discords, + elements, + githubs, + remainingLinks, + slacks, + telegrams, + twitters, + } = categorizeSocialLinks(community.social_links || []); return (
@@ -53,7 +68,7 @@ export const ExternalLinksModule = () => { onClick={() => window.open(link)} /> ))} - {remainingLinks.map((link) => ( + {[...remainingLinks, ...slacks].map((link) => ( { // Conditional Render Details const hasProposals = app.chain && - (app.chain.base === ChainBase.CosmosSDK || app.chain.meta.snapshot?.length); + (app.chain.base === ChainBase.CosmosSDK || + app.chain.meta?.snapshot?.length); - const isNotOffchain = app.chain?.meta.type !== ChainType.Offchain; + const isNotOffchain = app.chain?.meta?.type !== ChainType.Offchain; const showSnapshotOptions = app.chain?.base === ChainBase.Ethereum && @@ -157,7 +158,7 @@ export const GovernanceSection = () => { setGovernanceToggleTree('children.Snapshots.toggledState', toggle); resetSidebarState(); // Check if we have multiple snapshots for conditional redirect - const snapshotSpaces = app.chain.meta.snapshot; + const snapshotSpaces = app.chain?.meta?.snapshot; if (snapshotSpaces.length > 1) { handleRedirectClicks( navigate, diff --git a/packages/commonwealth/client/scripts/views/components/sidebar/sidebar_quick_switcher.tsx b/packages/commonwealth/client/scripts/views/components/sidebar/sidebar_quick_switcher.tsx index 831b923a773..361c05be36a 100644 --- a/packages/commonwealth/client/scripts/views/components/sidebar/sidebar_quick_switcher.tsx +++ b/packages/commonwealth/client/scripts/views/components/sidebar/sidebar_quick_switcher.tsx @@ -69,7 +69,10 @@ export const SidebarQuickSwitcher = ({ navigateToCommunity({ navigate, path: '', chain: item.id }) } diff --git a/packages/commonwealth/client/scripts/views/components/snapshot_proposal_selector/snapshot_proposal_selector.tsx b/packages/commonwealth/client/scripts/views/components/snapshot_proposal_selector/snapshot_proposal_selector.tsx index 1c5271a7d82..6b3a849c250 100644 --- a/packages/commonwealth/client/scripts/views/components/snapshot_proposal_selector/snapshot_proposal_selector.tsx +++ b/packages/commonwealth/client/scripts/views/components/snapshot_proposal_selector/snapshot_proposal_selector.tsx @@ -37,15 +37,17 @@ export const SnapshotProposalSelector = ({ useEffect(() => { if (allProposals.length === 0) { setLoading(true); - loadMultipleSpacesData(app.chain.meta.snapshot).then((data = []) => { - const loadedProposals = data.reduce( - (acc, curr) => [...acc, ...curr.proposals], - [], - ); - - setAllProposals(loadedProposals); - setLoading(false); - }); + loadMultipleSpacesData(app.chain.meta?.snapshot) + .then((data = []) => { + const loadedProposals = data.reduce( + (acc, curr) => [...acc, ...curr.proposals], + [], + ); + + setAllProposals(loadedProposals); + setLoading(false); + }) + .catch(console.error); } }, [allProposals]); diff --git a/packages/commonwealth/client/scripts/views/components/user/fullUser.tsx b/packages/commonwealth/client/scripts/views/components/user/fullUser.tsx index 1f715740d23..89a01dd84e5 100644 --- a/packages/commonwealth/client/scripts/views/components/user/fullUser.tsx +++ b/packages/commonwealth/client/scripts/views/components/user/fullUser.tsx @@ -5,6 +5,7 @@ import 'components/user/user.scss'; import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import useUserStore from 'state/ui/user'; import { Avatar } from 'views/components/Avatar'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; @@ -40,7 +41,13 @@ export const FullUser = ({ const loggedInUser = useUserStore(); const [isModalOpen, setIsModalOpen] = useState(false); - if (showSkeleton) { + const { data: userCommunity, isLoading: isLoadingUserCommunity } = + useGetCommunityByIdQuery({ + id: userCommunityId || '', + enabled: !!userCommunityId, + }); + + if (showSkeleton || isLoadingUserCommunity) { return ( address === userAddress, diff --git a/packages/commonwealth/client/scripts/views/components/user/user.tsx b/packages/commonwealth/client/scripts/views/components/user/user.tsx index 19cf0699686..1e40bfe03c2 100644 --- a/packages/commonwealth/client/scripts/views/components/user/user.tsx +++ b/packages/commonwealth/client/scripts/views/components/user/user.tsx @@ -4,6 +4,7 @@ import 'components/user/user.scss'; import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { useFetchProfilesByAddressesQuery } from 'state/api/profiles'; import useUserStore from 'state/ui/user'; import { Avatar } from 'views/components/Avatar'; @@ -46,7 +47,13 @@ export const User = ({ const [isModalOpen, setIsModalOpen] = useState(false); - if (showSkeleton) { + const { data: userCommunity, isLoading: isLoadingUserCommunity } = + useGetCommunityByIdQuery({ + id: userCommunityId || '', + enabled: !!userCommunityId, + }); + + if (showSkeleton || isLoadingUserCommunity) { return ( address === userAddress, diff --git a/packages/commonwealth/client/scripts/views/menus/create_content_menu.tsx b/packages/commonwealth/client/scripts/views/menus/create_content_menu.tsx index 7329e5187b6..68765cafa8f 100644 --- a/packages/commonwealth/client/scripts/views/menus/create_content_menu.tsx +++ b/packages/commonwealth/client/scripts/views/menus/create_content_menu.tsx @@ -34,7 +34,7 @@ const resetSidebarState = () => { const getCreateContentMenuItems = (navigate): PopoverMenuItem[] => { const showSnapshotOptions = - userStore.getState() && !!app.chain?.meta.snapshot.length; + userStore.getState() && !!app.chain?.meta?.snapshot?.length; const showOnChainProposalItem = app.chain?.base === ChainBase.CosmosSDK && @@ -63,7 +63,7 @@ const getCreateContentMenuItems = (navigate): PopoverMenuItem[] => { iconLeft: 'democraticProposal', onClick: () => { resetSidebarState(); - const snapshotSpaces = app.chain.meta.snapshot; + const snapshotSpaces = app.chain.meta?.snapshot; navigate(`/new/snapshot/${snapshotSpaces}`, { action: 'create-proposal', }); @@ -88,7 +88,7 @@ const getCreateContentMenuItems = (navigate): PopoverMenuItem[] => { const getDiscordBotConnectionItems = (): PopoverMenuItem[] => { const isAdmin = Permissions.isSiteAdmin() || Permissions.isCommunityAdmin(); - const botNotConnected = app.chain.meta.discordConfigId === null; + const botNotConnected = app.chain.meta?.discordConfigId === null; if (isAdmin && botNotConnected) { return [ diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx b/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx index 1a098818bff..a90aff5f616 100644 --- a/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx @@ -7,7 +7,6 @@ import { WalletSsoSource, } from '@hicommonwealth/shared'; import axios from 'axios'; -import { useUpdateUserMutation } from 'client/scripts/state/api/user'; import { completeClientLogin, createUserWithAddress, @@ -29,6 +28,7 @@ import React, { useEffect, useState } from 'react'; import { isMobile } from 'react-device-detect'; import app, { initAppState } from 'state'; import { SERVER_URL } from 'state/api/config'; +import { useUpdateUserMutation } from 'state/api/user'; import useUserStore from 'state/ui/user'; import { BaseMixpanelPayload, @@ -311,12 +311,7 @@ const useAuthentication = (props: UseAuthenticationProps) => { setDarkMode(true); } if (app.chain) { - const community = - user.activeCommunity || - app.config.chains.getById(app.activeChainId()); - await updateActiveAddresses({ - chain: community, - }); + await updateActiveAddresses(app.activeChainId()); } } diff --git a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/ManageCommunityStakeModal.tsx b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/ManageCommunityStakeModal.tsx index 6eb4ad16b48..daea85ba462 100644 --- a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/ManageCommunityStakeModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/ManageCommunityStakeModal.tsx @@ -13,13 +13,14 @@ import { ManageCommunityStakeModalState, } from './types'; +import { ChainBase } from '@hicommonwealth/shared'; +import { useManageCommunityStakeModalStore } from 'state/ui/modals'; import './ManageCommunityStakeModal.scss'; const ManageCommunityStakeModal = ({ onModalClose, mode, denomination, - community, }: ManageCommunityStakeModalProps) => { const [modalState, setModalState] = useState( ManageCommunityStakeModalState.Exchange, @@ -27,8 +28,12 @@ const ManageCommunityStakeModal = ({ const [successTransactionHash, setSuccessTransactionHash] = useState(''); const [numberOfStakeToExchange, setNumberOfStakeToExchange] = useState(1); + const { selectedCommunity: community } = useManageCommunityStakeModalStore(); + const { selectedAddress, setSelectedAddress, addressOptions } = - useStakeAddresses({ community }); + useStakeAddresses({ + stakedCommunityChainBase: community?.base as ChainBase, + }); const getModalBody = () => { switch (modalState) { @@ -44,7 +49,6 @@ const ManageCommunityStakeModal = ({ numberOfStakeToExchange={numberOfStakeToExchange} onSetNumberOfStakeToExchange={setNumberOfStakeToExchange} denomination={denomination} - community={community} /> ); case ManageCommunityStakeModalState.Loading: diff --git a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/StakeExchangeForm/StakeExchangeForm.tsx b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/StakeExchangeForm/StakeExchangeForm.tsx index 5cc14639181..ee91de8b1bf 100644 --- a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/StakeExchangeForm/StakeExchangeForm.tsx +++ b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/StakeExchangeForm/StakeExchangeForm.tsx @@ -2,7 +2,6 @@ import { commonProtocol } from '@hicommonwealth/shared'; import clsx from 'clsx'; import { findDenominationIcon } from 'helpers/findDenomination'; import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; -import ChainInfo from 'models/ChainInfo'; import React from 'react'; import { isMobile } from 'react-device-detect'; import { @@ -14,6 +13,7 @@ import { useBuyStakeMutation, useSellStakeMutation, } from 'state/api/communityStake'; +import { useManageCommunityStakeModalStore } from 'state/ui/modals'; import useUserStore from 'state/ui/user'; import { useCommunityStake } from 'views/components/CommunityStake'; import NumberSelector from 'views/components/NumberSelector'; @@ -61,7 +61,6 @@ interface StakeExchangeFormProps { numberOfStakeToExchange: number; onSetNumberOfStakeToExchange: React.Dispatch>; denomination: string; - community?: ChainInfo; } const StakeExchangeForm = ({ @@ -74,10 +73,11 @@ const StakeExchangeForm = ({ numberOfStakeToExchange, onSetNumberOfStakeToExchange, denomination, - community, }: StakeExchangeFormProps) => { const user = useUserStore(); + const { selectedCommunity: community } = useManageCommunityStakeModalStore(); + const chainRpc = community?.ChainNode?.url || app?.chain?.meta?.ChainNode?.url; const ethChainId = @@ -96,7 +96,10 @@ const StakeExchangeForm = ({ mode, address: selectedAddress?.value, numberOfStakeToExchange: numberOfStakeToExchange ?? 0, - community, + community: { + namespace: community?.namespace, + ChainNode: community?.ChainNode, + }, }); const { stakeBalance, stakeValue, currentVoteWeight, stakeData } = diff --git a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeAddresses.ts b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeAddresses.ts index 9024fa25e71..dc0f4f3994e 100644 --- a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeAddresses.ts +++ b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeAddresses.ts @@ -1,29 +1,28 @@ import { useState } from 'react'; import app from 'state'; +import { ChainBase } from '@hicommonwealth/shared'; import { getUniqueUserAddresses } from 'helpers/user'; -import ChainInfo from 'models/ChainInfo'; import useUserStore from 'state/ui/user'; -import { CommunityData } from 'views/pages/DirectoryPage/DirectoryPageContent'; import { getAvailableAddressesForStakeExchange, getInitialAccountValue, } from '../utils'; interface UseStakeAddressesProps { - community?: ChainInfo | CommunityData; + stakedCommunityChainBase: ChainBase; } -const useStakeAddresses = ({ community }: UseStakeAddressesProps = {}) => { +const useStakeAddresses = ({ + stakedCommunityChainBase, +}: UseStakeAddressesProps) => { const user = useUserStore(); const communityAddresses = (() => { - if (community) { + if (stakedCommunityChainBase) { // get all the addresses of the user that matches base chain of selected `community` const userAddresses = user.addresses - .filter( - (addr) => addr.community.base === (community as ChainInfo)?.base, - ) + .filter((addr) => addr.community.base === stakedCommunityChainBase) .map((addr) => addr.address); // return all the unique addresses diff --git a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeExchange.ts b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeExchange.ts index 7789ff9eaa1..4e506496277 100644 --- a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeExchange.ts +++ b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/hooks/useStakeExchange.ts @@ -1,5 +1,4 @@ import { commonProtocol } from '@hicommonwealth/shared'; -import ChainInfo from 'models/ChainInfo'; import app from 'state'; import { useFetchEthUsdRateQuery, @@ -8,13 +7,18 @@ import { } from 'state/api/communityStake'; import useGetUserEthBalanceQuery from 'state/api/communityStake/getUserEthBalance'; import { ManageCommunityStakeModalMode } from 'views/modals/ManageCommunityStakeModal/types'; -import { CommunityData } from 'views/pages/DirectoryPage/DirectoryPageContent'; interface UseStakeExchangeProps { mode: ManageCommunityStakeModalMode; address: string; numberOfStakeToExchange: number; - community?: ChainInfo | CommunityData; + community?: { + namespace?: string; + ChainNode?: { + url: string; + ethChainId: number; + }; + }; } const useStakeExchange = ({ diff --git a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/types.ts b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/types.ts index 50e3f13c7d4..88ed08e1b42 100644 --- a/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/types.ts +++ b/packages/commonwealth/client/scripts/views/modals/ManageCommunityStakeModal/types.ts @@ -1,12 +1,9 @@ -import ChainInfo from 'models/ChainInfo'; - export type ManageCommunityStakeModalMode = 'buy' | 'sell'; export interface ManageCommunityStakeModalProps { onModalClose: () => void; mode: ManageCommunityStakeModalMode; denomination: string; - community?: ChainInfo; } export enum ManageCommunityStakeModalState { diff --git a/packages/commonwealth/client/scripts/views/modals/WelcomeOnboardModal/steps/JoinCommunityStep/JoinCommunityCard/JoinCommunityCard.tsx b/packages/commonwealth/client/scripts/views/modals/WelcomeOnboardModal/steps/JoinCommunityStep/JoinCommunityCard/JoinCommunityCard.tsx index b750daa9c91..eaf50cd1f5c 100644 --- a/packages/commonwealth/client/scripts/views/modals/WelcomeOnboardModal/steps/JoinCommunityStep/JoinCommunityCard/JoinCommunityCard.tsx +++ b/packages/commonwealth/client/scripts/views/modals/WelcomeOnboardModal/steps/JoinCommunityStep/JoinCommunityCard/JoinCommunityCard.tsx @@ -26,7 +26,13 @@ const JoinCommunityCard = ({ : community?.profileCount; return (
- +
diff --git a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx index 350bf75ec66..3dce50224b9 100644 --- a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx @@ -83,7 +83,7 @@ export const UpdateProposalStatusModal = ({ const { isAddedToHomeScreen } = useAppStatus(); - const showSnapshot = !!app.chain.meta.snapshot?.length; + const showSnapshot = !!app.chain.meta?.snapshot?.length; const isCosmos = app.chain.base === ChainBase.CosmosSDK; const { mutateAsync: editThread } = useEditThreadMutation({ @@ -125,9 +125,9 @@ export const UpdateProposalStatusModal = ({ ); if (toAdd.length > 0) { - if (app.chain.meta.snapshot?.length === 1) { + if (app.chain.meta?.snapshot?.length === 1) { const enrichedSnapshot = { - id: `${app.chain.meta.snapshot[0]}/${toAdd[0].id}`, + id: `${app.chain.meta?.snapshot?.[0]}/${toAdd[0].id}`, title: toAdd[0].title, }; return addThreadLinks({ @@ -146,7 +146,7 @@ export const UpdateProposalStatusModal = ({ return { toDelete, links }; }); } else { - return loadMultipleSpacesData(app.chain.meta.snapshot) + return loadMultipleSpacesData(app.chain.meta?.snapshot) .then((data) => { let enrichedSnapshot; for (const { space: _space, proposals } of data) { diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx index 109fc62ce91..a30e3523c5e 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx @@ -3,14 +3,11 @@ import { notifyError } from 'controllers/app/notifications'; import useNecessaryEffect from 'hooks/useNecessaryEffect'; import 'pages/AdminPanel.scss'; import React, { useState } from 'react'; -import app from 'state'; import { SERVER_URL } from 'state/api/config'; import useUserStore from 'state/ui/user'; import { CWText } from '../../components/component_kit/cw_text'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; -import { ValidationStatus } from '../../components/component_kit/cw_validation_text'; -import { CWButton } from '../../components/component_kit/new_designs/CWButton'; import CWCircleMultiplySpinner from '../../components/component_kit/new_designs/CWCircleMultiplySpinner'; +import CommunityFinder from './CommunityFinder'; type Stats = { numCommentsLastMonth: number; @@ -30,15 +27,12 @@ const Analytics = () => { { id: string; created_at: string }[] >([]); const [globalStats, setGlobalStats] = useState(); - const [communityLookupValue, setCommunityLookupValue] = useState(''); - const [communityLookupValidated, setCommunityLookupValidated] = - useState(false); const [communityLookupCompleted, setCommunityLookupCompleted] = useState(false); const [communityAnalytics, setCommunityAnalytics] = useState(); const user = useUserStore(); - const getCommunityAnalytics = async (communityId: string) => { + const getCommunityAnalytics = (communityId: string) => { axios .get(`${SERVER_URL}/admin/analytics?community_id=${communityId}`, { params: { @@ -84,24 +78,6 @@ const Analytics = () => { } }, [initialized]); - const validationFn = (value: string): [ValidationStatus, string] | [] => { - if (!value || !app.config.chains.getById(value)) { - setCommunityLookupCompleted(false); - setCommunityLookupValidated(false); - return ['failure', 'Community not found']; - } - setCommunityLookupValidated(true); - return []; - }; - - const onInput = (e) => { - setCommunityLookupValue(e.target.value); - if (e.target.value.length === 0) { - setCommunityLookupValidated(false); - setCommunityLookupCompleted(false); - } - }; - return (
{!initialized ? ( @@ -182,23 +158,11 @@ const Analytics = () => { Search for 30 day analytics from a specific community. -
- - { - await getCommunityAnalytics(communityLookupValue); - }} - /> -
- {communityLookupValidated && communityLookupCompleted && ( + + {communityAnalytics && communityLookupCompleted && (
Total Threads diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/CommunityFinder.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/CommunityFinder.tsx new file mode 100644 index 00000000000..d472805f0db --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/CommunityFinder.tsx @@ -0,0 +1,56 @@ +import 'pages/AdminPanel.scss'; +import React, { useState } from 'react'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; +import { useDebounce } from 'usehooks-ts'; +import { CWButton } from '../../components/component_kit/new_designs/CWButton'; +import { CWTextInput } from '../../components/component_kit/new_designs/CWTextInput'; + +type CommunityFinderProps = { + ctaLabel: string; + actionDisabled?: boolean; + onAction?: (communityId: string) => void; +}; + +const CommunityFinder = ({ + ctaLabel, + actionDisabled, + onAction, +}: CommunityFinderProps) => { + const [communityLookupId, setCommunityLookupId] = useState(''); + const debouncedCommunityLookupId = useDebounce( + communityLookupId, + 500, + ); + + const { data: communityLookupData, isLoading: isLoadingCommunityLookupData } = + useGetCommunityByIdQuery({ + id: debouncedCommunityLookupId || '', + enabled: !!debouncedCommunityLookupId, + }); + + return ( +
+ setCommunityLookupId(e?.target?.value?.trim() || '')} + customError={ + !isLoadingCommunityLookupData && + (!communityLookupData || + Object.keys(communityLookupData || {})?.length === 0) + ? 'Community not found' + : '' + } + placeholder="Enter a community id" + fullWidth + /> + onAction?.(communityLookupId)} + /> +
+ ); +}; + +export default CommunityFinder; diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/DeleteChainTask.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/DeleteChainTask.tsx index 2ea86b6b21c..45fa0c4e5a2 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/DeleteChainTask.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/DeleteChainTask.tsx @@ -1,23 +1,21 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import 'pages/AdminPanel.scss'; import React, { useState } from 'react'; -import app from 'state'; import { CWText } from '../../components/component_kit/cw_text'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; -import { ValidationStatus } from '../../components/component_kit/cw_validation_text'; -import { CWButton } from '../../components/component_kit/new_designs/CWButton'; import { openConfirmation } from '../../modals/confirmation_modal'; +import CommunityFinder from './CommunityFinder'; import { deleteCommunity } from './utils'; const DeleteCommunityTask = () => { - const [deleteCommunityValue, setDeleteCommunityValue] = useState(''); - const [deleteCommunityValueValidated, setDeleteCommunityValueValidated] = - useState(false); + const [componentKey, setComponentKey] = useState(1); - const openConfirmationModal = () => { + const openConfirmationModal = (communityIdToDelete: string) => { openConfirmation({ title: 'Delete Community', - description: `Are you sure you want to delete ${deleteCommunityValue}? This action cannot be reversed. Note that this will NOT work if there is an admin in the community.`, + description: ` + Are you sure you want to delete ${communityIdToDelete}? + This action cannot be reversed. Note that this will NOT work if there is an admin in the community. + `, buttons: [ { label: 'Delete', @@ -25,8 +23,8 @@ const DeleteCommunityTask = () => { buttonHeight: 'sm', onClick: async () => { try { - await deleteCommunity({ id: deleteCommunityValue }); - setDeleteCommunityValue(''); + await deleteCommunity({ id: communityIdToDelete }); + setComponentKey((key) => key + 1); notifySuccess('Community deleted'); } catch (e) { notifyError('Error deleting community'); @@ -44,41 +42,17 @@ const DeleteCommunityTask = () => { }); }; - const onInput = (e) => { - setDeleteCommunityValue(e.target.value); - if (e.target.value.length === 0) setDeleteCommunityValueValidated(false); - }; - - const validationFn = (value: string): [ValidationStatus, string] | [] => { - if (!app.config.chains.getById(value)) { - setDeleteCommunityValueValidated(false); - return ['failure', 'Community not found']; - } - setDeleteCommunityValueValidated(true); - return []; - }; - return ( -
+
Delete Community Removes a CW community (chain) from the DB. This is destructive action that cannot be reversed. -
- - -
+ openConfirmationModal(communityId)} + />
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/DownloadMembersListTask.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/DownloadMembersListTask.tsx index 1b94c83b929..60ef9cd1e62 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/DownloadMembersListTask.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/DownloadMembersListTask.tsx @@ -1,40 +1,22 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import 'pages/AdminPanel.scss'; -import React, { useEffect, useState } from 'react'; -import app from 'state'; +import React, { useState } from 'react'; import { CWText } from '../../components/component_kit/cw_text'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; -import { ValidationStatus } from '../../components/component_kit/cw_validation_text'; -import { CWButton } from '../../components/component_kit/new_designs/CWButton'; import { openConfirmation } from '../../modals/confirmation_modal'; +import CommunityFinder from './CommunityFinder'; import { downloadCSV, getCSVContent } from './utils'; const DownloadMembersListTask = () => { - const [communityId, setCommunityId] = useState(''); - const [communityIdValidated, setCommunityIdValidated] = - useState(false); + const [componentKey, setComponentKey] = useState(1); const [isDownloading, setIsDownloading] = useState(false); - const [loadingDots, setLoadingDots] = useState(''); - useEffect(() => { - if (isDownloading) { - const interval = setInterval(() => { - setLoadingDots((prevDots) => { - if (prevDots.length < 3) { - return prevDots + '.'; - } else { - return ''; - } - }); - }, 200); - return () => clearInterval(interval); - } - }, [isDownloading]); - - const openConfirmationModal = () => { + const openConfirmationModal = (communityId: string) => { openConfirmation({ title: 'Download Members List', - description: `This will download a CSV file with member information for ${communityId}. This can take a few seconds.`, + description: ` + This will download a CSV file with member information for ${communityId}. + This can take a few seconds. + `, buttons: [ { label: 'Download', @@ -46,7 +28,7 @@ const DownloadMembersListTask = () => { const res = await getCSVContent({ id: communityId }); downloadCSV(res, `${communityId}_members_list.csv`); setIsDownloading(false); - setCommunityId(''); + setComponentKey((k) => k + 1); notifySuccess('Members list downloaded'); } catch (e) { notifyError('Error downloading members list'); @@ -63,46 +45,17 @@ const DownloadMembersListTask = () => { }); }; - const onInput = (e) => { - setCommunityId(e.target.value); - if (e.target.value.length === 0) setCommunityIdValidated(false); - }; - - const validationFn = (value: string): [ValidationStatus, string] | [] => { - if (!app.config.chains.getById(value)) { - setCommunityIdValidated(false); - return ['failure', 'Community not found']; - } - setCommunityIdValidated(true); - return []; - }; - return ( -
+
Download Members List Downloads a list of members for a CW community (chain) from the DB. -
- - {isDownloading ? ( -
- {`Downloading${loadingDots}`} -
- ) : ( - - )} -
+ openConfirmationModal(communityId)} + />
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/RPCEndpointTask.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/RPCEndpointTask.tsx index 989a78d619b..f23344d3f38 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/RPCEndpointTask.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/RPCEndpointTask.tsx @@ -2,12 +2,15 @@ import { BalanceType, ChainType } from '@hicommonwealth/shared'; import axios from 'axios'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import { detectURL } from 'helpers/threads'; -import CommunityInfo from 'models/ChainInfo'; import NodeInfo from 'models/NodeInfo'; import 'pages/AdminPanel.scss'; import React, { useEffect, useState } from 'react'; -import app from 'state'; +import { + useGetCommunityByIdQuery, + useUpdateCommunityMutation, +} from 'state/api/communities'; import { getNodeByCosmosChainId, getNodeByUrl } from 'state/api/nodes/utils'; +import { useDebounce } from 'usehooks-ts'; import useFetchNodesQuery from '../../../state/api/nodes/fetchNodes'; import { CWDropdown, @@ -15,22 +18,21 @@ import { } from '../../components/component_kit/cw_dropdown'; import { CWLabel } from '../../components/component_kit/cw_label'; import { CWText } from '../../components/component_kit/cw_text'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; +import { CWTextInput as CWTextInputOld } from '../../components/component_kit/cw_text_input'; import { CWValidationText, ValidationStatus, } from '../../components/component_kit/cw_validation_text'; import { CWButton } from '../../components/component_kit/new_designs/CWButton'; +import { CWTextInput } from '../../components/component_kit/new_designs/CWTextInput'; import { CWTypeaheadSelectList } from '../../components/component_kit/new_designs/CWTypeaheadSelectList'; import { openConfirmation } from '../../modals/confirmation_modal'; import { createChainNode, updateChainNode } from './utils'; const RPCEndpointTask = () => { const [errorMsg, setErrorMsg] = React.useState(null); - const [rpcEndpointCommunityValue, setRpcEndpointCommunityValue] = + const [rpcEndpointCommunityId, setRpcEndpointCommunityId] = useState(''); - // @ts-expect-error - const [communityInfo, setCommunityInfo] = useState(null); const [rpcEndpoint, setRpcEndpoint] = useState(''); const [communityInfoValueValidated, setCommunityInfoValueValidated] = useState(false); @@ -68,6 +70,24 @@ const RPCEndpointTask = () => { } }, [balanceType]); + const debouncedCommunityLookupId = useDebounce( + rpcEndpointCommunityId, + 500, + ); + + const { data: communityLookupData, isLoading: isLoadingCommunityLookupData } = + useGetCommunityByIdQuery({ + id: debouncedCommunityLookupId || '', + enabled: !!debouncedCommunityLookupId, + }); + + const communityNotFound = + !isLoadingCommunityLookupData && + (!communityLookupData || + Object.keys(communityLookupData || {})?.length === 0); + + const communityNotChain = communityLookupData?.type !== ChainType.Chain; + const buttonEnabled = (communityChainNodeValidated && communityChainNode && @@ -78,9 +98,14 @@ const RPCEndpointTask = () => { (bech32 !== '' || balanceType === BalanceType.Ethereum) && rpcEndpoint !== ''); + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: rpcEndpointCommunityId, + }); + const setCommunityIdInput = (e) => { - setRpcEndpointCommunityValue(e.target.value); - if (e.target.value.length === 0) setCommunityInfoValueValidated(false); + setRpcEndpointCommunityId(e?.target?.value?.trim() || ''); + if (e?.target?.value?.trim()?.length === 0) + setCommunityInfoValueValidated(false); }; const RPCEndpointValidationFn = ( @@ -139,27 +164,8 @@ const RPCEndpointTask = () => { return ['failure', err]; }; - const idValidationFn = (value: string): [ValidationStatus, string] | [] => { - const communityInfoData = app.config.chains.getById(value); - if (!communityInfoData) { - setCommunityInfoValueValidated(false); - const err = 'Community not found'; - setErrorMsg(err); - return ['failure', err]; - } - if (communityInfoData.type !== ChainType.Chain) { - setCommunityInfoValueValidated(false); - const err = 'Community is not a chain'; - setErrorMsg(err); - return ['failure', err]; - } - setCommunityInfo(communityInfoData); - setCommunityInfoValueValidated(true); - setErrorMsg(null); - return []; - }; - const update = async () => { + if (!communityLookupData?.id) return; try { let nodeId = null; if (chainNodeNotCreated) { @@ -187,18 +193,20 @@ const RPCEndpointTask = () => { nodeId = communityChainNode.id; } - if (communityInfo && communityInfoValueValidated) { - await communityInfo.updateChainData({ - chain_node_id: nodeId ?? communityChainNode.id.toString(), + if ( + Object.keys(communityLookupData || {}).length > 0 && + communityInfoValueValidated + ) { + await updateCommunity({ + communityId: communityLookupData?.id, + chainNodeId: nodeId ?? communityChainNode?.id?.toString(), type: ChainType.Chain, }); } - setRpcEndpointCommunityValue(''); + setRpcEndpointCommunityId(''); setRpcEndpoint(''); // @ts-expect-error - setCommunityInfo(null); - // @ts-expect-error setCommunityChainNode(null); setCommunityChainNodeValidated(false); setBech32(''); @@ -215,7 +223,7 @@ const RPCEndpointTask = () => { const openConfirmationModal = () => { openConfirmation({ title: 'Update RPC Endpoint', - description: `Are you sure you want to update the rpc endpoint on ${rpcEndpointCommunityValue}?`, + description: `Are you sure you want to update the rpc endpoint on ${rpcEndpointCommunityId}?`, buttons: [ { label: 'Update', @@ -248,6 +256,12 @@ const RPCEndpointTask = () => { return chainIds; }; + const communityIdInputError = (() => { + if (communityNotFound) return 'Community not found'; + if (communityNotChain) return 'Community is not a chain'; + return ''; + })(); + return (
Switch/Add RPC Endpoint @@ -258,12 +272,12 @@ const RPCEndpointTask = () => {
- { setRpcEndpoint(e.target.value); @@ -274,7 +288,7 @@ const RPCEndpointTask = () => {
@@ -293,7 +307,7 @@ const RPCEndpointTask = () => { setBalanceType(item.value as BalanceType); }} /> - { @@ -306,7 +320,7 @@ const RPCEndpointTask = () => {
{balanceType === BalanceType.Ethereum && (
- { setEthChainId(parseInt(e.target.value)); @@ -334,7 +348,7 @@ const RPCEndpointTask = () => { }} />
- { diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCommunityIdTask.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCommunityIdTask.tsx index a56a233d533..bb054d8d422 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCommunityIdTask.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCommunityIdTask.tsx @@ -1,27 +1,19 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import React, { useState } from 'react'; -import app from 'state'; -import { slugifyPreserveDashes } from 'utils'; +import { slugifyPreserveDashes } from 'shared/utils'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; +import { useDebounce } from 'usehooks-ts'; import { CWText } from 'views/components/component_kit/cw_text'; -import { ValidationStatus } from 'views/components/component_kit/cw_validation_text'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; import { updateCommunityId } from 'views/pages/AdminPanel/utils'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; +import { CWTextInput } from '../../components/component_kit/new_designs/CWTextInput'; import { openConfirmation } from '../../modals/confirmation_modal'; const UpdateCommunityIdTask = () => { - const [originalCommunityValue, setOriginalCommunityValue] = - useState(''); - const [originalValueValidated, setOriginalValueValidated] = - useState(false); - - const [newCommunityValue, setNewCommunityValue] = useState(''); - const [newValueValidated, setNewValueValidated] = useState(false); - const openConfirmationModal = () => { openConfirmation({ title: 'Update Community Id', - description: `Are you sure you want to update ${originalCommunityValue} to ${newCommunityValue}?`, + description: `Are you sure you want to update ${originalCommunityId} to ${newCommunityId}?`, buttons: [ { label: 'Update', @@ -30,11 +22,11 @@ const UpdateCommunityIdTask = () => { onClick: async () => { try { await updateCommunityId({ - community_id: originalCommunityValue, - new_community_id: newCommunityValue, + community_id: originalCommunityId, + new_community_id: newCommunityId, }); - setOriginalCommunityValue(''); - setNewCommunityValue(''); + setOriginalCommunityId(''); + setNewCommunityId(''); notifySuccess('Community id updated'); } catch (e) { notifyError('Error updating community id'); @@ -51,31 +43,34 @@ const UpdateCommunityIdTask = () => { }); }; - const validateFn = ( - value: string, - isNewCommunityId: boolean, - ): [ValidationStatus, string] | [] => { - const communityExists = app.config.chains.getById(value); + const [originalCommunityId, setOriginalCommunityId] = useState(''); + const debouncedOriginalCommunityId = useDebounce( + originalCommunityId, + 500, + ); + const { data: originalCommunity, isLoading: isLoadingOriginalCommunity } = + useGetCommunityByIdQuery({ + id: debouncedOriginalCommunityId || '', + enabled: !!debouncedOriginalCommunityId, + }); - if (communityExists && isNewCommunityId) { - setNewValueValidated(false); - return ['failure', 'Community already exists']; - } else if (!communityExists && !isNewCommunityId) { - setOriginalValueValidated(false); - return ['failure', 'Community not found']; - } else if (isNewCommunityId && value !== slugifyPreserveDashes(value)) { - setNewValueValidated(false); - return ['failure', 'Incorrect format.']; - } else if (!isNewCommunityId && value !== slugifyPreserveDashes(value)) { - setOriginalValueValidated(false); - return ['failure', 'Incorrect format']; - } + const [newCommunityId, setNewCommunityId] = useState(''); + const debouncedNewCommunityId = useDebounce( + newCommunityId, + 500, + ); + const { data: newCommunity, isLoading: isLoadingNewCommunity } = + useGetCommunityByIdQuery({ + id: debouncedNewCommunityId || '', + enabled: !!debouncedNewCommunityId, + }); - if (isNewCommunityId) { - setNewValueValidated(true); - } else setOriginalValueValidated(true); - return []; - }; + const originalCommunityNotFound = + !isLoadingOriginalCommunity && + Object.keys(originalCommunity || {})?.length === 0; + const newCommunityNameAlreadyExists = + !isLoadingNewCommunity && Object.keys(newCommunity || {})?.length > 0; + const isNewCommunityNameValid = slugifyPreserveDashes(newCommunityId || ''); return (
@@ -88,27 +83,35 @@ const UpdateCommunityIdTask = () => {
{ - setOriginalCommunityValue(e.target.value); - if (e.target.value.length === 0) setOriginalValueValidated(false); - }} - inputValidationFn={(value) => validateFn(value, false)} + value={originalCommunityId} + onInput={(e) => + setOriginalCommunityId(e?.target?.value?.trim() || '') + } + customError={originalCommunityNotFound ? 'Community not found' : ''} placeholder="Current community id" + fullWidth /> { - setNewCommunityValue(e.target.value); - if (e.target.value.length === 0) setNewValueValidated(false); - }} - inputValidationFn={(value) => validateFn(value, true)} + value={newCommunityId} + onInput={(e) => setNewCommunityId(e?.target?.value?.trim() || '')} + customError={ + newCommunityNameAlreadyExists + ? 'Community name already exists, please use a different name' + : '' + } placeholder="New community id" + fullWidth />
diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCustomDomainTask.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCustomDomainTask.tsx index 025341e3572..a6d272c7be3 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCustomDomainTask.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/UpdateCustomDomainTask.tsx @@ -1,20 +1,17 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import React, { useState } from 'react'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; +import { useDebounce } from 'usehooks-ts'; import { CWText } from 'views/components/component_kit/cw_text'; -import { ValidationStatus } from 'views/components/component_kit/cw_validation_text'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; import { updateCommunityCustomDomain } from 'views/pages/AdminPanel/utils'; -import { CWTextInput } from '../../components/component_kit/cw_text_input'; +import { CWTextInput } from '../../components/component_kit/new_designs/CWTextInput'; import { openConfirmation } from '../../modals/confirmation_modal'; const UpdateCustomDomainTask = () => { const [communityId, setCommunityId] = useState(''); - const [communityIdValidated, setCommunityIdValidated] = - useState(false); const [customDomain, setCustomDomain] = useState(''); - const [customDomainValidated, setCustomDomainValidated] = - useState(false); const openConfirmationModal = () => { openConfirmation({ @@ -52,50 +49,43 @@ const UpdateCustomDomainTask = () => { }); }; - const validateCommunityFn = ( - value: string, - ): [ValidationStatus, string] | [] => { - const communityExists = app.config.chains.getById(value); - if (!communityExists) { - setCommunityIdValidated(false); - return ['failure', 'Community not found']; - } - setCommunityIdValidated(true); - return []; - }; + const debouncedCommunityId = useDebounce( + communityId, + 500, + ); + + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: debouncedCommunityId || '', + enabled: !!debouncedCommunityId, + }); + const communityNotFound = + !isLoadingCommunity && Object.keys(community || {})?.length === 0; + + const customDomainValidatonError = (() => { + if (!customDomain) return 'Required'; - const validateCustomDomain = ( - value: string, - ): [ValidationStatus, string] | [] => { const validCustomDomainUrl = new RegExp( '^(([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}$', ); // TODO: enhance this validation to ensure a tighter format (no dangling paths) - if (!validCustomDomainUrl.test(value)) { - setCustomDomainValidated(false); - return ['failure', 'Invalid URL (try removing the http prefix)']; + if (!validCustomDomainUrl.test(customDomain)) { + return 'Invalid URL (try removing the http prefix)'; } // there's probably a better way to remove prefixes for duplicate finding purposes const existingCustomDomain = app.config.chains .getAll() .find( - (community) => - community.customDomain && - community.customDomain - .replace('https://', '') - .replace('http://', '') === value, + (c) => + c.customDomain && + c.customDomain.replace('https://', '').replace('http://', '') === + customDomain, ); if (existingCustomDomain) { - setCustomDomainValidated(false); - return [ - 'failure', - `Custom domain in use by community '${existingCustomDomain.id}'`, - ]; + return `Custom domain in use by community '${existingCustomDomain.id}'`; } - setCustomDomainValidated(true); - return []; - }; + })(); return (
@@ -106,29 +96,25 @@ const UpdateCustomDomainTask = () => {
{ - setCommunityId(e.target.value); - if (e.target.value.length === 0) setCommunityIdValidated(false); - }} - inputValidationFn={(value) => validateCommunityFn(value)} - placeholder="dydx" + value={communityId} + onInput={(e) => setCommunityId(e?.target?.value?.trim() || '')} + customError={communityNotFound ? 'Community not found' : ''} + placeholder="Enter a community id" + fullWidth /> { - setCustomDomain(e.target.value); - if (e.target.value.length === 0) setCustomDomainValidated(false); - }} - inputValidationFn={(value) => validateCustomDomain(value)} + value={customDomain} + onInput={(e) => setCustomDomain(e?.target?.value?.trim() || '')} + customError={customDomain && customDomainValidatonError} placeholder="my.customdomain.com" + fullWidth />
diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/Allowlist/Allowlist.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/Allowlist/Allowlist.tsx index ba8a5c85e6b..de192e6f56a 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/Allowlist/Allowlist.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/Allowlist/Allowlist.tsx @@ -4,6 +4,7 @@ import { formatAddressShort } from 'helpers'; import { APIOrderDirection } from 'helpers/constants'; import React, { useMemo, useState } from 'react'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { useRefreshMembershipQuery } from 'state/api/groups'; import useUserStore from 'state/ui/user'; import { useDebounce } from 'usehooks-ts'; @@ -89,8 +90,11 @@ const Allowlist = ({ apiEnabled: !!user.activeAccount?.address, }); - const isStakedCommunity = !!app.config.chains.getById(app.activeChainId()) - .namespace; + const { data: community } = useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + const isStakedCommunity = !!community?.namespace; const tableState = useCWTableState({ columns: tableColumns(isStakedCommunity), @@ -117,6 +121,7 @@ const Allowlist = ({ membersPerPage: MEMBERS_PER_PAGE, page: currentPage, allowedAddresses: memoizedAddresses, + isStakedEnabled: isStakedCommunity, }); const handlePageChange = (_e, page: number) => { diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/GroupForm.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/GroupForm.tsx index b51acc8bf7b..0941f6a3b57 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/GroupForm.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/common/GroupForm/GroupForm.tsx @@ -1,5 +1,4 @@ /* eslint-disable react/no-multi-comp */ -import CWPageLayout from 'client/scripts/views/components/component_kit/new_designs/CWPageLayout'; import { isValidEthAddress } from 'helpers/validateTypes'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useEffect, useMemo, useState } from 'react'; @@ -11,6 +10,7 @@ import { CWText } from 'views/components/component_kit/cw_text'; import { CWTextArea } from 'views/components/component_kit/cw_text_area'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; import { CWForm } from 'views/components/component_kit/new_designs/CWForm'; +import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import { CWSelectList } from 'views/components/component_kit/new_designs/CWSelectList'; import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextInput'; import { MessageRow } from 'views/components/component_kit/new_designs/CWTextInput/MessageRow'; diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx index 1ba9d2f925e..745625097b9 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx @@ -10,6 +10,7 @@ import { MixpanelPageViewEventPayload, } from 'shared/analytics/types'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { ApiEndpoints, queryClient } from 'state/api/config'; import { useFetchGroupsQuery, @@ -90,8 +91,11 @@ const CommunityMembersPage = () => { 500, ); - const isStakedCommunity = !!app.config.chains.getById(app.activeChainId()) - .namespace; + const { data: community } = useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + const isStakedCommunity = !!community?.namespace; const columns: CWTableColumnInfo[] = [ { @@ -163,8 +167,7 @@ const CommunityMembersPage = () => { }), include_group_ids: true, // only include stake balances if community has staking enabled - include_stake_balances: !!app.config.chains.getById(app.activeChainId()) - .namespace, + include_stake_balances: !!community?.namespace, }, { initialCursor: 1, diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/common/useMemberData.ts b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/common/useMemberData.ts index 623e3dd9af6..5109fa30293 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/common/useMemberData.ts +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/common/useMemberData.ts @@ -14,6 +14,7 @@ interface UseMemberDataProps { membersPerPage: number; page: number; allowedAddresses: string[]; + isStakedEnabled: boolean; } export const useMemberData = ({ @@ -24,6 +25,7 @@ export const useMemberData = ({ membersPerPage, page, allowedAddresses, + isStakedEnabled, }: UseMemberDataProps) => { const user = useUserStore(); const membershipsFilter = (() => { @@ -51,8 +53,7 @@ export const useMemberData = ({ cursor: page, allowedAddresses: allowedAddresses.join(', '), // only include stake balances if community has staking enabled - include_stake_balances: !!app.config.chains.getById(app.activeChainId()) - ?.namespace, + include_stake_balances: isStakedEnabled, }); const { data: groups } = useFetchGroupsQuery({ diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/CommunityProfile/CommunityProfileForm/CommunityProfileForm.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/CommunityProfile/CommunityProfileForm/CommunityProfileForm.tsx index f6db94faad4..212beeaf6d2 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/CommunityProfile/CommunityProfileForm/CommunityProfileForm.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/CommunityProfile/CommunityProfileForm/CommunityProfileForm.tsx @@ -1,13 +1,16 @@ import { DefaultPage } from '@hicommonwealth/shared'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import { linkValidationSchema } from 'helpers/formValidations/common'; -import getLinkType from 'helpers/linkType'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { getLinkType, isLinkValid } from 'helpers/link'; +import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; +import React, { useCallback, useState } from 'react'; import { slugifyPreserveDashes } from 'shared/utils'; import app from 'state'; import { useEditCommunityBannerMutation, useEditCommunityTagsMutation, + useGetCommunityByIdQuery, + useUpdateCommunityMutation, } from 'state/api/communities'; import { LinksArray, useLinksArray } from 'views/components/LinksArray'; import { @@ -22,21 +25,18 @@ import { CWIcon } from 'views/components/component_kit/cw_icons/cw_icon'; import { CWText } from 'views/components/component_kit/cw_text'; import { CWTextArea } from 'views/components/component_kit/cw_text_area'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; +import CWCircleMultiplySpinner from 'views/components/component_kit/new_designs/CWCircleMultiplySpinner'; import { CWForm } from 'views/components/component_kit/new_designs/CWForm'; import { CWTag } from 'views/components/component_kit/new_designs/CWTag'; import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextInput'; import { CWRadioButton } from 'views/components/component_kit/new_designs/cw_radio_button'; import { CWToggle } from 'views/components/component_kit/new_designs/cw_toggle'; +import ErrorPage from '../../../error'; import './CommunityProfileForm.scss'; import { FormSubmitValues } from './types'; import { communityProfileValidationSchema } from './validation'; const CommunityProfileForm = () => { - const community = app.config.chains.getById(app.activeChainId()); - - const [communityId] = useState( - slugifyPreserveDashes(community.id.toLowerCase()), - ); // `formKey` remounts the CWForm with new community default values after a // successful update, using the updated formKey. const [formKey, setFormKey] = useState(1); @@ -44,22 +44,31 @@ const CommunityProfileForm = () => { isDisabled: true, canDisable: true, }); - const [isCustomStagesEnabled, setIsCustomStagesEnabled] = useState( - community.stagesEnabled, - ); + const [isCustomStagesEnabled, setIsCustomStagesEnabled] = useState(); const [isProcessingProfileImage, setIsProcessingProfileImage] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const [initialLinks, setInitialLinks] = useState( - (community.socialLinks || []).map((link) => ({ - value: link, - canUpdate: true, - canDelete: true, - error: '', - })), - ); + const [initialLinks, setInitialLinks] = useState< + { + value: string; + canUpdate: boolean; + canDelete: boolean; + error: string; + }[] + >([]); + + const { data: community, isLoading: isCommunityLoading } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + + const { mutateAsync: editBanner } = useEditCommunityBannerMutation(); + const { mutateAsync: editTags } = useEditCommunityTagsMutation(); + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: community?.id || '', + }); - const areTagsInitiallySet = useRef(false); const { isLoadingTags, preferenceTags, @@ -67,35 +76,6 @@ const CommunityProfileForm = () => { toggleTagFromSelection, } = usePreferenceTags(); - const updatePreferenceTags = useCallback(() => { - const updatedTags = [...preferenceTags].map((tag) => ({ - ...tag, - isSelected: !!(community.CommunityTags || []).find( - (t) => t.id === tag.item.id, - ), - })); - setPreferenceTags(updatedTags); - }, [preferenceTags, setPreferenceTags, community]); - - useEffect(() => { - if ( - !isLoadingTags && - preferenceTags?.length > 0 && - community && - !areTagsInitiallySet.current - ) { - updatePreferenceTags(); - - areTagsInitiallySet.current = true; - } - }, [ - isLoadingTags, - preferenceTags, - setPreferenceTags, - community, - updatePreferenceTags, - ]); - const { links, setLinks, @@ -108,11 +88,44 @@ const CommunityProfileForm = () => { linkValidation: linkValidationSchema.required, }); - const { mutateAsync: editBanner } = useEditCommunityBannerMutation(); - const { mutateAsync: editTags } = useEditCommunityTagsMutation(); + const updatePreferenceTags = useCallback(() => { + const updatedTags = [...preferenceTags].map((tag) => ({ + ...tag, + isSelected: !!(community?.CommunityTags || []).find( + (t) => t.tag_id === tag.item.id, + ), + })); + setPreferenceTags(updatedTags); + }, [preferenceTags, setPreferenceTags, community]); + + useRunOnceOnCondition({ + callback: () => { + setIsCustomStagesEnabled(community?.stages_enabled); + + const linksFromCommunity = (community?.social_links || []).map( + (link) => ({ + value: link, + canUpdate: true, + canDelete: true, + error: '', + }), + ); + setLinks(linksFromCommunity); + setInitialLinks(linksFromCommunity); + + preferenceTags?.length > 0 && updatePreferenceTags(); + }, + shouldRun: + !isCommunityLoading && !!community && preferenceTags && !isLoadingTags, + }); + + const communityIdForUrl = slugifyPreserveDashes( + community?.id?.toLowerCase() || '', + ); const onSubmit = async (values: FormSubmitValues) => { if ( + !community?.id || isSubmitting || (links.filter((x) => x.value).length > 0 ? !areLinksValid() : false) ) { @@ -134,10 +147,11 @@ const CommunityProfileForm = () => { bannerText: values.communityBanner ?? '', }); - await community.updateChainData({ + await updateCommunity({ + communityId: community.id, name: values.communityName, description: values.communityDescription, - social_links: links.map((link) => link.value.trim()), + socialLinks: links.map((link) => link.value.trim()), stagesEnabled: values.hasStagesEnabled, customStages: values.customStages ? JSON.parse(values.customStages) @@ -169,21 +183,33 @@ const CommunityProfileForm = () => { } }; + if (isCommunityLoading || isLoadingTags) { + return ; + } + + if (!community) { + return ( +
+ +
+ ); + } + return ( 0 - ? JSON.stringify(community.customStages) + (community?.custom_stages || []).length > 0 + ? JSON.stringify(community?.custom_stages || []) : '', communityBanner: community.communityBanner || '', }} @@ -204,7 +230,7 @@ const CommunityProfileForm = () => { setNameFieldDisabledState((prevVal) => ({ ...prevVal, canDisable: - e?.target?.value?.trim() === community.name.trim(), + e?.target?.value?.trim() === community?.name?.trim(), })); }} iconRight={ @@ -233,7 +259,7 @@ const CommunityProfileForm = () => { fullWidth label="Community URL" placeholder="Community URL" - value={`${window.location.origin}/${communityId}`} + value={`${window.location.origin}/${communityIdForUrl}`} /> { fullWidth label="Community Namespace" placeholder="Community Namespace" - value={community.namespace} + value={community?.namespace || ''} /> { links={links.map((link) => ({ ...link, customElementAfterLink: - link.value && getLinkType(link.value, 'website') ? ( + link.value && isLinkValid(link.value) ? ( @@ -393,13 +419,13 @@ const CommunityProfileForm = () => { type="button" disabled={ !formState.isDirty && - (community.CommunityTags || []).length === + (community?.CommunityTags || []).length === preferenceTags.filter(({ isSelected }) => isSelected) .length && links.filter((x) => x.value).length === - (community.socialLinks || []).length && + (community?.social_links || []).length && links.every((x) => - (community.socialLinks || []).includes(x.value.trim()), + (community?.social_links || []).includes(x.value.trim()), ) } onClick={() => { diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Contests/ContestsList/ContestCard/ContestCard.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Contests/ContestsList/ContestCard/ContestCard.tsx index cf71254bb98..d6898e256db 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Contests/ContestsList/ContestCard/ContestCard.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Contests/ContestsList/ContestCard/ContestCard.tsx @@ -100,8 +100,8 @@ const ContestCard = ({ const { data: oneOffContestBalance } = useGetContestBalanceQuery({ contestAddress: address, - chainRpc: app.chain.meta.ChainNode.url, - ethChainId: app.chain.meta.ChainNode.ethChainId!, + chainRpc: app.chain.meta?.ChainNode?.url, + ethChainId: app.chain.meta?.ChainNode?.ethChainId || 0, apiEnabled: !isRecurring, }); diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/CustomTOS/CustomTOS.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/CustomTOS/CustomTOS.tsx index 85856e50a7c..72158a3a3e0 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/CustomTOS/CustomTOS.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/CustomTOS/CustomTOS.tsx @@ -1,7 +1,12 @@ import { notifySuccess } from 'controllers/app/notifications'; import { linkValidationSchema } from 'helpers/formValidations/common'; +import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import React, { useCallback, useState } from 'react'; import app from 'state'; +import { + useGetCommunityByIdQuery, + useUpdateCommunityMutation, +} from 'state/api/communities'; import { CWText } from 'views/components/component_kit/cw_text'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextInput'; @@ -9,9 +14,28 @@ import { ZodError } from 'zod'; import './CustomTOS.scss'; const CustomTOS = () => { - const [community] = useState(app.config.chains.getById(app.activeChainId())); + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: community?.id || '', + }); + + useRunOnceOnCondition({ + callback: () => { + setTerms({ + value: community?.terms || '', + error: '', + }); + }, + shouldRun: !isLoadingCommunity && !!community, + }); + const [terms, setTerms] = useState({ - value: community.terms || '', + value: '', error: '', }); const [isSaving, setIsSaving] = useState(false); @@ -33,12 +57,13 @@ const CustomTOS = () => { }, []); const onSaveChanges = useCallback(async () => { - if (isSaving || terms.error) return; + if (isSaving || terms.error || !community?.id) return; setIsSaving(true); try { - await community.updateChainData({ - terms: terms.value, + await updateCommunity({ + communityId: community?.id, + terms: terms.value || '', }); notifySuccess('TOS link updated!'); @@ -47,7 +72,7 @@ const CustomTOS = () => { } finally { setIsSaving(false); } - }, [isSaving, terms, community]); + }, [isSaving, terms, community, updateCommunity]); return (
diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Directory/Directory.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Directory/Directory.tsx index dfeb7cc348e..8713fa3c38a 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Directory/Directory.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Directory/Directory.tsx @@ -1,8 +1,13 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import useAppStatus from 'hooks/useAppStatus'; +import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useCallback, useState } from 'react'; import app from 'state'; +import { + useGetCommunityByIdQuery, + useUpdateCommunityMutation, +} from 'state/api/communities'; import { useFetchNodesQuery } from 'state/api/nodes'; import { CWLabel } from 'views/components/component_kit/cw_label'; import { CWText } from 'views/components/component_kit/cw_text'; @@ -22,16 +27,31 @@ const Directory = () => { const chainNodeOptionsSorted = (chainNodeOptions || []).sort((a, b) => a.label.localeCompare(b.label), ); - const communityDefaultChainNodeId = app.chain.meta.ChainNode.id; + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + includeNodeInfo: true, + }); + + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: community?.id || '', + }); + const communityDefaultChainNodeId = community?.ChainNode?.id; const navigate = useCommonNavigate(); - const [community] = useState(app.config.chains.getById(app.activeChainId())); - const [isEnabled, setIsEnabled] = useState(community.directoryPageEnabled); - const [chainNodeId, setChainNodeId] = useState( - community.directoryPageChainNodeId, - ); + const [isEnabled, setIsEnabled] = useState(false); + const [chainNodeId, setChainNodeId] = useState(); const [isSaving, setIsSaving] = useState(false); + useRunOnceOnCondition({ + callback: () => { + setIsEnabled(!!community?.directory_page_enabled); + setChainNodeId(community?.directory_page_chain_node_id); + }, + shouldRun: !isLoadingCommunity && !!community, + }); + const { isAddedToHomeScreen } = useAppStatus(); const defaultChainNodeId = chainNodeId ?? communityDefaultChainNodeId; @@ -42,16 +62,19 @@ const Directory = () => { const onSaveChanges = useCallback(async () => { if ( isSaving || - (isEnabled === community.directoryPageEnabled && - chainNodeId === community.directoryPageChainNodeId) + !community?.id || + (isEnabled === community?.directory_page_enabled && + chainNodeId === community?.directory_page_chain_node_id) ) return; try { setIsSaving(true); - await community.updateChainData({ - directory_page_enabled: isEnabled, - directory_page_chain_node_id: chainNodeId, + + await updateCommunity({ + communityId: community?.id, + directoryPageChainNodeId: chainNodeId || undefined, + directoryPageEnabled: isEnabled, isPWA: isAddedToHomeScreen, }); @@ -62,7 +85,16 @@ const Directory = () => { } finally { setIsSaving(false); } - }, [community, isEnabled, chainNodeId, isSaving, isAddedToHomeScreen]); + }, [ + isSaving, + chainNodeId, + community?.id, + community?.directory_page_enabled, + community?.directory_page_chain_node_id, + isEnabled, + isAddedToHomeScreen, + updateCommunity, + ]); return (
@@ -98,7 +130,7 @@ const Directory = () => { { className="cta-button-container" > { buttonType="secondary" label="Save Changes" disabled={ - isEnabled === community.directoryPageEnabled && - chainNodeId === community.directoryPageChainNodeId + (isEnabled === community?.directory_page_enabled && + chainNodeId === community?.directory_page_chain_node_id) || + isLoadingCommunity } onClick={onSaveChanges} /> diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/Discord.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/Discord.tsx index 418344aed85..cfe78b9da9f 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/Discord.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/Discord.tsx @@ -2,6 +2,10 @@ import { notifyError, notifySuccess } from 'controllers/app/notifications'; import { uuidv4 } from 'lib/util'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import app from 'state'; +import { + useGetCommunityByIdQuery, + useUpdateCommunityMutation, +} from 'state/api/communities'; import { useFetchDiscordChannelsQuery, useRemoveDiscordBotConfigMutation, @@ -21,9 +25,15 @@ const CTA_TEXT = { }; const Discord = () => { - const [community] = useState(app.config.chains.getById(app.activeChainId())); + const { data: community } = useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: community?.id || '', + }); const [connectionStatus, setConnectionStatus] = useState( - community.discordConfigId ? 'connected' : 'none', + community?.discord_config_id ? 'connected' : 'none', ); const queryParams = useMemo(() => { @@ -35,7 +45,7 @@ const Discord = () => { app.chain.meta.discordConfigId = queryParams.get('discordConfigId'); } const [isDiscordWebhooksEnabled, setIsDiscordWebhooksEnabled] = useState( - community.discordBotWebhooksEnabled, + community?.discord_bot_webhooks_enabled, ); const { data: discordChannels } = useFetchDiscordChannelsQuery({ chainId: app.activeChainId(), @@ -50,7 +60,7 @@ const Discord = () => { } = useRemoveDiscordBotConfigMutation(); useEffect(() => { - if (community.discordConfigId) { + if (community?.discord_config_id) { setConnectionStatus('connected'); } }, [community]); @@ -110,11 +120,13 @@ const Discord = () => { }, [connectionStatus, queryParams, removeDiscordBotConfig]); const onToggleWebhooks = useCallback(async () => { + if (!community?.id) return; const toggleMsgType = isDiscordWebhooksEnabled ? 'disable' : 'enable'; try { - await community.updateChainData({ - discord_bot_webhooks_enabled: !isDiscordWebhooksEnabled, + await updateCommunity({ + communityId: community?.id, + discordBotWebhooksEnabled: !isDiscordWebhooksEnabled, }); setIsDiscordWebhooksEnabled(!isDiscordWebhooksEnabled); @@ -122,7 +134,7 @@ const Discord = () => { } catch (e) { notifyError(e || `Failed to ${toggleMsgType} discord webhooks!`); } - }, [isDiscordWebhooksEnabled, community]); + }, [community?.id, isDiscordWebhooksEnabled, updateCommunity]); const onConnectDiscordChannel = async ( channelId: string, @@ -207,7 +219,7 @@ const Discord = () => {
diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Snapshots/Snapshots.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Snapshots/Snapshots.tsx index 748f87f7c5b..de01c4cc966 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Snapshots/Snapshots.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Snapshots/Snapshots.tsx @@ -1,6 +1,11 @@ import { notifySuccess } from 'controllers/app/notifications'; -import React, { useState } from 'react'; +import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; +import React from 'react'; import app from 'state'; +import { + useGetCommunityByIdQuery, + useUpdateCommunityMutation, +} from 'state/api/communities'; import _ from 'underscore'; import { LinksArray, useLinksArray } from 'views/components/LinksArray'; import { CWText } from 'views/components/component_kit/cw_text'; @@ -9,10 +14,31 @@ import './Snapshots.scss'; import { snapshotValidationSchema } from './validation'; const Snapshots = () => { - const community = app.config.chains.getById(app.activeChainId()); - const [hasExistingSnapshots, setHasExistingSnapshots] = useState( - community.snapshot.length > 0, - ); + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + }); + + useRunOnceOnCondition({ + callback: () => { + setLinks( + (community?.snapshot_spaces || []).map((x) => ({ + value: x, + error: '', + canDelete: true, + canUpdate: true, + })), + ); + }, + shouldRun: !isLoadingCommunity && !!community, + }); + + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId: community?.id || '', + }); + + const hasExistingSnapshots = (community?.snapshot_spaces || [])?.length > 0; const { links: snapshots, setLinks, @@ -21,17 +47,12 @@ const Snapshots = () => { onLinkUpdatedAtIndex, areLinksValid, } = useLinksArray({ - initialLinks: (community.snapshot || []).map((x) => ({ - value: x, - error: '', - canDelete: true, - canUpdate: true, - })), + initialLinks: [], linkValidation: snapshotValidationSchema, }); const onSaveChanges = async () => { - if (!areLinksValid()) return; + if (!areLinksValid() || !community?.id) return; try { // get unique snapshot names from links (if any value in array was link) @@ -46,9 +67,12 @@ const Snapshots = () => { }), ), ]; - await community.updateChainData({ + + await updateCommunity({ + communityId: community?.id, snapshot: newSnapshots, }); + setLinks( newSnapshots.map((snapshot) => ({ value: snapshot, @@ -60,7 +84,6 @@ const Snapshots = () => { notifySuccess('Snapshot links updated!'); app.sidebarRedraw.emit('redraw'); - setHasExistingSnapshots(newSnapshots.length > 0); } catch { notifySuccess('Failed to update snapshot links!'); } @@ -96,7 +119,9 @@ const Snapshots = () => { [...snapshots.map((x) => x.value.trim())].sort((a, b) => a.localeCompare(b), ), - [...(community.snapshot || [])].sort((a, b) => a.localeCompare(b)), + [...(community?.snapshot_spaces || [])].sort((a, b) => + a.localeCompare(b), + ), )} onClick={onSaveChanges} /> diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Webhooks/Webhooks.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Webhooks/Webhooks.tsx index 2ed1754de8a..2032cbfc258 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Webhooks/Webhooks.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Webhooks/Webhooks.tsx @@ -2,7 +2,7 @@ import { WebhookCategory } from '@hicommonwealth/shared'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import { pluralizeWithoutNumberPrefix } from 'helpers'; import { linkValidationSchema } from 'helpers/formValidations/common'; -import getLinkType from 'helpers/linkType'; +import { getLinkType, isLinkValid } from 'helpers/link'; import useNecessaryEffect from 'hooks/useNecessaryEffect'; import Webhook from 'models/Webhook'; import React, { useState } from 'react'; @@ -23,7 +23,7 @@ import { WebhookSettingsModal } from 'views/modals/webhook_settings_modal'; import './Webhooks.scss'; const Webhooks = () => { - const community = app.config.chains.getById(app.activeChainId()); + const communityId = app.activeChainId(); const [hasExistingWebhooks, setHasExistingWebhooks] = useState(false); const [webhookToConfigure, setWebhookToConfigure] = useState( null, @@ -45,7 +45,7 @@ const Webhooks = () => { const { data: existingWebhooks, isLoading: isLoadingWebhooks } = useFetchWebhooksQuery({ - communityId: community.id, + communityId: communityId, }); const { mutateAsync: deleteWebhook, isLoading: isDeletingWebhook } = @@ -80,7 +80,7 @@ const Webhooks = () => { await Promise.all( webhooksToCreate.map(async (webhook) => { await createWebhook({ - communityId: community.id, + communityId: communityId, webhookUrl: webhook.value.trim(), }); }), @@ -116,7 +116,7 @@ const Webhooks = () => { webhooks[index].canConfigure ) { await deleteWebhook({ - communityId: community.id, + communityId: communityId, webhookUrl: webhooks[index].value, }); notifySuccess('Webhook deleted!'); @@ -136,7 +136,7 @@ const Webhooks = () => { try { await editWebhook({ - communityId: community.id, + communityId: communityId, webhookId: webhook.id, webhookCategories: categories, }); @@ -173,7 +173,7 @@ const Webhooks = () => { canDelete: !isDeletingWebhook, canConfigure: webhook.canConfigure ? !isEditingWebhook : false, customElementAfterLink: - webhook.canConfigure && getLinkType(webhook.value) ? ( + webhook.canConfigure && isLinkValid(webhook.value) ? ( { const community = app.chain.meta; const communityChainId = `${ - community.ChainNode?.ethChainId || community.ChainNode?.cosmosChainId + community?.ChainNode?.ethChainId || community?.ChainNode?.cosmosChainId }`; const selectedAddress = user.addresses.find( (x) => x.address === user.activeAccount?.address && - x.community.id === community.id, + x.community?.id === community?.id, ); return ( diff --git a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Avatars.showcase.tsx b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Avatars.showcase.tsx index 9fd99fe1d98..58512c25ecc 100644 --- a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Avatars.showcase.tsx +++ b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Avatars.showcase.tsx @@ -1,7 +1,6 @@ import React from 'react'; import ghostImg from 'assets/img/ghost.svg'; -import ChainInfo from 'models/ChainInfo'; import { CWAvatar } from 'views/components/component_kit/cw_avatar'; import { CWAvatarGroup, @@ -130,19 +129,10 @@ const AvatarsShowcase = () => { Community Avatar
- - - - + + + +
); diff --git a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Cards.showcase.tsx b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Cards.showcase.tsx index c854bb451e0..898781ab76f 100644 --- a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Cards.showcase.tsx +++ b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Cards.showcase.tsx @@ -1,11 +1,18 @@ +import { ChainBase } from '@hicommonwealth/shared'; import React from 'react'; -import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { CWContentPageCard } from 'views/components/component_kit/CWContentPageCard'; import { CWCard } from 'views/components/component_kit/cw_card'; import { CWText } from 'views/components/component_kit/cw_text'; import { CWRelatedCommunityCard } from 'views/components/component_kit/new_designs/CWRelatedCommunityCard'; const CardsShowcase = () => { + const sampleCommunityId = 'dydx'; + const { data: community } = useGetCommunityByIdQuery({ + id: sampleCommunityId, + enabled: !!sampleCommunityId, + }); + return ( <> Card @@ -37,7 +44,18 @@ const CardsShowcase = () => { Related Community Card diff --git a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Tags.showcase.tsx b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Tags.showcase.tsx index 8e4c650a438..1ca611b3e53 100644 --- a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Tags.showcase.tsx +++ b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/Tags.showcase.tsx @@ -1,12 +1,15 @@ import React, { useState } from 'react'; -import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { CWText } from 'views/components/component_kit/cw_text'; import { CWIdentificationTag } from 'views/components/component_kit/new_designs/CWIdentificationTag'; import { CWTag } from 'views/components/component_kit/new_designs/CWTag'; const TagsShowcase = () => { - const dydx = app.config.chains.getById('dydx'); - const [communityId, setCommunityId] = useState(dydx); + const [communityId, setCommunityId] = useState('dydx'); + const { data: community } = useGetCommunityByIdQuery({ + id: communityId || '', + enabled: !!communityId, + }); return ( <> @@ -47,12 +50,14 @@ const TagsShowcase = () => { Input
- {communityId && ( + {communityId && community && ( + community={{ + iconUrl: community.icon_url || '', + name: community.name || '', + }} onClick={() => setCommunityId(null)} /> )} diff --git a/packages/commonwealth/client/scripts/views/pages/CreateCommunity/steps/CommunityStakeStep/SignStakeTransactions/useReserveCommunityNamespace.tsx b/packages/commonwealth/client/scripts/views/pages/CreateCommunity/steps/CommunityStakeStep/SignStakeTransactions/useReserveCommunityNamespace.tsx index 735ce6e187b..1fe9846aaaa 100644 --- a/packages/commonwealth/client/scripts/views/pages/CreateCommunity/steps/CommunityStakeStep/SignStakeTransactions/useReserveCommunityNamespace.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CreateCommunity/steps/CommunityStakeStep/SignStakeTransactions/useReserveCommunityNamespace.tsx @@ -29,7 +29,10 @@ const useReserveCommunityNamespace = ({ useState(defaultActionState); const { namespaceFactory } = useNamespaceFactory(parseInt(chainId)); - const { mutateAsync: updateCommunity } = useUpdateCommunityMutation(); + const { mutateAsync: updateCommunity } = useUpdateCommunityMutation({ + communityId, + reInitAppOnSuccess: true, + }); const { isAddedToHomeScreen } = useAppStatus(); const user = useUserStore(); diff --git a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/CommunityDirectoryCard.tsx b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/CommunityDirectoryCard.tsx new file mode 100644 index 00000000000..ee3b52a94b6 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/CommunityDirectoryCard.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; +import useUserStore from 'state/ui/user'; +import { CWText } from 'views/components/component_kit/cw_text'; +import CWCircleMultiplySpinner from 'views/components/component_kit/new_designs/CWCircleMultiplySpinner'; +import { CWRelatedCommunityCard } from 'views/components/component_kit/new_designs/CWRelatedCommunityCard'; + +interface CommunityDirectoryCardProps { + communityId: string; + threadsCount: number | string; + membersCount: number | string; +} + +const CommunityDirectoryCard = ({ + communityId, + membersCount, + threadsCount, +}: CommunityDirectoryCardProps) => { + const user = useUserStore(); + + const { data: community, isLoading: isCommunityLoading } = + useGetCommunityByIdQuery({ + id: communityId, + enabled: !!communityId, + includeNodeInfo: true, + }); + + // allow user to buy stake if they have a connected address that matches active community base chain + const canBuyStake = !!user.addresses.find?.( + (address) => address?.community?.base === community?.base, + ); + + if (isCommunityLoading) return ; + + if (!community) return Failed to load community; + + return ( + + ); +}; + +export default CommunityDirectoryCard; diff --git a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPage.tsx b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPage.tsx index 8aa4b5f9dd9..61887b937fe 100644 --- a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPage.tsx @@ -4,6 +4,7 @@ import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useState } from 'react'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { useFetchNodesQuery } from 'state/api/nodes'; import { getNodeById } from 'state/api/nodes/utils'; import { useDebounce } from 'usehooks-ts'; @@ -20,6 +21,7 @@ import useDirectoryPageData, { import ErrorPage from 'views/pages/error'; import { MixpanelPageViewEvent } from '../../../../../shared/analytics/types'; import useAppStatus from '../../../hooks/useAppStatus'; +import CWCircleMultiplySpinner from '../../components/component_kit/new_designs/CWCircleMultiplySpinner'; import './DirectoryPage.scss'; const DirectoryPage = () => { @@ -30,15 +32,19 @@ const DirectoryPage = () => { const { data: nodes } = useFetchNodesQuery(); - const directoryPageEnabled = app.config.chains.getById( - app.activeChainId(), - )?.directoryPageEnabled; - const communityDefaultChainNodeId = app.chain.meta.ChainNode.id; - const selectedChainNodeId = app.config.chains.getById( - app.activeChainId(), - )?.directoryPageChainNodeId; + const { data: community, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: app.activeChainId(), + enabled: !!app.activeChainId(), + includeNodeInfo: true, + }); + const directoryPageEnabled = community?.directory_page_enabled; + const communityDefaultChainNodeId = community?.ChainNode?.id; + const selectedChainNodeId = community?.directory_page_chain_node_id; const defaultChainNodeId = selectedChainNodeId ?? communityDefaultChainNodeId; - const baseChain = getNodeById(defaultChainNodeId, nodes); + const baseChain = defaultChainNodeId + ? getNodeById(defaultChainNodeId, nodes) + : undefined; const { isAddedToHomeScreen } = useAppStatus(); @@ -65,6 +71,10 @@ const DirectoryPage = () => { }, }); + if (isLoadingCommunity) { + return ; + } + if (!directoryPageEnabled) { return ( diff --git a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPageContent.tsx b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPageContent.tsx index 5976b955c72..a8f73e5c2a7 100644 --- a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPageContent.tsx +++ b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/DirectoryPageContent.tsx @@ -1,15 +1,13 @@ import { APIOrderDirection } from 'helpers/constants'; import NodeInfo from 'models/NodeInfo'; import React from 'react'; -import app from 'state'; -import useUserStore from 'state/ui/user'; import { CWText } from 'views/components/component_kit/cw_text'; -import { CWRelatedCommunityCard } from 'views/components/component_kit/new_designs/CWRelatedCommunityCard'; import { CWTable } from 'views/components/component_kit/new_designs/CWTable'; import { ViewType } from 'views/pages/DirectoryPage/useDirectoryPageData'; import CWCircleMultiplySpinner from '../../components/component_kit/new_designs/CWCircleMultiplySpinner'; import { CWTableColumnInfo } from '../../components/component_kit/new_designs/CWTable/CWTable'; import { useCWTableState } from '../../components/component_kit/new_designs/CWTable/useCWTableState'; +import CommunityDirectoryCard from './CommunityDirectoryCard'; import './DirectoryPageContent.scss'; type RowType = { @@ -84,8 +82,6 @@ const DirectoryPageContent = ({ tableData, filteredRelatedCommunitiesData, }: DirectoryPageContentProps) => { - const user = useUserStore(); - const tableState = useCWTableState({ columns, initialSortColumn: 'members', @@ -126,20 +122,12 @@ const DirectoryPageContent = ({ ) : (
{filteredRelatedCommunitiesData.map((community) => { - const chain = app.config.chains.getById(community.id); - - // allow user to buy stake if they have a connected address that matches active community base chain - const canBuyStake = !!user.addresses.find?.( - (address) => address?.community?.base === chain?.base, - ); - return ( - ); })} diff --git a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/useDirectoryPageData.tsx b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/useDirectoryPageData.tsx index 41236dc3f65..b80c7a1d681 100644 --- a/packages/commonwealth/client/scripts/views/pages/DirectoryPage/useDirectoryPageData.tsx +++ b/packages/commonwealth/client/scripts/views/pages/DirectoryPage/useDirectoryPageData.tsx @@ -1,6 +1,5 @@ import { isCommandClick } from 'helpers'; import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; -import ChainInfo from 'models/ChainInfo'; import { navigateToCommunity, useCommonNavigate } from 'navigation/helpers'; import React, { useCallback, useMemo } from 'react'; import { useFetchRelatedCommunitiesQuery } from 'state/api/communities'; @@ -95,7 +94,7 @@ const useDirectoryPageData = ({ > {c.name} diff --git a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/MyCommunityStake.tsx b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/MyCommunityStake.tsx index 22c7405e615..36dde94f230 100644 --- a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/MyCommunityStake.tsx +++ b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/MyCommunityStake.tsx @@ -1,8 +1,6 @@ import { formatAddressShort } from 'helpers'; -import { getCommunityStakeSymbol } from 'helpers/stakes'; import useTransactionHistory from 'hooks/useTransactionHistory'; import React, { useState } from 'react'; -import app from 'state'; import useUserStore from 'state/ui/user'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import { CWIcon } from '../../components/component_kit/cw_icons/cw_icon'; @@ -57,12 +55,6 @@ const MyCommunityStake = () => { filterOptions, addressFilter, }); - const updatedData = data.map((info) => { - info.chain = getCommunityStakeSymbol( - app.config.chains.getById(info.community.id)?.ChainNode?.name || '', - ); - return info; - }); if (!user.isLoggedIn) { return ; @@ -124,11 +116,9 @@ const MyCommunityStake = () => { {activeTabIndex === 0 ? ( - // @ts-expect-error - + ) : ( - // @ts-expect-error - + )} )} diff --git a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Stakes/Stakes.tsx b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Stakes/Stakes.tsx index 08e7b59f6f6..6725eba244f 100644 --- a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Stakes/Stakes.tsx +++ b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Stakes/Stakes.tsx @@ -78,7 +78,7 @@ const Stakes = ({ transactions }: TransactionsProps) => { accumulatedStakes[key] = { ...transaction, ...(accumulatedStakes[key] || {}), - chain: transaction.chain, + chain: transaction?.community?.chain_node_name, stake: (accumulatedStakes[key]?.stake || 0) + transaction.stake * action, voteWeight: diff --git a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Transactions/Transactions.tsx b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Transactions/Transactions.tsx index 2e9e54e3afb..3b376521aad 100644 --- a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Transactions/Transactions.tsx +++ b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/Transactions/Transactions.tsx @@ -87,16 +87,12 @@ const Transactions = ({ transactions }: TransactionsProps) => { rowData={transactions.map((tx) => ({ ...tx, community: { - // @ts-expect-error - sortValue: tx.community.name.toLowerCase(), + sortValue: tx?.community?.name?.toLowerCase(), customElement: ( - iconUrl={tx.community.icon_url} - // @ts-expect-error + symbol={tx.community.default_symbol || ''} + iconUrl={tx.community.icon_url || ''} name={tx.community.name} - // @ts-expect-error communityId={tx.community.id} /> ), diff --git a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/types.ts b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/types.ts index a8210d226f2..3f498c23230 100644 --- a/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/types.ts +++ b/packages/commonwealth/client/scripts/views/pages/MyCommunityStake/types.ts @@ -6,11 +6,12 @@ export type FilterOptions = { export type TransactionsProps = { transactions: { community: { - id?: string; - default_symbol?: string; - icon_url?: string; - name?: string; - chain_node_id?: number; + id: string; + default_symbol?: string | null; + icon_url?: string | null; + name: string; + chain_node_id?: number | null; + chain_node_name?: string | null; }; address: string; price: string; @@ -21,6 +22,5 @@ export type TransactionsProps = { totalPrice: string; avgPrice: string; etherscanLink: string; - chain: string; }[]; }; diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommentSubscriptionEntry.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommentSubscriptionEntry.tsx index c9a339d6b64..e9b11660b68 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommentSubscriptionEntry.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommentSubscriptionEntry.tsx @@ -69,8 +69,8 @@ export const CommentSubscriptionEntry = (
diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommunityEntry.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommunityEntry.tsx index cc6f0b00735..96de18bcefe 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommunityEntry.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/CommunityEntry.tsx @@ -1,6 +1,5 @@ import { CommunityAlert } from '@hicommonwealth/schemas'; import { notifySuccess } from 'controllers/app/notifications'; -import type ChainInfo from 'models/ChainInfo'; import React, { useCallback, useState } from 'react'; import { useCreateCommunityAlertMutation } from 'state/api/trpc/subscription/useCreateCommunityAlertMutation'; import { useDeleteCommunityAlertMutation } from 'state/api/trpc/subscription/useDeleteCommunityAlertMutation'; @@ -9,14 +8,19 @@ import { CWToggle } from 'views/components/component_kit/cw_toggle'; import { z } from 'zod'; type CommunityEntryProps = Readonly<{ - communityInfo: ChainInfo; - communityAlert: z.infer; + id: string; + name: string; + iconUrl: string; + alert: z.infer; }>; -export const CommunityEntry = (props: CommunityEntryProps) => { - const { communityInfo, communityAlert } = props; - - const [subscribed, setSubscribed] = useState(!!communityAlert); +export const CommunityEntry = ({ + name, + iconUrl, + id, + alert, +}: CommunityEntryProps) => { + const [subscribed, setSubscribed] = useState(!!alert); const createCommunityAlert = useCreateCommunityAlertMutation(); @@ -27,13 +31,13 @@ export const CommunityEntry = (props: CommunityEntryProps) => { if (subscribed) { await deleteCommunityAlert.mutateAsync({ id: 0, // this should be the aggregate id (user?) - community_ids: [communityInfo.id], + community_ids: [id], }); notifySuccess('Unsubscribed!'); } else { await createCommunityAlert.mutateAsync({ id: 0, // this should be the aggregate id (user?) - community_id: communityInfo.id, + community_id: id, }); notifySuccess('Subscribed!'); } @@ -42,22 +46,13 @@ export const CommunityEntry = (props: CommunityEntryProps) => { } doAsync().catch(console.error); - }, [ - communityInfo.id, - createCommunityAlert, - deleteCommunityAlert, - subscribed, - ]); + }, [id, createCommunityAlert, deleteCommunityAlert, subscribed]); return ( -
+
- +
diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/ThreadSubscriptionEntry.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/ThreadSubscriptionEntry.tsx index 0cdc1f16c00..fff113eb7d7 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/ThreadSubscriptionEntry.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/ThreadSubscriptionEntry.tsx @@ -66,8 +66,8 @@ export const ThreadSubscriptionEntry = (
diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx index db8770cdd88..e06d68a7695 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx @@ -108,8 +108,10 @@ const NotificationSettings = () => { return ( ); })} diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/SubscriptionEntry.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/SubscriptionEntry.tsx new file mode 100644 index 00000000000..db3e619f5eb --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/SubscriptionEntry.tsx @@ -0,0 +1,76 @@ +import NotificationSubscription from 'models/NotificationSubscription'; +import 'pages/notification_settings/index.scss'; +import React from 'react'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; +import { CWCheckbox } from '../../components/component_kit/cw_checkbox'; +import { CWCommunityAvatar } from '../../components/component_kit/cw_community_avatar'; +import { CWText } from '../../components/component_kit/cw_text'; +import { CWToggle } from '../../components/component_kit/cw_toggle'; + +type SubscriptionEntryProps = { + communitId: string; + subscriptions: NotificationSubscription[]; + showSubscriptionsCount?: boolean; + canToggleEmailNotifications?: boolean; + areEmailNotificationsEnabled?: boolean; + onToggleReceiveEmailsNotifications: () => void; + canToggleInAppNotifications?: boolean; + areInAppNotificationsEnabled?: boolean; + onToggleReceiveInAppNotifications: () => void; +}; + +const SubscriptionEntry = ({ + communitId, + subscriptions, + showSubscriptionsCount, + canToggleEmailNotifications = true, + areEmailNotificationsEnabled, + onToggleReceiveEmailsNotifications, + areInAppNotificationsEnabled, + canToggleInAppNotifications = true, + onToggleReceiveInAppNotifications, +}: SubscriptionEntryProps) => { + const { data: communityInfo } = useGetCommunityByIdQuery({ + id: communitId, + enabled: !!communitId, + }); + + return ( +
+
+
+
+ + + {communityInfo?.name} + +
+ {showSubscriptionsCount && ( + + {subscriptions.length} subscriptions + + )} +
+ onToggleReceiveEmailsNotifications()} + /> + onToggleReceiveInAppNotifications()} + /> +
+
+ ); +}; + +export default SubscriptionEntry; diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/helper_components.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/helper_components.tsx index 4f8a78369d5..f0d16febc22 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/helper_components.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/helper_components.tsx @@ -7,8 +7,8 @@ import 'pages/notification_settings/helper_components.scss'; import NotificationSubscription from '../../../models/NotificationSubscription'; import { useCommonNavigate } from 'navigation/helpers'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { PopoverMenu } from 'views/components/component_kit/CWPopoverMenu'; -import app from '../../../state'; import { CWIconButton } from '../../components/component_kit/cw_icon_button'; import { CWIcon } from '../../components/component_kit/cw_icons/cw_icon'; import { CWText } from '../../components/component_kit/cw_text'; @@ -24,6 +24,7 @@ const getTextRows = ( options?: NavigateOptions, prefix?: null | string, ) => void, + communityName?: string, ) => { if (subscription.Thread) { const threadUrl = getNotificationUrlPath(subscription); @@ -117,8 +118,7 @@ const getTextRows = ( type={isWindowExtraSmall(window.innerWidth) ? 'caption' : 'b2'} fontWeight="bold" > - {/* @ts-expect-error StrictNullChecks*/} - {app.config.chains.getById(subscription.communityId)?.name} + {communityName}
); @@ -136,6 +136,11 @@ export const SubscriptionRowTextContainer = ({ }: SubscriptionRowProps) => { const navigate = useCommonNavigate(); + const { data: community } = useGetCommunityByIdQuery({ + id: subscription.communityId || '', + enabled: !!subscription.communityId, + }); + return (
- {getTextRows(subscription, navigate)} + {getTextRows(subscription, navigate, community?.name || '')}
); diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/index.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/index.tsx index ef1191b206d..9adcbcf46d6 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettingsOld/index.tsx @@ -16,7 +16,6 @@ import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayou import { CWCard } from '../../components/component_kit/cw_card'; import { CWCheckbox } from '../../components/component_kit/cw_checkbox'; import { CWCollapsible } from '../../components/component_kit/cw_collapsible'; -import { CWCommunityAvatar } from '../../components/component_kit/cw_community_avatar'; import { CWText } from '../../components/component_kit/cw_text'; import { CWTextInput } from '../../components/component_kit/cw_text_input'; import { CWToggle } from '../../components/component_kit/cw_toggle'; @@ -24,6 +23,7 @@ import { isWindowExtraSmall } from '../../components/component_kit/helpers'; import { CWButton } from '../../components/component_kit/new_designs/CWButton'; import { User } from '../../components/user/user'; import { PageLoading } from '../loading'; +import SubscriptionEntry from './SubscriptionEntry'; import { SubscriptionRowMenu, SubscriptionRowTextContainer, @@ -113,7 +113,9 @@ const NotificationSettingsPage = () => { onClick: () => { updateEmailSettings({ emailNotificationInterval: 'weekly', - }).catch(console.log); + }) + .then(() => undefined) + .catch(console.log); setCurrentFrequency('weekly'); forceRerender(); }, @@ -123,7 +125,9 @@ const NotificationSettingsPage = () => { onClick: () => { updateEmailSettings({ emailNotificationInterval: 'never', - }).catch(console.log); + }) + .then(() => undefined) + .catch(console.log); setCurrentFrequency('never'); forceRerender(); }, @@ -225,90 +229,58 @@ const NotificationSettingsPage = () => {
{relevantSubscribedCommunities .sort((x, y) => x.name.localeCompare(y.name)) - .map((community) => { + .map((community, index) => { return ( -
-
-
-
- - - {community.name} - -
-
- { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleEmailSubscriptions(false, []); - }} - /> - { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - app.user.notifications - .subscribe({ - categoryId: NotificationCategories.ChainEvent, - options: { communityId: community.id }, - }) - .then(() => { - forceRerender(); - }); - }} - /> -
-
+ { + handleEmailSubscriptions(false, []) + .then(() => undefined) + .catch(console.error); + }} + areInAppNotificationsEnabled={false} + onToggleReceiveInAppNotifications={() => { + app.user.notifications + .subscribe({ + categoryId: NotificationCategories.ChainEvent, + options: { communityId: community.id }, + }) + .then(() => { + forceRerender(); + }) + .catch(console.error); + }} + /> ); })} {Object.entries(chainEventSubs) .sort((x, y) => x[0].localeCompare(y[0])) - .map(([communityName, subs]) => { - const communityInfo = app.config.chains.getById(communityName); + .map(([communityId, subs], index) => { const hasSomeEmailSubs = subs.some((s) => s.immediateEmail); const hasSomeInAppSubs = subs.some((s) => s.isActive); - return ( -
-
-
-
- - - {communityInfo?.name} - -
-
- { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleEmailSubscriptions(hasSomeEmailSubs, subs); - }} - /> - { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleSubscriptions(hasSomeInAppSubs, subs); - }} - /> -
-
+ { + handleEmailSubscriptions(hasSomeEmailSubs, subs) + .then(() => undefined) + .catch(console.error); + }} + areInAppNotificationsEnabled={hasSomeInAppSubs} + onToggleReceiveInAppNotifications={() => { + handleSubscriptions(hasSomeInAppSubs, subs) + .then(() => undefined) + .catch(console.error); + }} + /> ); })} {
{Object.entries(bundledSubs) .sort((x, y) => x[0].localeCompare(y[0])) - .map(([communityName, subs]) => { - const communityInfo = app?.config.chains.getById(communityName); - const sortedSubs = subs.sort((a, b) => - a.category.localeCompare(b.category), - ); - const hasSomeEmailSubs = sortedSubs.some((s) => s.immediateEmail); - const hasSomeInAppSubs = sortedSubs.some((s) => s.isActive); - - if (!communityInfo?.id) return null; // handles incomplete loading case + .map(([communityId, subs], index) => { + const hasSomeEmailSubs = subs.some((s) => s.immediateEmail); + const hasSomeInAppSubs = subs.some((s) => s.isActive); return ( -
+
-
-
- - - {communityInfo?.name} - -
- - {subs.length} subscriptions - -
- - handleEmailSubscriptions(hasSomeEmailSubs, subs) - } - /> - - handleSubscriptions(hasSomeInAppSubs, subs) - } - /> -
+ { + handleEmailSubscriptions(hasSomeEmailSubs, subs) + .then(() => undefined) + .catch(console.error); + }} + areInAppNotificationsEnabled={hasSomeInAppSubs} + onToggleReceiveInAppNotifications={() => { + handleSubscriptions(hasSomeInAppSubs, subs) + .then(() => undefined) + .catch(console.error); + }} + /> } collapsibleContent={
@@ -571,15 +524,17 @@ const NotificationSettingsPage = () => { label="Receive Emails" checked={hasSomeEmailSubs} onChange={() => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleEmailSubscriptions(hasSomeEmailSubs, subs); + handleEmailSubscriptions(hasSomeEmailSubs, subs) + .then(() => undefined) + .catch(console.error); }} /> { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleSubscriptions(hasSomeInAppSubs, subs); + handleSubscriptions(hasSomeInAppSubs, subs) + .then(() => undefined) + .catch(console.error); }} />
diff --git a/packages/commonwealth/client/scripts/views/pages/communities.tsx b/packages/commonwealth/client/scripts/views/pages/communities.tsx index ac417f71045..e78ebfbd787 100644 --- a/packages/commonwealth/client/scripts/views/pages/communities.tsx +++ b/packages/commonwealth/client/scripts/views/pages/communities.tsx @@ -37,7 +37,7 @@ const buildCommunityString = (numCommunities: number) => // Handle mapping provided by ChainCategories table const communityCategories = Object.values(CommunityCategoryType); -const communityNetworks = Object.keys(ChainNetwork).filter( +const communityNetworks: string[] = Object.keys(ChainNetwork).filter( (val) => val === 'ERC20', ); // We only are allowing ERC20 for now const communityBases = Object.keys(ChainBase); @@ -71,9 +71,8 @@ const CommunitiesPage = () => { const { data: nodes } = useFetchNodesQuery(); - const [selectedCommunity, setSelectedCommunity] = - // @ts-expect-error - React.useState(null); + const [selectedCommunityId, setSelectedCommunityId] = + React.useState(); const oneDayAgo = useRef(new Date().getTime() - 24 * 60 * 60 * 1000); @@ -104,7 +103,7 @@ const CommunitiesPage = () => { return list.filter((data) => { const communityNetwork = Object.keys(ChainNetwork)[ - Object.values(ChainNetwork).indexOf(data.network) + Object.values(ChainNetwork).indexOf(data.network as ChainNetwork) ]; // Converts chain.base into a ChainBase key to match our filterMap keys if (communityNetworks.includes(communityNetwork)) { @@ -175,11 +174,22 @@ const CommunitiesPage = () => { return ( setSelectedCommunity(community)} + onStakeBtnClick={() => setSelectedCommunityId(community?.id || '')} ethUsdRate={ethUsdRate} historicalPrice={historicalPriceMap?.get(community.id)} onlyShowIfStakeEnabled={!!filterMap[STAKE_FILTER_KEY]} @@ -287,9 +297,8 @@ const CommunitiesPage = () => { mode={modeOfManageCommunityStakeModal} // @ts-expect-error onModalClose={() => setModeOfManageCommunityStakeModal(null)} - community={selectedCommunity} denomination={ - findDenominationString(selectedCommunity?.id) || 'ETH' + findDenominationString(selectedCommunityId || '') || 'ETH' } /> } diff --git a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/AuthorAndPublishInfo/AuthorAndPublishInfo.tsx b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/AuthorAndPublishInfo/AuthorAndPublishInfo.tsx index f9d5f8ceeda..ef8542eb634 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/AuthorAndPublishInfo/AuthorAndPublishInfo.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/AuthorAndPublishInfo/AuthorAndPublishInfo.tsx @@ -2,12 +2,13 @@ import { PopperPlacementType } from '@mui/base/Popper'; import { threadStageToLabel } from 'helpers'; import moment from 'moment'; import React, { useRef } from 'react'; -import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { ArchiveTrayWithTooltip } from 'views/components/ArchiveTrayWithTooltip'; import { LockWithTooltip } from 'views/components/LockWithTooltip'; import CommunityInfo from 'views/components/component_kit/CommunityInfo'; import { CWText } from 'views/components/component_kit/cw_text'; import { getClasses } from 'views/components/component_kit/helpers'; +import CWCircleMultiplySpinner from 'views/components/component_kit/new_designs/CWCircleMultiplySpinner'; import CWPopover, { usePopover, } from 'views/components/component_kit/new_designs/CWPopover'; @@ -109,19 +110,29 @@ export const AuthorAndPublishInfo = ({ })); const isCommunityFirstLayout = layoutType === 'community-first'; - const communtyInfo = app.config.chains.getById(authorCommunityId); + const { data: communtyInfo, isLoading: isLoadingCommunity } = + useGetCommunityByIdQuery({ + id: authorCommunityId, + enabled: !!authorCommunityId, + }); return (
{isCommunityFirstLayout && ( <> - - {dotIndicator} + {isLoadingCommunity ? ( + + ) : ( + <> + + {dotIndicator} + + )} )} ); + } const hasAdminPermissions = Permissions.isSiteAdmin() || - Permissions.isCommunityAdmin(thread.communityId) || - Permissions.isCommunityModerator(thread.communityId); + Permissions.isCommunityAdmin(community) || + Permissions.isCommunityModerator(community); const isThreadAuthor = Permissions.isThreadAuthor(thread); const isThreadCollaborator = Permissions.isThreadCollaborator(thread); diff --git a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx index 764b5c04960..fa999505a6a 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx @@ -255,10 +255,10 @@ export const AdminActions = ({ }; const handleSnapshotProposalClick = () => { - const snapshotSpaces = app.chain.meta.snapshot; + const snapshotSpaces = app.chain.meta?.snapshot; onSnapshotProposalFromThread && onSnapshotProposalFromThread(); navigate( - snapshotSpaces.length > 1 + snapshotSpaces?.length > 1 ? '/multiple-snapshots' : `/snapshot/${snapshotSpaces}`, ); @@ -372,7 +372,7 @@ export const AdminActions = ({ ], ...(canUpdateThread && (isThreadAuthor || hasAdminPermissions) ? [ - ...(app.chain?.meta.snapshot.length + ...(app.chain?.meta?.snapshot?.length ? [ { label: 'Snapshot proposal from thread', @@ -389,7 +389,7 @@ export const AdminActions = ({ iconLeft: 'democraticProposal' as const, iconLeftWeight: 'bold' as const, onClick: () => { - const snapshotSpaces = app.chain.meta.snapshot; + const snapshotSpaces = app?.chain?.meta?.snapshot; // @ts-expect-error StrictNullChecks onSnapshotProposalFromThread(); navigate( diff --git a/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx index ee00903be39..f59501c93d6 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx @@ -13,7 +13,7 @@ export default function DiscussionsRedirect() { useEffect(() => { if (!app.chain) return; - const view = app.chain.meta.defaultOverview + const view = app.chain.meta?.defaultOverview ? DefaultPage.Overview : DefaultPage.Discussions; diff --git a/packages/commonwealth/client/scripts/views/pages/notifications/helpers.tsx b/packages/commonwealth/client/scripts/views/pages/notifications/helpers.tsx index 049bddf2fb5..bdd9c5c669b 100644 --- a/packages/commonwealth/client/scripts/views/pages/notifications/helpers.tsx +++ b/packages/commonwealth/client/scripts/views/pages/notifications/helpers.tsx @@ -9,12 +9,15 @@ import _ from 'lodash'; import moment from 'moment'; import 'pages/notifications/notification_row.scss'; import React from 'react'; -import app from 'state'; import { getCommunityUrl } from 'utils'; import { User } from 'views/components/user/user'; import { QuillRenderer } from '../../components/react_quill_editor/quill_renderer'; -const getNotificationFields = (category, data: IForumNotificationData) => { +const getNotificationFields = ( + category, + data: IForumNotificationData, + communityName = '', +) => { const { created_at, thread_id, @@ -31,9 +34,6 @@ const getNotificationFields = (category, data: IForumNotificationData) => { let notificationHeader; let notificationBody; - const communityName = - app.config.chains.getById(community_id)?.name || 'Unknown chain'; - const decodedTitle = decodeURIComponent(root_title).trim(); if (comment_text) { @@ -112,9 +112,10 @@ const getNotificationFields = (category, data: IForumNotificationData) => { export const getBatchNotificationFields = ( category, data: IForumNotificationData[], + communityName: string = 'Unknown chain', ) => { if (data.length === 1) { - return getNotificationFields(category, data[0]); + return getNotificationFields(category, data[0], communityName); } const { @@ -136,9 +137,6 @@ export const getBatchNotificationFields = ( const length = authorInfo.length - 1; - const communityName = - app.config.chains.getById(community_id)?.name || 'Unknown chain'; - let notificationHeader; let notificationBody; const decodedTitle = decodeURIComponent(root_title).trim(); diff --git a/packages/commonwealth/client/scripts/views/pages/notifications/notification_row.tsx b/packages/commonwealth/client/scripts/views/pages/notifications/notification_row.tsx index fb725ebbcda..c501ba71cc3 100644 --- a/packages/commonwealth/client/scripts/views/pages/notifications/notification_row.tsx +++ b/packages/commonwealth/client/scripts/views/pages/notifications/notification_row.tsx @@ -2,6 +2,7 @@ import { NotificationCategories } from '@hicommonwealth/shared'; import React, { useEffect } from 'react'; import type Notification from '../../../models/Notification'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import { ChainEventNotificationRow, DefaultNotificationRow, @@ -12,6 +13,7 @@ export type NotificationRowProps = { notification: Notification; onListPage?: boolean; allRead: boolean; + communityName?: string; }; export const NotificationRow = (props: NotificationRowProps) => { @@ -27,11 +29,17 @@ export const NotificationRow = (props: NotificationRowProps) => { const category = notification.categoryId; + const { data: community } = useGetCommunityByIdQuery({ + id: notification.chainEvent?.chain || '', + enabled: !!notification.chainEvent?.chain, + }); + if (category === NotificationCategories.ChainEvent) { return ( ); } else if (category === NotificationCategories.SnapshotProposal) { @@ -50,6 +58,7 @@ export const NotificationRow = (props: NotificationRowProps) => { handleSetMarkingRead={handleSetMarkingRead} markingRead={markingRead} allRead={allRead} + communityName={community?.name} /> ); } diff --git a/packages/commonwealth/client/scripts/views/pages/notifications/notification_row_components.tsx b/packages/commonwealth/client/scripts/views/pages/notifications/notification_row_components.tsx index 203163ca487..b183635d607 100644 --- a/packages/commonwealth/client/scripts/views/pages/notifications/notification_row_components.tsx +++ b/packages/commonwealth/client/scripts/views/pages/notifications/notification_row_components.tsx @@ -22,11 +22,11 @@ import { UserGallery } from '../../components/user/user_gallery'; import { getBatchNotificationFields } from './helpers'; import type { NotificationRowProps } from './notification_row'; -export const ChainEventNotificationRow = ( - props: Omit, -) => { - const { notification, onListPage } = props; - +export const ChainEventNotificationRow = ({ + notification, + communityName, + onListPage, +}: Omit) => { const navigate = useCommonNavigate(); if (!notification.chainEvent) { @@ -46,8 +46,6 @@ export const ChainEventNotificationRow = ( data: notification.chainEvent.data, }; - const communityName = app.config.chains.getById(communityId)?.name; - let label: IEventLabel | undefined; try { label = ChainEventLabel(communityId, chainEvent); @@ -129,7 +127,13 @@ type ExtendedNotificationRowProps = NotificationRowProps & { // eslint-disable-next-line react/no-multi-comp export const DefaultNotificationRow = (props: ExtendedNotificationRowProps) => { - const { handleSetMarkingRead, markingRead, notification, allRead } = props; + const { + handleSetMarkingRead, + markingRead, + notification, + allRead, + communityName, + } = props; const [isRead, setIsRead] = useState(notification.isRead); const category = notification.categoryId; @@ -140,10 +144,14 @@ export const DefaultNotificationRow = (props: ExtendedNotificationRowProps) => { typeof notif.data === 'string' ? JSON.parse(notif.data) : notif.data, ); + const response = getBatchNotificationFields( + category, + notificationData, + communityName, + ); const { authorInfo, createdAt, notificationHeader, notificationBody } = - getBatchNotificationFields(category, notificationData); - - let { path } = getBatchNotificationFields(category, notificationData); + response; + let { path } = response; if (app.isCustomDomain()) { if ( diff --git a/packages/commonwealth/client/scripts/views/pages/search/helpers.tsx b/packages/commonwealth/client/scripts/views/pages/search/helpers.tsx index f2fdf68b535..c13f754b5f7 100644 --- a/packages/commonwealth/client/scripts/views/pages/search/helpers.tsx +++ b/packages/commonwealth/client/scripts/views/pages/search/helpers.tsx @@ -204,7 +204,10 @@ const CommunityResultRow = ({ className="community-result-row" onClick={handleClick} > - +
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/view_multiple_snapshot_spaces/index.tsx b/packages/commonwealth/client/scripts/views/pages/view_multiple_snapshot_spaces/index.tsx index 95f9a4e472e..7c58810f700 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_multiple_snapshot_spaces/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_multiple_snapshot_spaces/index.tsx @@ -3,13 +3,18 @@ import { loadMultipleSpacesData } from 'helpers/snapshot_utils'; import 'pages/snapshot/multiple_snapshots_page.scss'; import React from 'react'; import app from 'state'; +import { useGetCommunityByIdQuery } from 'state/api/communities'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import { Skeleton } from '../../components/Skeleton'; import { CardsCollection } from '../../components/cards_collection'; import { SnapshotSpaceCard } from './SnapshotSpaceCard'; const MultipleSnapshotsPage = () => { - const [snapshotSpaces, setSnapshotSpaces] = React.useState>(); + const { data: community } = useGetCommunityByIdQuery({ + id: app.activeChainId(), + }); + const snapshotSpaces: string[] = community?.snapshot_spaces || []; + const [spacesMetadata, setSpacesMetadata] = React.useState< Array<{ space: SnapshotSpace; @@ -17,13 +22,7 @@ const MultipleSnapshotsPage = () => { }> >(); - if (app.chain && !snapshotSpaces) { - setSnapshotSpaces( - app.config.chains?.getById(app.activeChainId()).snapshot || [], - ); - } - - if (!spacesMetadata && snapshotSpaces) { + if (!spacesMetadata && snapshotSpaces.length > 0) { loadMultipleSpacesData(snapshotSpaces).then((data) => { setSpacesMetadata(data); }); diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/linked_proposals_card.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/linked_proposals_card.tsx index 2d3f6eed2ba..42ebdfff734 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/linked_proposals_card.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/linked_proposals_card.tsx @@ -65,22 +65,24 @@ export const LinkedProposalsCard = ({ }`, ); } else { - loadMultipleSpacesData(app.chain.meta.snapshot).then((data) => { - for (const { space: _space, proposals } of data) { - const matchingSnapshot = proposals.find( - (sn) => sn.id === proposal.identifier, - ); - if (matchingSnapshot) { - setSnapshotTitle(matchingSnapshot.title); - setSnapshotUrl( - `${ - app.isCustomDomain() ? '' : `/${thread.communityId}` - }/snapshot/${_space.id}/${matchingSnapshot.id}`, + loadMultipleSpacesData(app.chain.meta?.snapshot || []) + .then((data) => { + for (const { space: _space, proposals } of data) { + const matchingSnapshot = proposals.find( + (sn) => sn.id === proposal.identifier, ); - break; + if (matchingSnapshot) { + setSnapshotTitle(matchingSnapshot.title); + setSnapshotUrl( + `${ + app.isCustomDomain() ? '' : `/${thread.communityId}` + }/snapshot/${_space.id}/${matchingSnapshot.id}`, + ); + break; + } } - } - }); + }) + .catch(console.error); } setSnapshotProposalsLoaded(true); } diff --git a/packages/commonwealth/server/controllers/server_communities_methods/create_community.ts b/packages/commonwealth/server/controllers/server_communities_methods/create_community.ts index 1e1ff239d41..c790c3d3c34 100644 --- a/packages/commonwealth/server/controllers/server_communities_methods/create_community.ts +++ b/packages/commonwealth/server/controllers/server_communities_methods/create_community.ts @@ -11,7 +11,6 @@ import { CreateCommunity } from '@hicommonwealth/schemas'; import { BalanceType, ChainBase, - ChainNetwork, ChainType, DefaultPage, NotificationCategories, @@ -354,7 +353,7 @@ export async function __createCommunity( default_symbol, icon_url, description, - network: network as ChainNetwork, + network, type, // @ts-expect-error StrictNullChecks social_links: uniqueLinksArray, diff --git a/packages/commonwealth/server/controllers/server_communities_methods/update_community_id.ts b/packages/commonwealth/server/controllers/server_communities_methods/update_community_id.ts index c7bffc3e470..32088ac0a00 100644 --- a/packages/commonwealth/server/controllers/server_communities_methods/update_community_id.ts +++ b/packages/commonwealth/server/controllers/server_communities_methods/update_community_id.ts @@ -4,7 +4,6 @@ import { CommunityInstance, ModelInstance, } from '@hicommonwealth/model'; -import { ChainNetwork } from '@hicommonwealth/shared'; import { type ModelStatic } from 'sequelize'; import { z } from 'zod'; import { ServerCommunitiesController } from '../server_communities_controller'; @@ -61,9 +60,10 @@ export async function __updateCommunityId( id: new_community_id, ...communityData, redirect: community_id, - network: (communityData.network === id - ? new_community_id - : communityData.network) as ChainNetwork, + network: + communityData.network === id + ? new_community_id + : communityData.network, }, { transaction }, );