From 919c133d005ead12fe0123f5594c3e40bbf30c61 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 7 Nov 2024 00:57:58 -0500 Subject: [PATCH] added banner to DAO home page --- packages/i18n/locales/en/translation.json | 3 +- .../MintNft/stateless/UploadNftMetadata.tsx | 4 +- .../core/actions/UpdateInfo/Component.tsx | 176 ++++++++++-------- .../actions/core/actions/UpdateInfo/README.md | 4 +- .../actions/core/actions/UpdateInfo/index.tsx | 116 ++++++++---- packages/stateful/clients/dao/base.ts | 7 + .../stateful/components/dao/CreateDaoForm.tsx | 76 +++++--- .../stateless/components/dao/DaoHeader.tsx | 61 ++++-- .../stateless/components/dao/DaoImage.tsx | 4 +- .../components/dao/DaoSplashHeader.tsx | 1 + .../components/inputs/ImageSelector.tsx | 22 ++- packages/types/clients/dao.ts | 5 + packages/types/dao.ts | 1 + packages/utils/validation/index.ts | 3 +- 14 files changed, 320 insertions(+), 163 deletions(-) diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 1684777172..fc4d263344 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -610,6 +610,7 @@ "automaticallyAddNFTsTooltip": "Should NFTs sent to the DAO get added to the treasury?", "automaticallyAddTokensTitle": "Automatically add tokens", "automaticallyAddTokensTooltip": "Should tokens sent to the DAO get added to the treasury?", + "banner": "Banner", "baseToken": "Base token", "becomeSubDaoAdminInputLabel": "New parent DAO", "blocksToPauseFor": "Blocks to pause for", @@ -1510,7 +1511,7 @@ }, "upToDate": "Up to date", "updateContractAdminActionDescription": "Update the CosmWasm level admin of a smart contract.", - "updateInfoActionDescription": "Update your DAO's name, image, and description.", + "updateInfoActionDescription": "Update your DAO's name, description, image, and banner.", "updateMinterAllowanceDescription": "Allow an account to mint tokens, or remove the allowance.", "updateMinterAllowanceExplanation": "This action is needed to allow an account to mint tokens.", "updatePostDescription": "Update a post on the DAO's press.", diff --git a/packages/stateful/actions/core/actions/MintNft/stateless/UploadNftMetadata.tsx b/packages/stateful/actions/core/actions/MintNft/stateless/UploadNftMetadata.tsx index 1c22bbe79b..02a1a996c1 100644 --- a/packages/stateful/actions/core/actions/MintNft/stateless/UploadNftMetadata.tsx +++ b/packages/stateful/actions/core/actions/MintNft/stateless/UploadNftMetadata.tsx @@ -173,7 +173,7 @@ export const UploadNftMetadata: ActionComponent = ({ 'metadata.properties.audio') as 'metadata.properties.audio' } register={register} - validation={[(v) => !v || validateUrlWithIpfs(v)]} + validation={[validateUrlWithIpfs]} /> @@ -189,7 +189,7 @@ export const UploadNftMetadata: ActionComponent = ({ 'metadata.properties.video') as 'metadata.properties.video' } register={register} - validation={[(v) => !v || validateUrlWithIpfs(v)]} + validation={[validateUrlWithIpfs]} /> diff --git a/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx b/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx index 20a2c8447f..cc427e8f35 100644 --- a/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx +++ b/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx @@ -24,7 +24,9 @@ import { import { LinkWrapper, Trans } from '../../../../components' -export type UpdateInfoData = ConfigV1Response | ConfigV2Response +export type UpdateInfoData = (ConfigV1Response | ConfigV2Response) & { + banner?: string | null +} export const UpdateInfoComponent: ActionComponent< undefined, @@ -35,98 +37,120 @@ export const UpdateInfoComponent: ActionComponent< const { t } = useTranslation() const { register, watch, setValue } = useFormContext() + const banner = watch(fieldNamePrefix + 'banner') + const isNeutronForkDao = context.type === ActionContextType.Dao && context.dao.chainId === ChainId.NeutronMainnet && isNeutronForkVersion(context.dao.coreVersion) return ( -
- {!isNeutronForkDao && - (isCreating ? ( -
- - -
- ) : ( - - ))} - -
-
- - -
- -
- + {(isCreating || !!banner) && ( +
+ + -
+ )} +
{!isNeutronForkDao && ( -
- + + {isCreating ? ( + + ) : ( + + )} +
+ )} + +
+
+ + +
- + +
- )} - {!isCreating && ( -

- {t('info.daoInfoWillRefresh', { - seconds: DAO_STATIC_PROPS_CACHE_SECONDS.toLocaleString(), - })} -

- )} + {!isNeutronForkDao && ( +
+ + + +
+ )} + + {!isCreating && ( +

+ {t('info.daoInfoWillRefresh', { + seconds: DAO_STATIC_PROPS_CACHE_SECONDS.toLocaleString(), + })} +

+ )} +
-
+ ) } diff --git a/packages/stateful/actions/core/actions/UpdateInfo/README.md b/packages/stateful/actions/core/actions/UpdateInfo/README.md index 7387fa285a..7aeea88de1 100644 --- a/packages/stateful/actions/core/actions/UpdateInfo/README.md +++ b/packages/stateful/actions/core/actions/UpdateInfo/README.md @@ -1,6 +1,6 @@ # UpdateInfo -Update the name, description, image, and some other config for the DAO. +Update the name, description, image, banner, and some other config for the DAO. ## Bulk import format @@ -19,6 +19,8 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "description": "", // Optional. If unset, will be removed. "image_url": "", + // Optional. If unset, will be removed. + "banner": "", "automatically_add_cw20s": , "automatically_add_cw721s": , // Only on v2 and above. Optional. If unset, will be removed. diff --git a/packages/stateful/actions/core/actions/UpdateInfo/index.tsx b/packages/stateful/actions/core/actions/UpdateInfo/index.tsx index b3885f6e87..e434752a93 100644 --- a/packages/stateful/actions/core/actions/UpdateInfo/index.tsx +++ b/packages/stateful/actions/core/actions/UpdateInfo/index.tsx @@ -13,12 +13,15 @@ import { objectMatchesStructure, } from '@dao-dao/utils' +import { ManageStorageItemsAction } from '../ManageStorageItems' import { UpdateInfoComponent as Component, UpdateInfoData } from './Component' export class UpdateInfoAction extends ActionBase { public readonly key = ActionKey.UpdateInfo public readonly Component = Component + private manageStorageItemsAction: ManageStorageItemsAction + constructor(options: ActionOptions) { if (options.context.type !== ActionContextType.Dao) { throw new Error('Only DAOs can update info.') @@ -29,51 +32,67 @@ export class UpdateInfoAction extends ActionBase { label: options.t('title.updateInfo'), description: options.t('info.updateInfoActionDescription'), }) + + this.manageStorageItemsAction = new ManageStorageItemsAction(options) } async setup() { - this.defaults = await this.options.queryClient.fetchQuery( - daoDaoCoreQueries.config(this.options.queryClient, { - chainId: this.options.chain.chainId, - contractAddress: this.options.address, - }) - ) + if (this.options.context.type !== ActionContextType.Dao) { + throw new Error('Only DAOs can update info.') + } + + this.defaults = { + ...(await this.options.queryClient.fetchQuery( + daoDaoCoreQueries.config(this.options.queryClient, { + chainId: this.options.chain.chainId, + contractAddress: this.options.address, + }) + )), + banner: this.options.context.dao.bannerImageUrl, + } } - encode(data: UpdateInfoData): UnifiedCosmosMsg { + encode({ banner, ...data }: UpdateInfoData): UnifiedCosmosMsg[] { // Type-check. Should be validated in the constructor. if (this.options.context.type !== ActionContextType.Dao) { throw new Error('Only DAOs can update info.') } - return makeExecuteSmartContractMessage({ - chainId: this.options.chain.chainId, - sender: this.options.address, - contractAddress: this.options.address, - msg: { - update_config: { - config: - this.options.context.dao.chainId === ChainId.NeutronMainnet && - isNeutronForkVersion(this.options.context.dao.coreVersion) - ? // The Neutron fork DAO has a different config structure. - { - name: data.name, - description: data.description, - dao_uri: 'dao_uri' in data ? data.dao_uri : null, - } - : { - ...data, - // Replace empty string with null. - image_url: data.image_url?.trim() || null, - }, + return [ + makeExecuteSmartContractMessage({ + chainId: this.options.chain.chainId, + sender: this.options.address, + contractAddress: this.options.address, + msg: { + update_config: { + config: + this.options.context.dao.chainId === ChainId.NeutronMainnet && + isNeutronForkVersion(this.options.context.dao.coreVersion) + ? // The Neutron fork DAO has a different config structure. + { + name: data.name, + description: data.description, + dao_uri: 'dao_uri' in data ? data.dao_uri : null, + } + : { + ...data, + // Replace empty string with null. + image_url: data.image_url?.trim() || null, + }, + }, }, - }, - }) + }), + this.manageStorageItemsAction.encode({ + setting: !!banner, + key: 'banner', + value: banner || '', + }), + ] } - match([{ decodedMessage }]: ProcessedMessage[]): ActionMatch { - return ( - objectMatchesStructure(decodedMessage, { + match(messages: ProcessedMessage[]): ActionMatch { + const isUpdateInfo = + objectMatchesStructure(messages[0].decodedMessage, { wasm: { execute: { contract_addr: {}, @@ -88,12 +107,26 @@ export class UpdateInfoAction extends ActionBase { }, }, }, - }) && decodedMessage.wasm.execute.contract_addr === this.options.address - ) + }) && + messages[0].decodedMessage.wasm.execute.contract_addr === + this.options.address + + if (!isUpdateInfo) { + return false + } + + const isChangingBanner = + messages.length >= 2 && + this.manageStorageItemsAction.match([messages[1]]) && + this.manageStorageItemsAction.decode([messages[1]]).key === 'banner' + + // If is changing banner, match both update info and banner change. + // Otherwise, just match update info. + return isChangingBanner ? 2 : 1 } - decode([ - { + decode(messages: ProcessedMessage[]): UpdateInfoData { + const { decodedMessage: { wasm: { execute: { @@ -103,8 +136,13 @@ export class UpdateInfoAction extends ActionBase { }, }, }, - }, - ]: ProcessedMessage[]): UpdateInfoData { + } = messages[0] + + const decodedBanner = + messages.length === 2 + ? this.manageStorageItemsAction.decode([messages[1]]) + : undefined + return { name: config.name, description: config.description, @@ -116,6 +154,8 @@ export class UpdateInfoAction extends ActionBase { image_url: config.image_url, }), + banner: decodedBanner?.setting ? decodedBanner.value : undefined, + // V2 passthrough // Only add dao URI if in the message. ...('dao_uri' in config && { diff --git a/packages/stateful/clients/dao/base.ts b/packages/stateful/clients/dao/base.ts index 4cf148b020..b67976658d 100644 --- a/packages/stateful/clients/dao/base.ts +++ b/packages/stateful/clients/dao/base.ts @@ -123,6 +123,13 @@ export abstract class DaoBase implements IDaoBase { return this.info.imageUrl } + /** + * DAO banner image URL. + */ + get bannerImageUrl(): string | undefined { + return this.info.items['banner'] + } + /** * Get the proposal module with the given address. */ diff --git a/packages/stateful/components/dao/CreateDaoForm.tsx b/packages/stateful/components/dao/CreateDaoForm.tsx index ad5d827c66..e641882ed2 100644 --- a/packages/stateful/components/dao/CreateDaoForm.tsx +++ b/packages/stateful/components/dao/CreateDaoForm.tsx @@ -1,6 +1,7 @@ import { toBase64, toUtf8 } from '@cosmjs/encoding' import { ArrowBack, Clear } from '@mui/icons-material' import { useQueryClient } from '@tanstack/react-query' +import clsx from 'clsx' import cloneDeep from 'lodash.clonedeep' import merge from 'lodash.merge' import { nanoid } from 'nanoid' @@ -312,6 +313,7 @@ export const InnerCreateDaoForm = ({ name, description, imageUrl, + bannerImageUrl, creator: { id: creatorId, data: creatorData }, proposalModuleAdapters, votingConfig, @@ -445,25 +447,36 @@ export const InnerCreateDaoForm = ({ ) ) + const initialItems: InitialItem[] = [ + // Add banner image if set. + ...(bannerImageUrl?.trim() + ? [ + { + key: 'banner', + value: bannerImageUrl.trim(), + }, + ] + : []), + // Add widgets if configured. + ...(widgets && Object.keys(widgets).length > 0 + ? Object.entries(widgets).flatMap(([id, values]): InitialItem | [] => + values + ? { + key: getWidgetStorageItemKey(id), + value: JSON.stringify(values), + } + : [] + ) + : []), + ] + const commonConfig = { // If parentDao exists, let's make a subDAO :D admin: parentDao?.coreAddress ?? null, name: name.trim(), description, imageUrl, - // Add widgets if configured. - ...(widgets && - Object.keys(widgets).length > 0 && { - initialItems: Object.entries(widgets).flatMap( - ([id, values]): InitialItem | [] => - values - ? { - key: getWidgetStorageItemKey(id), - value: JSON.stringify(values), - } - : [] - ), - }), + initialItems: initialItems.length > 0 ? initialItems : undefined, } if (isSecretNetwork(chainId)) { @@ -1048,24 +1061,35 @@ export const InnerCreateDaoForm = ({ /> - +
+ -

