diff --git a/backend/main/cadence/scripts/get_nfts_ids.cdc b/backend/main/cadence/scripts/get_nfts_ids.cdc index 5c908a9bd..a9c509162 100644 --- a/backend/main/cadence/scripts/get_nfts_ids.cdc +++ b/backend/main/cadence/scripts/get_nfts_ids.cdc @@ -5,7 +5,7 @@ pub fun main(address: Address): [UInt64] { let account = getAccount(address) let collectionRef = account - .getCapability("TOKEN_NAME".CollectionPublicPath) + .getCapability(/public/"COLLECTION_PUBLIC_PATH") .borrow<&{NonFungibleToken.CollectionPublic}>() ?? panic("Could not borrow capability from public collection") diff --git a/backend/main/models/community.go b/backend/main/models/community.go index 06b5dc309..ef7ad2083 100644 --- a/backend/main/models/community.go +++ b/backend/main/models/community.go @@ -76,9 +76,8 @@ type UpdateCommunityRequestPayload struct { } type Strategy struct { - Name *string `json:"name,omitempty"` - shared.Contract `json:"contract,omitempty"` - RequiresSnapshot bool + Name *string `json:"name,omitempty"` + shared.Contract `json:"contract,omitempty"` } type CommunityType struct { diff --git a/backend/main/server/app.go b/backend/main/server/app.go index 45b9e1aea..3aed420ac 100644 --- a/backend/main/server/app.go +++ b/backend/main/server/app.go @@ -68,13 +68,14 @@ type Strategy interface { GetVoteWeightForBalance(vote *models.VoteWithBalance, proposal *models.Proposal) (float64, error) InitStrategy(f *shared.FlowAdapter, db *shared.Database, sc *shared.SnapshotClient) FetchBalance(b *models.Balance, p *models.Proposal) (*models.Balance, error) + RequiresSnapshot() bool } var strategyMap = map[string]Strategy{ - "token-weighted-default": &strategies.TokenWeightedDefault{RequiresSnapshot: true}, - "staked-token-weighted-default": &strategies.StakedTokenWeightedDefault{RequiresSnapshot: true}, - "one-address-one-vote": &strategies.OneAddressOneVote{RequiresSnapshot: false}, - "balance-of-nfts": &strategies.BalanceOfNfts{RequiresSnapshot: false}, + "token-weighted-default": &strategies.TokenWeightedDefault{}, + "staked-token-weighted-default": &strategies.StakedTokenWeightedDefault{}, + "one-address-one-vote": &strategies.OneAddressOneVote{}, + "balance-of-nfts": &strategies.BalanceOfNfts{}, } const ( @@ -793,9 +794,11 @@ func (a *App) createProposal(w http.ResponseWriter, r *http.Request) { return } + s := strategyMap[*p.Strategy] + s.InitStrategy(a.FlowAdapter, a.DB, a.SnapshotClient) var snapshotResponse *shared.SnapshotResponse - if strategy.RequiresSnapshot { + if s.RequiresSnapshot() { snapshotResponse, err = a.SnapshotClient.TakeSnapshot(strategy.Contract) if err != nil { log.Error().Err(err).Msg("error taking snapshot") diff --git a/backend/main/shared/flow.go b/backend/main/shared/flow.go index 4678fe23f..2115e41a7 100644 --- a/backend/main/shared/flow.go +++ b/backend/main/shared/flow.go @@ -52,6 +52,7 @@ var ( placeholderFungibleTokenAddr = regexp.MustCompile(`"[^"\s]*FUNGIBLE_TOKEN_ADDRESS"`) placeholderNonFungibleTokenAddr = regexp.MustCompile(`"[^"\s]*NON_FUNGIBLE_TOKEN_ADDRESS"`) placeholderMetadataViewsAddr = regexp.MustCompile(`"[^"\s]*METADATA_VIEWS_ADDRESS"`) + placeholderCollectionPublicPath = regexp.MustCompile(`"[^"\s]*COLLECTION_PUBLIC_PATH"`) ) func NewFlowClient(flowEnv string) *FlowAdapter { @@ -354,6 +355,7 @@ func (fa *FlowAdapter) ReplaceContractPlaceholders(code string, c *Contract, isF if isFungible { code = placeholderFungibleTokenAddr.ReplaceAllString(code, fungibleTokenAddr) } else { + code = placeholderCollectionPublicPath.ReplaceAllString(code, *c.Public_path) code = placeholderNonFungibleTokenAddr.ReplaceAllString(code, nonFungibleTokenAddr) } diff --git a/backend/main/strategies/balance_of_nfts.go b/backend/main/strategies/balance_of_nfts.go index 34c2857f4..a826f438f 100644 --- a/backend/main/strategies/balance_of_nfts.go +++ b/backend/main/strategies/balance_of_nfts.go @@ -11,8 +11,6 @@ type BalanceOfNfts struct { s.StrategyStruct SC s.SnapshotClient DB *s.Database - - RequiresSnapshot bool } func (b *BalanceOfNfts) FetchBalance( @@ -98,6 +96,10 @@ func (s *BalanceOfNfts) GetVotes( return votes, nil } +func (s *BalanceOfNfts) RequiresSnapshot() bool { + return false +} + func (s *BalanceOfNfts) InitStrategy( f *shared.FlowAdapter, db *shared.Database, diff --git a/backend/main/strategies/one_address_one_vote.go b/backend/main/strategies/one_address_one_vote.go index d00b5359c..32484a9c8 100644 --- a/backend/main/strategies/one_address_one_vote.go +++ b/backend/main/strategies/one_address_one_vote.go @@ -14,8 +14,6 @@ type OneAddressOneVote struct { s.StrategyStruct SC s.SnapshotClient DB *s.Database - - RequiresSnapshot bool } func (s *OneAddressOneVote) FetchBalance( @@ -81,6 +79,10 @@ func (s *OneAddressOneVote) GetVotes( return votes, nil } +func (s *OneAddressOneVote) RequiresSnapshot() bool { + return false +} + func (s *OneAddressOneVote) InitStrategy( f *shared.FlowAdapter, db *shared.Database, diff --git a/backend/main/strategies/staked_token_weighted_default.go b/backend/main/strategies/staked_token_weighted_default.go index 5e618b141..6f26d15c3 100644 --- a/backend/main/strategies/staked_token_weighted_default.go +++ b/backend/main/strategies/staked_token_weighted_default.go @@ -14,8 +14,6 @@ type StakedTokenWeightedDefault struct { s.StrategyStruct SC s.SnapshotClient DB *s.Database - - RequiresSnapshot bool } func (s *StakedTokenWeightedDefault) FetchBalance( @@ -111,6 +109,10 @@ func (s *StakedTokenWeightedDefault) GetVotes( return votes, nil } +func (s *StakedTokenWeightedDefault) RequiresSnapshot() bool { + return true +} + func (s *StakedTokenWeightedDefault) InitStrategy( f *shared.FlowAdapter, db *shared.Database, diff --git a/backend/main/strategies/token_weighted_default.go b/backend/main/strategies/token_weighted_default.go index 162f3ad81..60850b317 100644 --- a/backend/main/strategies/token_weighted_default.go +++ b/backend/main/strategies/token_weighted_default.go @@ -16,8 +16,6 @@ type TokenWeightedDefault struct { s.StrategyStruct SC s.SnapshotClient DB *s.Database - - RequiresSnapshot bool } type FTBalanceResponse struct { @@ -132,6 +130,10 @@ func (s *TokenWeightedDefault) GetVotes( return votes, nil } +func (s *TokenWeightedDefault) RequiresSnapshot() bool { + return true +} + func (s *TokenWeightedDefault) InitStrategy( f *shared.FlowAdapter, db *shared.Database, diff --git a/frontend/packages/client/src/App.sass b/frontend/packages/client/src/App.sass index ed71b490e..d239fdaf0 100644 --- a/frontend/packages/client/src/App.sass +++ b/frontend/packages/client/src/App.sass @@ -142,6 +142,8 @@ code border: 1px solid $grey-lighter .border-lighter-dark-grey border: 1px solid $lighter-dark-grey +.border-dashed-dark + border: 2px dashed $lighter-dark-grey .flex-1 flex: 1 0 0 .flex-2 @@ -254,7 +256,9 @@ hr .dropdown.is-disabled background-color: rgba(239, 239, 239, 0.3) pointer-events: none - +@media (min-width: 769px) and (max-width: 820px) + .is-hidden-connect + display: none @media (min-width: 1024px) .container > .navbar .navbar-brand margin-left: 0 !important @@ -530,8 +534,6 @@ span[data-tooltip] -o-animation: fadein 0.5s animation: fadein 0.5s -.word-break-all - word-break: break-all .word-break word-break: break-word .line-clamp-2 @@ -561,8 +563,6 @@ span[data-tooltip] max-width: 44px width: 44px text-align: center !important -.word-break-all - word-break: break-all .pulse .title-text diff --git a/frontend/packages/client/src/components/Community/CommunityEditorDetails.js b/frontend/packages/client/src/components/Community/CommunityEditorDetails.js index 44fadb78f..c91313571 100644 --- a/frontend/packages/client/src/components/Community/CommunityEditorDetails.js +++ b/frontend/packages/client/src/components/Community/CommunityEditorDetails.js @@ -328,15 +328,17 @@ export default function CommunityEditorDetails({ communityId } = {}) { return ( <> diff --git a/frontend/packages/client/src/components/Community/CommunityEditorProfile.js b/frontend/packages/client/src/components/Community/CommunityEditorProfile.js index 63f3027cd..04af315d0 100644 --- a/frontend/packages/client/src/components/Community/CommunityEditorProfile.js +++ b/frontend/packages/client/src/components/Community/CommunityEditorProfile.js @@ -1,15 +1,17 @@ import React, { useState, useEffect, useCallback } from 'react'; +import classnames from 'classnames'; import { useDropzone } from 'react-dropzone'; import { Upload } from 'components/Svg'; import { WrapperResponsive, Loader } from 'components'; import { getReducedImg } from 'utils'; import { useErrorHandlerContext } from 'contexts/ErrorHandler'; -import { MAX_AVATAR_FILE_SIZE } from 'const'; +import { MAX_AVATAR_FILE_SIZE, MAX_FILE_SIZE } from 'const'; function CommunityEditorProfile({ name, body = '', logo, + banner, // fn to update community payload updateCommunity, // fn to upload image @@ -18,15 +20,19 @@ function CommunityEditorProfile({ const [communityName, setCommunityName] = useState(name); const [communityDescription, setCommunityDescription] = useState(body); const [isUpdating, setIsUpdating] = useState(''); + const [isUpdatingImage, setIsUpdatingImage] = useState(false); + const [isUpdatingBanner, setIsUpdatingBanner] = useState(false); const [enableSave, setEnableSave] = useState(false); const [image, setImage] = useState({ imageUrl: logo }); + const [bannerImage, setBannerImage] = useState({ imageUrl: banner }); const { notifyError } = useErrorHandlerContext(); useEffect(() => { if ( (communityName !== name && communityName.length > 0) || communityDescription !== body || - image.file + image.file || + bannerImage.file ) { setEnableSave(true); } @@ -34,34 +40,46 @@ function CommunityEditorProfile({ communityName.trim().length === 0 || (communityName === name && communityDescription === body && - image.file === undefined) + image.file === undefined && + bannerImage.file === undefined) ) { setEnableSave(false); } - }, [name, body, communityName, communityDescription, image]); + }, [name, body, communityName, communityDescription, image, bannerImage]); const saveData = async () => { setIsUpdating(true); + // upload images if any let newImageUrl; - // upload image if any + let newBannerImageUrl; if (image.file) { + setIsUpdatingImage(true); newImageUrl = await uploadFile(image.file); } + if (bannerImage.file) { + setIsUpdatingBanner(true); + newBannerImageUrl = await uploadFile(bannerImage.file); + } const updates = { ...(communityName !== name ? { name: communityName.trim() } : undefined), ...(communityDescription !== body ? { body: communityDescription.trim() } : undefined), ...(newImageUrl?.fileUrl ? { logo: newImageUrl.fileUrl } : undefined), + ...(newBannerImageUrl?.fileUrl + ? { bannerImgUrl: newBannerImageUrl.fileUrl } + : undefined), }; // updated fields if (Object.keys(updates).length > 0) await updateCommunity(updates); setIsUpdating(false); + setIsUpdatingImage(false); + setIsUpdatingBanner(false); setEnableSave(false); }; const onDrop = useCallback( - (acceptedFiles) => { + (filename, dataKey, maxFileSize, maxWidth) => (acceptedFiles) => { acceptedFiles.forEach((imageFile) => { // validate type if ( @@ -75,37 +93,62 @@ function CommunityEditorProfile({ } // validate size if (imageFile.size > MAX_AVATAR_FILE_SIZE) { + const sizeLimit = + maxFileSize === MAX_AVATAR_FILE_SIZE ? '2MB' : '5MB'; notifyError({ status: 'Image file size not allowed', - statusText: 'The selected file exceeds the 2MB limit.', + statusText: `The selected file exceeds the ${sizeLimit} limit.`, }); return; } const imageAsURL = URL.createObjectURL(imageFile); - + const setters = { + logo: setImage, + banner: setBannerImage, + }; const img = new Image(); img.onload = function (e) { - // reduce image if necessary before upload - if (e.target.width > 150) { - getReducedImg(e.target, 150, 'community_image').then((result) => { - setImage({ imageUrl: imageAsURL, file: result.imageFile }); + // reduce images if necessary before upload + if (e.target.width > maxWidth) { + getReducedImg(e.target, maxWidth, filename).then((result) => { + setters[dataKey]({ + imageUrl: imageAsURL, + file: result.imageFile, + }); }); } else { - setImage({ imageUrl: imageAsURL, file: imageFile }); + setters[dataKey]({ imageUrl: imageAsURL, file: imageFile }); } }; img.src = imageAsURL; }); }, - [setImage, notifyError] + [setImage, setBannerImage, notifyError] ); - const { getRootProps, getInputProps } = useDropzone({ - onDrop, + const { getRootProps: getLogoRootProps, getInputProps: getLogoInputProps } = + useDropzone({ + onDrop: onDrop('community_image', 'logo', MAX_AVATAR_FILE_SIZE, 150), + maxFiles: 1, + accept: 'image/jpeg,image/png', + }); + + const { + getRootProps: getBannerRootProps, + getInputProps: getBannerInputProps, + } = useDropzone({ + onDrop: onDrop('community_banner', 'banner', MAX_FILE_SIZE, 1200), maxFiles: 1, accept: 'image/jpeg,image/png', }); + const imageDropClasses = classnames( + 'is-flex is-flex-direction-column is-align-items-center is-justify-content-center cursor-pointer rounded-lg', + { + 'border-dashed-dark': !bannerImage.file && !bannerImage.imageUrl, + } + ); + return ( - {!image && !isUpdating && ( + {!isUpdatingImage && !image?.imageUrl && !image?.file && ( <> Avatar - + )} {image?.imageUrl && ( @@ -164,7 +207,7 @@ function CommunityEditorProfile({ }} /> )} - {!(isUpdating && image.file) ? ( + {!isUpdatingImage && (image?.imageUrl || image?.file) && (
- + +
+ )} + {isUpdatingImage && ( +
+

Uploading...

+
+ )} + + + +
+
+
+ {!isUpdatingBanner && !bannerImage?.imageUrl && ( + <> + + Community Banner Image + + JPG or PNG 200px X 1200px recommended + + + + )} + {bannerImage?.imageUrl && ( +
+ )} + {!isUpdatingBanner && (bannerImage.imageUrl || bannerImage.file) && ( +
+ +
- ) : ( + )} + {isUpdatingBanner && (
{ + (filename, dataKey, maxFileSize, maxWidth) => (acceptedFiles) => { acceptedFiles.forEach((imageFile) => { // validate type if ( @@ -59,10 +60,12 @@ export default function StepOne({ return; } // validate size - if (imageFile.size > MAX_AVATAR_FILE_SIZE) { + if (imageFile.size > maxFileSize) { + const sizeLimit = + maxFileSize === MAX_AVATAR_FILE_SIZE ? '2MB' : '5MB'; notifyError({ status: 'Image file size not allowed', - statusText: 'The selected file exceeds the 2MB limit.', + statusText: `The selected file exceeds the ${sizeLimit} limit.`, }); return; } @@ -70,15 +73,17 @@ export default function StepOne({ const img = new Image(); img.onload = function (e) { - // reduce image if necessary before upload - if (e.target.width > 150) { - getReducedImg(e.target, 150, 'community_image').then((result) => { + // reduce images if necessary before upload + if (e.target.width > maxWidth) { + getReducedImg(e.target, maxWidth, filename).then((result) => { onDataChange({ - logo: { imageUrl: imageAsURL, file: result.imageFile }, + [dataKey]: { imageUrl: imageAsURL, file: result.imageFile }, }); }); } else { - onDataChange({ logo: { imageUrl: imageAsURL, file: imageFile } }); + onDataChange({ + [dataKey]: { imageUrl: imageAsURL, file: imageFile }, + }); } }; img.src = imageAsURL; @@ -87,8 +92,18 @@ export default function StepOne({ [onDataChange, notifyError] ); - const { getRootProps, getInputProps } = useDropzone({ - onDrop, + const { getRootProps: getLogoRootProps, getInputProps: getLogoInputProps } = + useDropzone({ + onDrop: onDrop('community_image', 'logo', MAX_AVATAR_FILE_SIZE, 150), + maxFiles: 1, + accept: 'image/jpeg,image/png', + }); + + const { + getRootProps: getBannerRootProps, + getInputProps: getBannerInputProps, + } = useDropzone({ + onDrop: onDrop('community_banner', 'banner', MAX_FILE_SIZE, 1200), maxFiles: 1, accept: 'image/jpeg,image/png', }); @@ -97,6 +112,7 @@ export default function StepOne({ communityName, communityDescription, logo, + banner, communityTerms, category, } = stepData || {}; @@ -147,6 +163,13 @@ export default function StepOne({ setStepValid(isValid && isCommunityLinksValid); }, [stepData, setStepValid, onDataChange, isCommunityLinksValid]); + const imageDropClasses = classnames( + 'is-flex is-flex-direction-column is-align-items-center is-justify-content-center cursor-pointer rounded-lg', + { + 'border-dashed-dark': !banner?.file, + } + ); + const showNameInputError = !checkValidNameLength(communityName ?? ''); return ( @@ -183,18 +206,11 @@ export default function StepOne({ width: '90px', overflow: 'hidden', position: 'relative', - ...(!logo ? { border: '1px dashed #757575' } : undefined), + ...(!logo ? { border: '2px dashed #757575' } : undefined), }} - {...getRootProps()} + {...getLogoRootProps()} > - {!logo && ( - <> - - Avatar - - - )} - {logo && ( + {logo ? (
+ ) : ( + <> + + Avatar + + )} {logo?.file ? (
- + +
+ ) : null} +
+
+
+
+
+
+ {banner ? ( +
+ ) : ( + <> + + Community Banner Image + + JPG or PNG 200px X 1200px recommended + + + + )} + {banner?.file ? ( +
+ +
) : null}
@@ -242,7 +312,7 @@ export default function StepOne({