diff --git a/src/assets/locales/en/common.json b/src/assets/locales/en/common.json index 1916a6bc..b0a3f4cd 100644 --- a/src/assets/locales/en/common.json +++ b/src/assets/locales/en/common.json @@ -44,11 +44,12 @@ "yes": "Yes", "no": "No", "description": "Description", + "summary": "Summary", "i understand": "I understand", "my address": "My address", "copy address": "Copy address", "change network": "Change network", "generating": "Generating...", "balance": "Balance", - "go back": "Go back" + "go back": "Go back", } diff --git a/src/assets/locales/en/messages/common.json b/src/assets/locales/en/messages/common.json index bc1eb640..a7ef1b1b 100644 --- a/src/assets/locales/en/messages/common.json +++ b/src/assets/locales/en/messages/common.json @@ -1,5 +1,6 @@ { "signer": "Signer", "user": "User", - "update params": "Update params" + "update params": "Update params", + "update module params": "Update {{ module }} module params", } diff --git a/src/components/InlineProfile/index.tsx b/src/components/InlineProfile/index.tsx new file mode 100644 index 00000000..3f0ce0b8 --- /dev/null +++ b/src/components/InlineProfile/index.tsx @@ -0,0 +1,90 @@ +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import TypographyContentLoaders from 'components/ContentLoaders/Typography'; +import CopiableAddress from 'components/CopiableAddress'; +import ProfileImage from 'components/ProfileImage'; +import Spacer from 'components/Spacer'; +import Typography from 'components/Typography'; +import { makeStyle } from 'config/theme'; +import useGetProfile from 'hooks/profile/useGetProfile'; +import { getProfileDisplayName } from 'lib/ProfileUtils'; +import { RootNavigatorParamList } from 'navigation/RootNavigator'; +import ROUTES from 'navigation/routes'; +import React from 'react'; +import { View } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { DesmosProfile } from 'types/desmos'; + +interface InlineProfileProps { + /** + * Address of the profile to display. + */ + readonly address: string; + /** + * Optional prefetched profile. + */ + readonly profile?: DesmosProfile; +} + +/** + * Components that displays an user profile and if clicked opens the + * profile screen. + */ +const InlineProfile: React.FC = ({ address, profile }) => { + const styles = useStyles(); + const [toDisplayProfile, setToDisplayProfile] = React.useState(profile); + const [profileLoading, setProfileLoading] = React.useState(profile === undefined); + + const navigation = useNavigation>(); + const getProfile = useGetProfile(); + const showProfile = React.useCallback(() => { + navigation.navigate(ROUTES.PROFILE, { + visitingProfile: address, + }); + }, [address, navigation]); + + React.useEffect(() => { + (async () => { + if (profile === undefined) { + setProfileLoading(true); + const fetchedProfile = await getProfile(address); + if (fetchedProfile.isOk()) { + setToDisplayProfile(fetchedProfile.value); + } + setProfileLoading(false); + } + })(); + }, [address, getProfile, profile]); + + if (toDisplayProfile === undefined && profileLoading === false) { + return ( + + + + ); + } + + return ( + + + + + {profileLoading ? ( + + ) : ( + {getProfileDisplayName(toDisplayProfile!)} + )} + + + ); +}; + +export default InlineProfile; + +const useStyles = makeStyle(() => ({ + root: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, +})); diff --git a/src/components/Messages/MessageDetails.tsx b/src/components/Messages/MessageDetails.tsx index ac4db9eb..cc50c680 100644 --- a/src/components/Messages/MessageDetails.tsx +++ b/src/components/Messages/MessageDetails.tsx @@ -2,6 +2,7 @@ import { EncodeObject } from '@cosmjs/proto-signing'; import React from 'react'; import { messageDetailsComponents } from './components'; import MsgUnknownComponents from './MsgUnknown'; +import MsgUpdateParamsDetails from './common/MsgUpdateParamsDetails'; export type MessageDetailsProps = { /** @@ -18,6 +19,9 @@ export type MessageDetailsProps = { const MessageDetails: React.FC = ({ message, toBroadcastMessage }) => React.useMemo(() => { + if (message.typeUrl.indexOf('MsgUpdateParams') > 0) { + return ; + } const DetailsComponent = messageDetailsComponents[message.typeUrl] || MsgUnknownComponents; return ; }, [message, toBroadcastMessage]); diff --git a/src/components/Messages/common/MsgUpdateParamsDetails/index.tsx b/src/components/Messages/common/MsgUpdateParamsDetails/index.tsx new file mode 100644 index 00000000..9695f621 --- /dev/null +++ b/src/components/Messages/common/MsgUpdateParamsDetails/index.tsx @@ -0,0 +1,60 @@ +import { EncodeObject } from '@desmoslabs/desmjs'; +import { MessageDetailsComponent } from 'components/Messages/BaseMessage'; +import BaseMessageDetails, { + MessageDetailsField, +} from 'components/Messages/BaseMessage/BaseMessageDetails'; +import Typography from 'components/Typography'; +import { formatCoin, formatCoins, isCoin } from 'lib/FormatUtils'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +const MsgUpdateParamsDetails: MessageDetailsComponent = ({ message }) => { + const { t } = useTranslation('messages.common'); + const moduleName = React.useMemo(() => { + const [, name] = message.typeUrl.split('.'); + return `${name[0].toUpperCase()}${name.substring(1)}`; + }, [message.typeUrl]); + + const fields = React.useMemo(() => { + const result: MessageDetailsField[] = []; + Object.keys(message.value.params).forEach((key) => { + const objectValue = message.value.params[key]; + let serializedValue: string; + + if (typeof objectValue === 'object') { + // Special case for the coin object. + if (isCoin(objectValue)) { + serializedValue = formatCoin(objectValue); + } else if ( + Object.prototype.toString.call(objectValue) === '[object Array]' && + isCoin(objectValue[0]) + ) { + // Special case for array of coins. + serializedValue = formatCoins(objectValue); + } else { + serializedValue = JSON.stringify(objectValue, undefined, 4); + } + } else if (objectValue === null || objectValue === undefined) { + serializedValue = 'null'; + } else { + serializedValue = objectValue.toString(); + } + + result.push({ + label: key, + value: serializedValue, + }); + }); + return result; + }, [message.value]); + + return ( + + + {t('update module params', { module: moduleName })} + + + ); +}; + +export default MsgUpdateParamsDetails; diff --git a/src/components/Messages/components.ts b/src/components/Messages/components.ts index 962ca99d..dc7cf16e 100644 --- a/src/components/Messages/components.ts +++ b/src/components/Messages/components.ts @@ -17,7 +17,6 @@ import { MsgExecTypeUrl, MsgSoftwareUpgradeTypeUrl, MsgTransferTypeUrl, - MsgUpdateStakingModuleParamsTypeUrl, SoftwareUpgradeProposalTypeUrl, } from 'types/cosmos'; import MsgExecDetails from 'components/Messages/authz/MsgExecDetails'; @@ -26,7 +25,6 @@ import MsgDepositDetails from 'components/Messages/gov/MsgDepositDetails'; import MsgTransferDetails from 'components/Messages/ibc/MsgTransferDetails'; import MsgCreateValidatorDetails from 'components/Messages/staking/MsgCreateValidatorDetails'; import MsgEditValidatorDetails from 'components/Messages/staking/MsgEditValidatorDetails'; -import MsgUpdateStakingModuleParams from 'components/Messages/staking/MsgUpdateParams'; import SoftwareUpgradeProposal from 'components/Messages/upgrade/v1beta1/SoftwareUpgradeProposal'; import MsgSoftwareUpgrade from 'components/Messages/upgrade/v1beta1/MsgSoftwareUpgrade'; import MsgMovePostDetails from 'components/Messages/posts/MsgMovePostDetails'; @@ -100,7 +98,6 @@ import MsgCreateDenomDetails from './tokenfactory/MsgCreateDenomDetails'; import MsgMintDetails from './tokenfactory/MsgMintDetails'; import MsgBurnDetails from './tokenfactory/MsgBurnDetails'; import MsgSetDenomMetadataDetails from './tokenfactory/MsgSetDenomMetadataDetails'; -import MsgUpdateParamsDetails from './tokenfactory/MsgUpdateParamsDetails'; export const messageDetailsComponents: Record> = { // x/authz @@ -137,7 +134,6 @@ export const messageDetailsComponents: Record = ({ - message, -}) => { - const { t } = useTranslation('messages.staking'); - - const fields = React.useMemo( - () => [ - { - label: t('bond denom'), - value: message.value.params.bondDenom, - }, - { - label: t('max entries'), - value: message.value.params.maxEntries.toString(), - }, - { - label: t('max validators'), - value: message.value.params.maxValidators.toString(), - }, - { - label: t('unbonding time'), - value: message.value.params.unbondingTime, - }, - { - label: t('historical entries'), - value: message.value.params.historicalEntries.toString(), - }, - { - label: t('min commission rate'), - value: message.value.params.minCommissionRate, - }, - ], - [ - message.value.params.bondDenom, - message.value.params.historicalEntries, - message.value.params.maxEntries, - message.value.params.maxValidators, - message.value.params.minCommissionRate, - message.value.params.unbondingTime, - t, - ], - ); - - return ( - - - - - - ); -}; - -export default MsgUpdateParams; diff --git a/src/components/Messages/tokenfactory/MsgUpdateParamsDetails/index.tsx b/src/components/Messages/tokenfactory/MsgUpdateParamsDetails/index.tsx deleted file mode 100644 index 0c141063..00000000 --- a/src/components/Messages/tokenfactory/MsgUpdateParamsDetails/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { TokenFactory } from '@desmoslabs/desmjs'; -import React from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import BaseMessageDetails, { - MessageDetailsField, -} from 'components/Messages/BaseMessage/BaseMessageDetails'; -import { MessageDetailsComponent } from 'components/Messages/BaseMessage'; -import Typography from 'components/Typography'; -import CopiableAddress from 'components/CopiableAddress'; -import { isArrayEmptyOrUndefined } from 'lib/AssertionUtils'; - -/** - * Displays the full details of a MsgUpdateParams - * @constructor - */ -const MsgUpdateParamsDetails: MessageDetailsComponent< - TokenFactory.v1.MsgUpdateParamsEncodeObject -> = ({ message, toBroadcastMessage }) => { - const { t } = useTranslation('messages.tokenfactory'); - - const fields = React.useMemo( - () => [ - { - label: t('default send enabled'), - value: message.value.params?.defaultSendEnabled ? 'true' : 'false', - hide: message.value.params?.defaultSendEnabled === undefined, - }, - { - label: t('send enabled'), - value: message.value.params?.sendEnabled - ?.map((se) => `${se.denom}: ${se.enabled}`) - .join('\n'), - hide: isArrayEmptyOrUndefined(message.value.params?.sendEnabled), - }, - ], - [t, message], - ); - - return ( - - - ]} - /> - - - ); -}; - -export default MsgUpdateParamsDetails; diff --git a/src/lib/FormatUtils/index.ts b/src/lib/FormatUtils/index.ts index b27289b2..e71096c0 100644 --- a/src/lib/FormatUtils/index.ts +++ b/src/lib/FormatUtils/index.ts @@ -100,6 +100,18 @@ export const formatCoins = ( separator: string = '\n', ): string => (amount || []).map(formatCoin).join(separator); +/** + * Checks if the given value is a Coin. + * @param value - The value to check. + */ +export const isCoin = (value: any): value is Coin => + value && + typeof value === 'object' && + 'denom' in value && + 'amount' in value && + typeof value.denom === 'string' && + typeof value.amount === 'string'; + /** * Formats the provided amount using the application's fiat representation style. * @param amount - The amount to be formatted. diff --git a/src/lib/GraphQLUtils/message.ts b/src/lib/GraphQLUtils/message.ts index 7bd67d92..47161ae8 100644 --- a/src/lib/GraphQLUtils/message.ts +++ b/src/lib/GraphQLUtils/message.ts @@ -143,7 +143,6 @@ import { MsgCreateDenom, MsgMint, MsgSetDenomMetadata, - MsgUpdateParams, } from '@desmoslabs/desmjs-types/desmos/tokenfactory/v1/msgs'; import { Metadata } from '@desmoslabs/desmjs-types/cosmos/bank/v1beta1/bank'; @@ -1591,13 +1590,12 @@ const decodeTokenFactoryMessages = (type: string, value: any): EncodeObject | un case TokenFactory.v1.MsgUpdateParamsTypeUrl: return { typeUrl: TokenFactory.v1.MsgUpdateParamsTypeUrl, - value: MsgUpdateParams.fromPartial({ + value: { authority: value.authority, params: { - defaultSendEnabled: value.params.default_send_enabled, - sendEnabled: value.params.send_enabled, + denomCreationFee: value.params.denom_creation_fee, }, - }), + }, }; default: diff --git a/src/screens/GovernanceProposalDetails/components/ProposalDetails/index.tsx b/src/screens/GovernanceProposalDetails/components/ProposalDetails/index.tsx index f12c412f..78c2e86c 100644 --- a/src/screens/GovernanceProposalDetails/components/ProposalDetails/index.tsx +++ b/src/screens/GovernanceProposalDetails/components/ProposalDetails/index.tsx @@ -1,16 +1,15 @@ import React from 'react'; -import { Dimensions, View } from 'react-native'; +import { View } from 'react-native'; import Typography from 'components/Typography'; import { useTranslation } from 'react-i18next'; import { Proposal, ProposalContent } from 'types/proposals'; import StyledMarkDown from 'components/StyledMarkdown'; -import CopiableAddress from 'components/CopiableAddress'; import { format } from 'date-fns'; import Spacer from 'components/Spacer'; import decodeGqlRawMessage from 'lib/GraphQLUtils/message'; import MessageDetails from 'components/Messages/MessageDetails'; import { Message } from 'types/transactions'; -import { FlashList } from '@shopify/flash-list'; +import InlineProfile from 'components/InlineProfile'; export interface ProposalDetailsProps { /** @@ -45,28 +44,14 @@ const ProposalDetails: React.FC = ({ proposal }) => { const planContent = React.useMemo(() => { if (proposal.content.map !== undefined) { - const { width: windowWidth } = Dimensions.get('window'); - const messages: Message[] = proposal.content.map((content: ProposalContent) => - decodeGqlRawMessage(content), - ); - - // The proposal content is an array, so we have a gov v1 content. - return ( - ( - // We use the window width instead of '100%' as width because - // we are displaying the items in a horizontal list and so - // width = '100%' means infinite width. - - - - )} - horizontal={true} - estimatedItemSize={299} - ItemSeparatorComponent={() => } - /> - ); + return proposal.content + .map((content: ProposalContent) => decodeGqlRawMessage(content)) + .map((message: Message, index: number) => ( + + {index > 0 && } + + + )); } // The proposal content is an object, treat it as a gov v1beta1 proposal. @@ -79,6 +64,15 @@ const ProposalDetails: React.FC = ({ proposal }) => { return ( + {/* Summary */} + {proposal.summary && proposal.summary.length > 0 && ( + <> + {t('common:summary')} + {proposal.summary} + + + )} + {/* Description */} {t('common:description')} {proposal.description} @@ -92,7 +86,7 @@ const ProposalDetails: React.FC = ({ proposal }) => { {/* Proposer */} {t('proposer')} - + {/* Submit time */} diff --git a/src/services/graphql/queries/fragments/ProposalFields.ts b/src/services/graphql/queries/fragments/ProposalFields.ts index dc288dad..ab5f0aa8 100644 --- a/src/services/graphql/queries/fragments/ProposalFields.ts +++ b/src/services/graphql/queries/fragments/ProposalFields.ts @@ -4,6 +4,7 @@ const ProposalFields = gql` fragment ProposalFields on proposal { id title + summary description proposerAddress: proposer_address status diff --git a/src/types/proposals.ts b/src/types/proposals.ts index 1c713ac5..5d80409a 100644 --- a/src/types/proposals.ts +++ b/src/types/proposals.ts @@ -45,6 +45,7 @@ export interface ProposalContent extends Record { export interface Proposal { readonly id: number; readonly title: string; + readonly summary: string; readonly description: string; readonly proposerAddress: string; readonly status: ProposalStatus;