- {t('form.addAnImage')} -

+ +
) : ( follow?: FollowState @@ -25,6 +31,7 @@ export const DaoHeader = ({ name, description, imageUrl, + bannerImageUrl, parentDao, LinkWrapper, follow, @@ -34,21 +41,51 @@ export const DaoHeader = ({ const [descriptionCollapsed, setDescriptionCollapsed] = useState(false) const [descriptionVisible, setDescriptionVisible] = useState(false) + const image = ( + + ) + return ( <> -
- +
+ {bannerImageUrl ? ( +
+ {image} +
+ ) : ( + image + )} -
+
-

{name}

+

{name}

{follow && ( // Only show following toggle on desktop. Mobile shows in header. diff --git a/packages/stateless/components/dao/DaoImage.tsx b/packages/stateless/components/dao/DaoImage.tsx index 79f44ca123..10d18e295f 100644 --- a/packages/stateless/components/dao/DaoImage.tsx +++ b/packages/stateless/components/dao/DaoImage.tsx @@ -12,7 +12,7 @@ import { Tooltip } from '../tooltip' export interface DaoImageProps { daoName: string - size: 'sm' | 'md' | 'lg' + size: 'sm' | 'md' | 'lg' | 'xl' imageUrl: string | undefined | null // Used to get placeholder image if no `imageUrl` present. coreAddress?: string @@ -51,6 +51,8 @@ export const DaoImage = ({ 'h-7 w-7': size === 'md', // DAO home page 'h-20 w-20 xs:h-24 xs:w-24': size === 'lg', + // DAO home page with banner + 'h-20 w-20 xs:h-24 xs:w-24 sm:h-28 sm:w-28': size === 'xl', }) return ( diff --git a/packages/stateless/components/dao/DaoSplashHeader.tsx b/packages/stateless/components/dao/DaoSplashHeader.tsx index 71661a61b9..10dd6d85e5 100644 --- a/packages/stateless/components/dao/DaoSplashHeader.tsx +++ b/packages/stateless/components/dao/DaoSplashHeader.tsx @@ -100,6 +100,7 @@ export const DaoSplashHeader = ({ ) => { const { t } = useTranslation() const imageUrl = watch(fieldName) ?? '' @@ -79,13 +81,18 @@ export const ImageSelectorModal = < return ( onCloseOrDone(false)} visible={visible} >
+
{ setUploadError(undefined) setValue(fieldName, url as any) @@ -164,6 +172,7 @@ export interface ImageSelectorProps< error?: FieldError className?: string size?: string | number + style?: 'avatar' | 'banner' } export const ImageSelector = < @@ -176,6 +185,7 @@ export const ImageSelector = < className, disabled, size, + style = 'avatar', ...props }: ImageSelectorProps) => { const [showImageSelect, setShowImageSelect] = useState(false) @@ -185,11 +195,12 @@ export const ImageSelector = < <>