From f42fd1fec029481ce1c6bc82bb4a63fd9d93ef89 Mon Sep 17 00:00:00 2001 From: Monalisha Mishra <42746736+mishramonalisha76@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:55:55 +0530 Subject: [PATCH] Create group chat comp (#778) * Chat profile refractor (#727) * fix: fixed minor bugs and made add wallets reusable * fix: refactored code * fix: fixed ens domains not working while adding more wallets to a group * fix: fixed back btn in add wallet and cofirm btn ui * fix: fixed theme in chat profile * fix: refractor of addWallet * fix: ens issues * fix: adde changes * fix: fixed lint issues * fix: fixed review comments * fix: fixed minor review comments --------- Co-authored-by: KlausMikhaelson * Refactoring of messageInput, chatBubble, connectButton,chatViewList, chatViewComponent (#718) * fix: added accoutn hook * fix: refactoring code of messageInput chatlist chatbubble * fix: fixed review comment * fix: fixed * fix: added reusables * fix: fixed toast * feat: create group modal * fix: fixed condition in messageInput * fix: fixed * fix: fixed code * feat: create group info validation done (#738) * fix: fixed autoconnect issues * Group criteria added (#764) * feat: created define criteria and add criteria * feat: added some more dropdowns in add criteria * fix: added critera options * feat: worked on criterias * Arbitrum changes (#735) * fix: inital implementation for arbitrum changes * fix: more changes * fix: added final changes * fix: fixed few ui issues * fix: fixed minor issue * fix: added operstor * fix: added dummy 2d data * fix: fixed test * feat: condition type added * feat: adder done * feat: all/any added on condition box * fix: minor fixes * fix: fixed typo * fix: fixed upload group image ui * Multiple group criteria (#747) * fix: fixed multiple criteria Ui * fix: fixed conditions code --------- Co-authored-by: KlausMikhaelson * docs: added Class Examples (#739) * docs: added documentation * docs: fixed chat class examples * docs: fix notif class * docs: fixed stream examples * docs: fix nft grp update * docs: fixed logs * Message Type Implementations (#730) * fix: added video & audio messages * fix: fixed meta, reaction & added intent & readReceipt * fix: added reply * fix: added composite, fixed receipt * fix: local tests (#744) * fix(env and index): fixes typo RECIPEINT to RECIPIENT (#743) fix #733 * Update README.md (#742) Changed the discord link * feat: add criteria done * fix: fixed themes * feat: rule deletion done * feat: condition update done * feat: add criteria update lablel done * feat: edit rule auto fill done * feat: pr changes done * feat: refactor type and hooks * fix: fixed mobilde view * fix: fixed scroll * feat: added icon in groupinfo to show if it is token gated or not (#748) * feat: added icon in groupinfo to show if it is token gated or not * fix: fixed minor text * feat: created group criteria info modal and fixed dropdown * fix: made changes as per the review * fix: removed more options from conditions in group info * fix: fixed minor issues --------- Co-authored-by: Monalisha Mishra * feat: drop down labels * fix: fixed * fix: fixed dropdown * Gp states fix (#763) * feat: conditions to chat added * feat: generation of payload done * refactor: rule generation done * feat: operator undefined fix * fix: fixed condition ui * feat: create group rest api call done * fix: fixed dropdown * feat: group detail info fix done * fix: added validation for guild --------- Co-authored-by: Abishek Bashyal --------- Co-authored-by: KlausMikhaelson Co-authored-by: Ashis Kumar Pradhan <38760485+akp111@users.noreply.github.com> Co-authored-by: Abishek Bashyal Co-authored-by: akp111 Co-authored-by: Aman Gupta Co-authored-by: strykerin Co-authored-by: Rahul Pandey Co-authored-by: dinesh <67892133+dinesh11515@users.noreply.github.com> Co-authored-by: Satyam <100528412+KlausMikhaelson@users.noreply.github.com> * fix: fixed * feat: group msg fix (#765) * feat: group msg fix * fix: minor changes --------- Co-authored-by: Monalisha Mishra * fix: added validation for group name and group desc * fix: fixed scroll * fix: added validation for custom endpoint * fix: fixed specific value for guild while edit * Create group chat comp contract verify (#772) * feat: contract fectch done * feat: contract fectch done * feat: token validation done * feat: token symbol display done * feat: duplicate rules fixed * fix: fixed ui issues * feat: added token truncation * feat: token default value done * fix: fixed minor issues --------- Co-authored-by: Monalisha Mishra * feat: infura key loaded (#780) --------- Co-authored-by: KlausMikhaelson Co-authored-by: Abishek Bashyal Co-authored-by: Ashis Kumar Pradhan <38760485+akp111@users.noreply.github.com> Co-authored-by: akp111 Co-authored-by: Aman Gupta Co-authored-by: strykerin Co-authored-by: Rahul Pandey Co-authored-by: dinesh <67892133+dinesh11515@users.noreply.github.com> Co-authored-by: Satyam <100528412+KlausMikhaelson@users.noreply.github.com> --- .../sdk-frontend-react/src/app/app.tsx | 2 +- .../chat/CreateGroup/AddCriteria.tsx | 434 ++++++++++++------ .../chat/CreateGroup/ConditionsComponent.tsx | 23 +- .../chat/CreateGroup/CreateGroupModal.tsx | 96 ++-- .../chat/CreateGroup/CreateGroupType.tsx | 155 ++++--- .../chat/MessageInput/MessageInput.tsx | 1 - .../chat/helpers/tokenGatedGroup.ts | 131 +++++- .../chat/helpers/tokenHelpers/abi.ts | 32 ++ .../chat/helpers/tokenHelpers/chain.ts | 33 ++ .../chat/helpers/tokenHelpers/fetch.ts | 114 +++++ .../chat/helpers/tokenHelpers/index.ts | 3 + .../chat/reusables/DropDownInput.tsx | 13 +- .../chat/reusables/OptionButtons.tsx | 7 +- .../chat/reusables/QuantityInput.tsx | 19 +- .../components/chat/reusables/TextArea.tsx | 7 +- .../components/chat/reusables/TextInput.tsx | 6 +- .../src/lib/components/chat/theme/index.ts | 3 + .../chat/types/tokenGatedGroupCreationType.ts | 19 +- .../src/lib/hooks/chat/useCreateGatedGroup.ts | 5 +- .../src/lib/hooks/chat/useCriteriaState.ts | 35 ++ 20 files changed, 832 insertions(+), 306 deletions(-) create mode 100644 packages/uiweb/src/lib/components/chat/helpers/tokenHelpers/abi.ts create mode 100644 packages/uiweb/src/lib/components/chat/helpers/tokenHelpers/chain.ts create mode 100644 packages/uiweb/src/lib/components/chat/helpers/tokenHelpers/fetch.ts create mode 100644 packages/uiweb/src/lib/components/chat/helpers/tokenHelpers/index.ts diff --git a/packages/examples/sdk-frontend-react/src/app/app.tsx b/packages/examples/sdk-frontend-react/src/app/app.tsx index 98a5bd2c7..e4e7d2303 100644 --- a/packages/examples/sdk-frontend-react/src/app/app.tsx +++ b/packages/examples/sdk-frontend-react/src/app/app.tsx @@ -313,7 +313,7 @@ export function App() { - + { const [selectedTypeValue, setSelectedTypeValue] = useState(0); + const [validationErrors, setValidationErrors] = + useState({}); const [selectedCategoryValue, setSelectedCategoryValue] = useState(0); const [selectedSubCategoryValue, setSelectedSubCategoryValue] = useState(0); - const [guildComparison, setGuildComparison] = useState('') + const [validationLoading, setValidationLoading] = useState(false); + const [guildComparison, setGuildComparison] = useState(''); const [selectedChainValue, setSelectedChainValue] = useState(0); const [contract, setContract] = useState(''); const [inviteCheckboxes, setInviteCheckboxes] = useState<{ @@ -56,6 +70,8 @@ const AddCriteria = ({ const [url, setUrl] = useState(''); const [guildId, setGuildId] = useState(''); const [specificRoleId, setSpecificRoleId] = useState(''); + const [unit, setUnit] = useState('TOKEN'); + const [decimals, setDecimals] = useState(18); const [quantity, setQuantity] = useState<{ value: number; range: number }>({ value: 0, @@ -63,6 +79,7 @@ const AddCriteria = ({ }); const { env } = useChatData(); const theme = useContext(ThemeContext); + const groupInfoToast = useToast(); const isMobile = useMediaQuery(device.mobileL); @@ -151,23 +168,15 @@ const AddCriteria = ({ }, }; - const tokenCategoryValues = [ - { - id: 0, + const dropdownSubCategoryValues: DropdownSubCategoryValuesType = { + ERC20: { value: SUBCATEGORY.HOLDER, title: 'Holder', - function: () => setSelectedSubCategoryValue(0), }, - { - id: 1, + ERC721:{ value: SUBCATEGORY.OWENER, title: 'Owner', - function: () => setSelectedSubCategoryValue(1), }, - ]; - const dropdownSubCategoryValues: DropdownSubCategoryValuesType = { - ERC20: tokenCategoryValues, - ERC721: tokenCategoryValues, INVITE: { value: SUBCATEGORY.DEFAULT, title: 'Default', @@ -284,133 +293,207 @@ const AddCriteria = ({ setQuantity({ ...quantity, value: e.target.value }); }; - const verifyAndDoNext = ()=>{ - const _type = dropdownTypeValues[selectedTypeValue].value as 'PUSH' | 'GUILD' - const category:string = _type === "PUSH" ? (dropdownCategoryValues[_type] as DropdownValueType[])[ - selectedCategoryValue - ].value || CATEGORY.ERC20 : "ROLES" - - let subCategory = "DEFAULT" - if(_type === "PUSH"){ - if(category === CATEGORY.ERC20 || category === CATEGORY.ERC721){ - subCategory = tokenCategoryValues[selectedSubCategoryValue].value - }else if(category === CATEGORY.CustomEndpoint){ - subCategory = "GET" - } + const verifyAndDoNext = async () => { + setValidationLoading(true); + const _type = dropdownTypeValues[selectedTypeValue].value as + | 'PUSH' + | 'GUILD'; + const category: string = + _type === 'PUSH' + ? (dropdownCategoryValues[_type] as DropdownValueType[])[ + selectedCategoryValue + ].value || CATEGORY.ERC20 + : 'ROLES'; + + let subCategory = 'DEFAULT'; + if (_type === 'PUSH') { + if (category === CATEGORY.ERC20 || category === CATEGORY.ERC721) { + subCategory = category === CATEGORY.ERC20 ? SUBCATEGORY.HOLDER : SUBCATEGORY.OWENER + } else if (category === CATEGORY.CustomEndpoint) { + subCategory = 'GET'; + } } - - const getData = (type:string, category:string):Data=>{ - if(type === "PUSH"){ - if(category === CATEGORY.ERC20 || category === CATEGORY.ERC721){ - const selectedChain = dropdownChainsValues[selectedChainValue].value || 'eip155:1'; + + const getData = (type: string, category: string): Data => { + if (type === 'PUSH') { + if (category === CATEGORY.ERC20 || category === CATEGORY.ERC721) { + const selectedChain = + dropdownChainsValues[selectedChainValue].value || 'eip155:1'; return { contract: `${selectedChain}:${contract}`, amount: quantity.value, - comparison:dropdownQuantityRangeValues[quantity.range].value, - decimals: 18, - } - }else if(category === CATEGORY.INVITE){ - const _inviteRoles = [] - if(inviteCheckboxes.admin){ - _inviteRoles.push("ADMIN") + comparison: dropdownQuantityRangeValues[quantity.range].value, + decimals: category === CATEGORY.ERC20 ? decimals : undefined, + token: unit, + }; + } else if (category === CATEGORY.INVITE) { + const _inviteRoles = []; + if (inviteCheckboxes.admin) { + _inviteRoles.push('ADMIN'); } - if(inviteCheckboxes.owner){ - _inviteRoles.push("OWNER") + if (inviteCheckboxes.owner) { + _inviteRoles.push('OWNER'); } - return{ - inviterRoles: _inviteRoles as ['OWNER' | 'ADMIN'] - } - }else{ - // CATEGORY.CustomEndpoint - // TODO: validate url - return{ - url:url - } + return { + inviterRoles: _inviteRoles as ['OWNER' | 'ADMIN'], + }; + } else { + return { + url: url, + }; } - }else{ - // GUILD type + } else { return { - id:guildId, - comparison:guildComparison, - role:guildComparison === 'specific' ? specificRoleId : "*", - } + id: guildId, + comparison: guildComparison === 'specific' ? '' : guildComparison, + role: guildComparison === 'specific' ? specificRoleId : '*', + }; } - } + }; - const rule:Rule = { + const rule: Rule = { type: _type, category: category, subcategory: subCategory, data: getData(_type, category), + }; + + //guild validation added + const errors = await validationCriteria(rule); + setValidationLoading(false); + if (Object.keys(errors).length) { + setValidationErrors(errors); + } else { + const isSuccess = criteriaState.addNewRule(rule); + if (!isSuccess) { + showError('Selected Criteria was already added'); + return; + } + if (handlePrevious) { + handlePrevious(); + } } + }; - criteriaState.addNewRule(rule) + const criteriaState = criteriaStateManager.getSelectedCriteria(); - if(handlePrevious){ - handlePrevious() - } + // Autofill the form for the update + useEffect(() => { + if (criteriaState.isUpdateCriteriaEnabled()) { + //Load the states + const oldValue = + criteriaState.selectedRules[criteriaState.updateCriteriaIdx]; + + if (oldValue.type === 'PUSH') { + // category + setSelectedCategoryValue( + (dropdownCategoryValues.PUSH as DropdownValueType[]).findIndex( + (obj) => obj.value === oldValue.category + ) + ); - } + const pushData = oldValue.data as PushData; - const criteriaState = criteriaStateManager.getSelectedCriteria() + // sub category + if ( + oldValue.category === CATEGORY.ERC20 || + oldValue.category === CATEGORY.ERC721 + ) { + + if(pushData.token){ + setUnit(pushData.token) + } + if (pushData.decimals) { + setDecimals(decimals); + } - // Autofill the form for the update - useEffect(()=>{ - if(criteriaState.isUpdateCriteriaEnabled()){ - //Load the states - const oldValue = criteriaState.selectedRules[criteriaState.updateCriteriaIdx] - - if(oldValue.type === 'PUSH'){ - - // category - setSelectedCategoryValue( - (dropdownCategoryValues.PUSH as DropdownValueType[]).findIndex(obj => obj.value === oldValue.category) - ) - - const pushData = oldValue.data as PushData - - // sub category - if(oldValue.category === CATEGORY.ERC20 || oldValue.category === CATEGORY.ERC721){ - setSelectedSubCategoryValue( - tokenCategoryValues.findIndex(obj => obj.value === oldValue.subcategory) - ) - - const contractAndChain:string[] = (pushData.contract || "eip155:1:0x").split(':') - setSelectedChainValue( - dropdownChainsValues.findIndex( - obj => obj.value === contractAndChain[0]+":"+contractAndChain[1] - ) - ) - setContract(contractAndChain.length === 3 ? contractAndChain[2]: "") - setQuantity({ - value:pushData.amount || 0, - range:dropdownQuantityRangeValues.findIndex( - obj => obj.value === pushData.comparison - ) - }) - }else if(oldValue.category === CATEGORY.INVITE){ - setInviteCheckboxes({ - admin:true, - owner:true, - }) - }else{ - // invite - setUrl(pushData.url || "") + // TODO: make helper function for this + const contractAndChain: string[] = ( + pushData.contract || 'eip155:1:0x' + ).split(':'); + setSelectedChainValue( + dropdownChainsValues.findIndex( + (obj) => + obj.value === contractAndChain[0] + ':' + contractAndChain[1] + ) + ); + setContract(contractAndChain.length === 3 ? contractAndChain[2] : ''); + setQuantity({ + value: pushData.amount || 0, + range: dropdownQuantityRangeValues.findIndex( + (obj) => obj.value === pushData.comparison + ), + }); + } else if (oldValue.category === CATEGORY.INVITE) { + setInviteCheckboxes({ + admin: true, + owner: true, + }); + } else { + // invite + setUrl(pushData.url || ''); + } + } else { + // guild condition + setGuildId((oldValue.data as GuildData).id); + setSpecificRoleId((oldValue.data as GuildData).role); + setGuildComparison(((oldValue.data as GuildData).comparison) || GUILD_COMPARISON_OPTIONS[2].value); } - }else{ - // guild condition - setGuildId((oldValue.data as GuildData).id) - setSpecificRoleId((oldValue.data as GuildData).role) - setGuildComparison((oldValue.data as GuildData).comparison) + + setSelectedTypeValue( + dropdownTypeValues.findIndex((obj) => obj.value === oldValue.type) + ); } - - setSelectedTypeValue( - dropdownTypeValues.findIndex(obj => obj.value === oldValue.type) - ) - } - },[]) + }, []); + + const getSeletedType = () => { + return dropdownTypeValues[selectedTypeValue].value || 'PUSH'; + }; + + const getSelectedCategory = () => { + const category: string = + (dropdownCategoryValues['PUSH'] as DropdownValueType[])[ + selectedCategoryValue + ].value || CATEGORY.ERC20; + + return category; + }; + + const getSelectedChain = () => { + return dropdownChainsValues[selectedChainValue].value || 'eip155:1'; + }; + + // Fetch the contract info + useEffect(() => { + // TODO: optimize to reduce this call call when user is typing + (async()=>{ + setValidationErrors(prev => ({...prev, tokenError:undefined})) + + const _type = getSeletedType(); + const _category: string = getSelectedCategory(); + const _chainInfo = getSelectedChain(); + + await tokenFetchHandler( + contract, + _type, + _category, + _chainInfo, + setUnit, + setDecimals + ); + })(); + }, [contract, selectedCategoryValue, selectedChainValue]); + + const showError = (errorMessage: string) => { + groupInfoToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: errorMessage, + toastType: 'ERROR', + getToastIcon: (size) => , + }); + }; return (
- setContract(e.target.value)} - placeholder="e.g. 0x123..." - /> +
+ setContract(e.target.value)} + placeholder="e.g. 0x123..." + error={!!validationErrors?.tokenError} + /> + {!!validationErrors?.tokenError && ( + {validationErrors?.tokenError} + )} +
+
+ {!!validationErrors?.tokenAmount && ( + {validationErrors?.tokenAmount} + )} +
)} {checkIfCustomEndpoint() && ( +
setUrl(e.target.value)} placeholder="e.g. abc.com" + error={!!validationErrors?.url} /> + {!!validationErrors?.url && ( + {validationErrors?.url} + )} +
)} {checkIfPushInvite() && (
@@ -505,8 +612,8 @@ const AddCriteria = ({ } onToggle={() => setInviteCheckboxes({ - admin:true, - owner:true + admin: true, + owner: true, }) } checked={ @@ -519,34 +626,52 @@ const AddCriteria = ({ {checkIfGuild() && ( <> - setGuildId(e.target.value)} - placeholder="e.g. 4687" - /> - { - setGuildComparison(newEl)}} - /> - - {guildComparison === "specific" && +
setSpecificRoleId(e.target.value)} + labelName="ID" + inputValue={guildId} + onInputChange={(e: any) => setGuildId(e.target.value)} placeholder="e.g. 4687" + error={!!validationErrors?.guildId} /> - } - - + {!!validationErrors?.guildId && ( + {validationErrors?.guildId} + )} +
+
+ { + setGuildComparison(newEl); + }} + /> + {!!validationErrors?.guildComparison && ( + {validationErrors?.guildComparison} + )} +
+ {guildComparison === 'specific' && ( +
+ setSpecificRoleId(e.target.value)} + placeholder="e.g. 4687" + error={!!validationErrors?.guildRole} + /> + {!!validationErrors?.guildRole && ( + {validationErrors?.guildRole} + )} +
+ )} )}
@@ -555,3 +680,8 @@ const AddCriteria = ({ export default AddCriteria; +const ErrorSpan = styled(Span)` + font-size: 12px; + font-weight: 500; + color: #ed5858; +`; diff --git a/packages/uiweb/src/lib/components/chat/CreateGroup/ConditionsComponent.tsx b/packages/uiweb/src/lib/components/chat/CreateGroup/ConditionsComponent.tsx index 4efb0d594..db704210a 100644 --- a/packages/uiweb/src/lib/components/chat/CreateGroup/ConditionsComponent.tsx +++ b/packages/uiweb/src/lib/components/chat/CreateGroup/ConditionsComponent.tsx @@ -8,7 +8,7 @@ import { ThemeContext } from '../theme/ThemeProvider'; import Dropdown, { DropdownValueType } from '../reusables/DropDown'; import { ConditionArray, ConditionData, IChatTheme } from '../exportedTypes'; import { useClickAway } from '../../../hooks'; -import { CATEGORY, CRITERIA_TYPE, CriteriaType, TOKEN_NFT_COMPARISION, TokenNftComparision } from '../types'; +import { CATEGORY, CRITERIA_TYPE, CriteriaType, PushData, TOKEN_NFT_COMPARISION, TokenNftComparision } from '../types'; import EditSvg from '../../../icons/EditSvg.svg'; import RemoveSvg from '../../../icons/RemoveSvg.svg'; @@ -86,14 +86,31 @@ const CriteriaSection = ({ criteria }: { criteria: ConditionData }) => { }; const getGuildRole = () =>{ + console.log(criteria?.data?.['comparison']) + if(!criteria?.data?.['comparison']) + { + return 'SPECIFIC'; + } return (GUILD_COMPARISON_OPTIONS.find(option => option.value === criteria?.data?.['comparison']))?.heading; } + + const getTokenSymbol = (conditionData:ConditionData)=>{ + if(conditionData.data){ + const data:PushData = conditionData.data; + if(data.token){ + return shortenText(data.token, 15) + } + } + + return conditionData.category + } + return (
{ {getTokenNftComparisionLabel()}{' '} {/* need to fetch token symbol */} - {criteria?.data?.['amount']} {criteria.category} + {criteria?.data?.['amount']} {getTokenSymbol(criteria)} )} {criteria.category === CATEGORY.INVITE && ( diff --git a/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupModal.tsx b/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupModal.tsx index 9b2ad7045..49add35d8 100644 --- a/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupModal.tsx +++ b/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupModal.tsx @@ -26,6 +26,7 @@ import { import { Image } from '../../../config/styles'; import { ProfilePicture, device } from '../../../config'; +import { CriteriaValidationErrorType } from '../types'; export const CREATE_GROUP_STEP_KEYS = { INPUT_DETAILS: 1, @@ -69,12 +70,12 @@ export const CreateGroupModal: React.FC = ({ } }, [activeComponent]); - const [groupInputDetails, setGroupInputDetails] = - useState({ - groupName: '', - groupDescription: '', - groupImage: '', - }); + const useDummyGroupInfo = false; + const [groupInputDetails, setGroupInputDetails] = useState({ + groupName: useDummyGroupInfo ? 'This is duumy group name' : '', + groupDescription: useDummyGroupInfo ? 'This is dummy group description for testing' : '', + groupImage: useDummyGroupInfo ? ProfilePicture : '' + }) const renderComponent = () => { switch (activeComponent) { @@ -162,7 +163,8 @@ const CreateGroupDetail = ({ const groupInfoToast = useToast(); const { groupName, groupDescription, groupImage } = groupInputDetails; const theme = useContext(ThemeContext); - + const [validationErrors, setValidationErrors] = + useState({}); const fileUploadInputRef = useRef(null); const isMobile = useMediaQuery(device.mobileL); @@ -205,21 +207,20 @@ const CreateGroupDetail = ({ if (!skipVerify) { // verify name if (groupName.trim().length === 0) { - showError('Group Name is empty'); + setValidationErrors({ + groupName: 'Group name cannot be empty', + }); return; } // verify description if (groupDescription.trim().length === 0) { - showError('Group Description is empty'); + setValidationErrors({ + groupDescription: 'Group Description is empty', + }); return; } - // verify description - // if (!groupImage) { - // showError("Group image can't be empty"); - // return; - // } } if (handleNext) { @@ -268,31 +269,42 @@ const CreateGroupDetail = ({ onChange={(e) => handleChange(e as unknown as Event)} /> - - setGroupInputDetails({ - groupDescription, - groupName: e.target.value, - groupImage, - }) - } - /> - -