From 16143bda8fb197e69070f423dcefd87dc897a5e5 Mon Sep 17 00:00:00 2001 From: NTB Date: Wed, 1 Jun 2022 10:43:12 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20add=20conversation=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/App.tsx | 5 +- components/Conversation/MessagesList.tsx | 28 +- components/ConversationFilter.tsx | 102 + components/ConversationFilterSlideOver.tsx | 359 + components/CyberConnectProvider.tsx | 152 + components/LitACLItem.tsx | 98 + components/NavigationPanel.tsx | 27 +- components/Selector.tsx | 81 + contexts/cyberConnect.ts | 58 + gql/GET_IDENTITY.ts | 76 + hooks/useCyberConnect.ts | 17 + next.config.js | 6 +- package-lock.json | 2044 ++++- package.json | 5 +- pnpm-lock.yaml | 9672 ++++++++++++++++++++ 15 files changed, 12281 insertions(+), 449 deletions(-) create mode 100644 components/ConversationFilter.tsx create mode 100644 components/ConversationFilterSlideOver.tsx create mode 100644 components/CyberConnectProvider.tsx create mode 100644 components/LitACLItem.tsx create mode 100644 components/Selector.tsx create mode 100644 contexts/cyberConnect.ts create mode 100644 gql/GET_IDENTITY.ts create mode 100644 hooks/useCyberConnect.ts create mode 100644 pnpm-lock.yaml diff --git a/components/App.tsx b/components/App.tsx index 37ac1e6..802fbee 100644 --- a/components/App.tsx +++ b/components/App.tsx @@ -1,4 +1,5 @@ import XmtpProvider from './XmtpProvider' +import CyberConnectProvider from './CyberConnectProvider' import Layout from '../components/Layout' import { WalletProvider } from './WalletProvider' @@ -10,7 +11,9 @@ function App({ children }: AppProps) { return ( - {children} + + {children} + ) diff --git a/components/Conversation/MessagesList.tsx b/components/Conversation/MessagesList.tsx index 997ab6c..ac98a62 100644 --- a/components/Conversation/MessagesList.tsx +++ b/components/Conversation/MessagesList.tsx @@ -25,18 +25,24 @@ const formatDate = (d?: Date) => const MessageTile = ({ message, isSender }: MessageTileProps): JSX.Element => (
- + + +
- + {formatTime(message.sent)}
- + {message.error ? ( `Error: ${message.error?.message}` ) : ( @@ -56,9 +62,9 @@ const DateDividerBorder: React.FC = ({ children }) => ( ) const DateDivider = ({ date }: { date?: Date }): JSX.Element => ( -
+
- + {formatDate(date)} @@ -66,8 +72,8 @@ const DateDivider = ({ date }: { date?: Date }): JSX.Element => ( ) const ConversationBeginningNotice = (): JSX.Element => ( -
- +
+ This is the beginning of the conversation
@@ -81,9 +87,9 @@ const MessagesList = ({ let lastMessageDate: Date | undefined return ( -
-
-
+
+
+
{messages && messages.length ? ( @@ -96,7 +102,7 @@ const MessagesList = ({ const dateHasChanged = !isOnSameDay(lastMessageDate, msg.sent) lastMessageDate = msg.sent return dateHasChanged - ? [, tile] + ? [, tile] : tile })}
diff --git a/components/ConversationFilter.tsx b/components/ConversationFilter.tsx new file mode 100644 index 0000000..22079a1 --- /dev/null +++ b/components/ConversationFilter.tsx @@ -0,0 +1,102 @@ +import { classNames } from '../helpers' +import { Fragment, useEffect, useState } from 'react' +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, SelectorIcon, FilterIcon } from '@heroicons/react/solid' +import ConversationFilterSlideOver from './ConversationFilterSlideOver' +import useCyberConnect from '../hooks/useCyberConnect' + +export default function ConversationFilter() { + const { filterBy, updateFilterBy } = useCyberConnect() + const items = ['friends', 'followings', 'followers', 'all'] + const [open, setOpen] = useState(false) + + useEffect(() => { + if (!filterBy) { + updateFilterBy(items[0]) + } + }) + return ( +
+
+
+ + {({ open }) => ( + <> + +
+ + {filterBy} + + + + + + + {items.map((name) => ( + + classNames( + active ? 'text-white bg-p-600' : 'text-gray-900', + 'cursor-pointer select-none relative py-2 pl-3 pr-9' + ) + } + value={name} + > + {({ selected, active }) => ( + <> + + {name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+ + )} +
+
+ +
+ setOpen(false)} open={open} /> +
+ ) +} diff --git a/components/ConversationFilterSlideOver.tsx b/components/ConversationFilterSlideOver.tsx new file mode 100644 index 0000000..beb3c49 --- /dev/null +++ b/components/ConversationFilterSlideOver.tsx @@ -0,0 +1,359 @@ +// @ts-nocheck +import { Fragment, useContext, useEffect, useState } from 'react' +import CyberConnectContext, { ConditionItem } from '../contexts/cyberConnect' +import { chainItems } from './CyberConnectProvider' +import { Dialog, Transition } from '@headlessui/react' +import { XIcon } from '@heroicons/react/outline' +import Selector from './Selector' +import LitACLItem from './LitACLItem' +import { PlusCircleIcon } from '@heroicons/react/solid' +import { booleanLogicItems } from '../contexts/cyberConnect' +// @ts-ignore +import LitJsSdk from 'lit-js-sdk' +import useXmtp from '../hooks/useXmtp' +import { ethers } from 'ethers' +import { union, intersection } from 'lodash' +const defaultConditionItem = { + id: '', + tokenId: '', + contractType: 'ERC721', + contractAddress: '', + comparator: '', + number: '', +} + +type ConversationFilterSlideOverProps = { + open: boolean + onClose: (value: boolean) => void +} + +export default function ConversationFilterSlideOver({ + open, + onClose, +}: ConversationFilterSlideOverProps) { + const { conversations } = useXmtp() + const [isLoading, setIsLoading] = useState(false) + + const { + booleanLogic, + conditionItems, + setBooleanLogic, + setConditionItems, + chainItem, + setChainItem, + allLitValidateAddress, + setAllLitValidateAddress, + } = useContext(CyberConnectContext) + + const addItem = () => { + const newItem = { + ...defaultConditionItem, + id: Date.now() + '' + Math.floor(Math.random() * 100000), + } + // @ts-ignore + const newItems: Array = [...conditionItems, newItem] + setConditionItems(newItems) + } + + const deleteItem = (id: string) => { + const items = conditionItems.filter((item) => item.id !== id) + setConditionItems(items) + } + + // @ts-ignore + const doSubmit = async (e) => { + e.preventDefault() + setIsLoading(true) + const client = new LitJsSdk.LitNodeClient({ + alertWhenUnauthorized: false, + }) + await client.connect() + const chain = chainItem?.id + const authSig = await LitJsSdk.checkAndSignAuthMessage({ chain }) + const allAddressArr = conversations.map((item) => item.peerAddress) + + const allValidateAddress = [] + const filterResultAddressArr = await Promise.all( + conditionItems.map(async (item) => { + const { contractAddress, comparator, contractType } = item + const verifiedRz = await Promise.all( + allAddressArr.map(async (userWalletAddress) => { + const accessControlConditions = [] + switch (contractType) { + case 'ETH': + accessControlConditions.push({ + contractAddress: '', + standardContractType: '', + chain, + method: 'eth_getBalance', + parameters: [userWalletAddress, 'latest'], + returnValueTest: { + comparator, + value: ethers.utils + .parseEther(item.number.toString()) + .toString(), + }, + }) + break + case 'ERC20': + accessControlConditions.push({ + contractAddress: '', + standardContractType: '', + chain, + method: 'balanceOf', + parameters: [userWalletAddress], + returnValueTest: { + comparator, + value: ethers.utils + .parseEther(item.number.toString()) + .toString(), + }, + }) + break + case 'ERC721': + accessControlConditions.push({ + contractAddress, + standardContractType: 'ERC721', + chain, + method: 'balanceOf', + parameters: [userWalletAddress], + returnValueTest: { + comparator, + value: item.number, + }, + }) + break + case 'ERC777': + accessControlConditions.push({ + contractAddress, + standardContractType: 'ERC777', + chain, + method: 'balanceOf', + parameters: [userWalletAddress], + returnValueTest: { + comparator, + value: item.number, + }, + }) + break + case 'ERC1155': + accessControlConditions.push({ + contractAddress, + standardContractType: 'ERC1155', + chain, + method: 'balanceOf', + parameters: [userWalletAddress, item.tokenId], + returnValueTest: { + comparator, + value: item.number, + }, + }) + break + } + + const randomNumber = Math.floor(Math.random() * 100000) + const resourceId = { + baseUrl: 'https://chat.xmtp.com', + path: `/${contractAddress}/${userWalletAddress}/${Date.now()}/${randomNumber}`, + orgId: '', + role: '', + extraData: '', + } + + try { + await client.saveSigningCondition({ + accessControlConditions, + chain, + authSig, + resourceId, + }) + const jwt = await client.getSignedToken({ + accessControlConditions, + chain, + authSig, + resourceId, + }) + + const { verified } = LitJsSdk.verifyJwt({ jwt }) + return [userWalletAddress, verified] + } catch (err) { + console.log('err', err) + + return [userWalletAddress, false] + } + }) + ) + const validateAddressArr = verifiedRz + .filter((item) => item[1] === true) + .map((item) => item[0]) + allValidateAddress.push(validateAddressArr) + return { + ...item, + validateAddressArr, + } + }) + ) + setConditionItems(filterResultAddressArr) + + setIsLoading(false) + } + useEffect(() => { + const funcMap = { + intersection, + union, + } + // @ts-ignore + const allValidateAddress = conditionItems.map( + // @ts-ignore + ({ validateAddressArr }) => validateAddressArr + ) + // @ts-ignore + setAllLitValidateAddress(funcMap[booleanLogic.id](...allValidateAddress)) + }, [booleanLogic, conditionItems, setAllLitValidateAddress]) + + return ( + + + {/* The backdrop, rendered as a fixed sibling to the panel container */} + + + ) +} diff --git a/components/CyberConnectProvider.tsx b/components/CyberConnectProvider.tsx new file mode 100644 index 0000000..8eb584a --- /dev/null +++ b/components/CyberConnectProvider.tsx @@ -0,0 +1,152 @@ +// @ts-nocheck +import { useCallback, useEffect, useState } from 'react' +// @ts-ignore +import LitJsSdk from 'lit-js-sdk' +import useXmtp from '../hooks/useXmtp' +import { + CyberConnectContext, + CyberConnectContextType, + BooleanLogic, + ChainItem, + ConditionItem, + booleanLogicItems, +} from '../contexts/cyberConnect' +import { GET_IDENTITY } from '../gql/GET_IDENTITY' + +import { + ApolloClient, + InMemoryCache, + ApolloProvider, + useLazyQuery, +} from '@apollo/client' +// import { Conversations } from '@xmtp/xmtp-js/dist/types/src' +const CYBERCONNECT_ENDPOINT = 'https://api.cybertino.io/connect/' + +const client = new ApolloClient({ + uri: CYBERCONNECT_ENDPOINT, + cache: new InMemoryCache(), +}) + +export const chainItems = Object.keys(LitJsSdk.ALL_LIT_CHAINS).map((key) => { + return { id: key, name: key } +}) + +export const CyberConnectProvider: React.FC = ({ children }) => { + const [filterBy, setFilterBy] = useState('friends') + const [allLitValidateAddress, setAllLitValidateAddress] = useState<[string]>([ + '', + ]) + const [chainItem, setChainItem] = useState({ + id: 'ethereum', + name: 'ethereum', + }) + const [booleanLogic, setBooleanLogic] = useState( + booleanLogicItems[0] + ) + const [conditionItems, setConditionItems] = useState([]) + const [categoryBy, setCategoryBy] = useState() + const [identity, setIdentity] = useState() + const { walletAddress } = useXmtp() + + const updateFilterBy = useCallback( + (newFilterBy) => { + setFilterBy(newFilterBy) + }, + [setFilterBy] + ) + + const [getIdentity, { loading, error, data }] = useLazyQuery(GET_IDENTITY, { + client, + }) + + useEffect(() => { + if (!walletAddress) return + + getIdentity({ + variables: { address: walletAddress }, + }) + }, [walletAddress, getIdentity]) + + const updateIdentity = useCallback( + (identity) => setIdentity(identity), + [setIdentity] + ) + + useEffect(() => { + if (!data) return + if (loading) return + if (error) { + console.error(error) + return + } + updateIdentity(data.identity) + }, [loading, error, data, updateIdentity]) + + const updateCategoryBy = useCallback( + (newCategoryBy) => { + setCategoryBy(newCategoryBy) + }, + [setCategoryBy] + ) + + const [providerState, setProviderState] = useState({ + filterBy, + booleanLogic, + conditionItems, + categoryBy, + identity, + setBooleanLogic, + setConditionItems, + updateFilterBy, + updateCategoryBy, + updateIdentity, + chainItem, + setChainItem, + allLitValidateAddress, + setAllLitValidateAddress, + }) + + useEffect(() => { + setProviderState({ + filterBy, + booleanLogic, + conditionItems, + categoryBy, + identity, + updateFilterBy, + setBooleanLogic, + setConditionItems, + updateCategoryBy, + updateIdentity, + chainItem, + setChainItem, + allLitValidateAddress, + setAllLitValidateAddress, + }) + }, [ + filterBy, + booleanLogic, + conditionItems, + categoryBy, + identity, + updateFilterBy, + setBooleanLogic, + setConditionItems, + updateCategoryBy, + updateIdentity, + chainItem, + setChainItem, + allLitValidateAddress, + setAllLitValidateAddress, + ]) + + return ( + + + {children} + + + ) +} + +export default CyberConnectProvider diff --git a/components/LitACLItem.tsx b/components/LitACLItem.tsx new file mode 100644 index 0000000..62d2532 --- /dev/null +++ b/components/LitACLItem.tsx @@ -0,0 +1,98 @@ +// @ts-nocheck +import Selector from './Selector' +import { MinusCircleIcon } from '@heroicons/react/solid' + +export default function LitACLItem(props) { + const contractTypeItems = [ + { id: 'ETH', name: 'ETH' }, + { id: 'ERC20', name: 'ERC20' }, + { id: 'ERC721', name: 'ERC721' }, + { id: 'ERC777', name: 'ERC777' }, + { id: 'ERC1155', name: 'ERC1155' }, + ] + const selected = { + id: props.contractType, + name: props.contractType, + } + + let contractAddressInput = '' + if (props.contractType !== 'ETH') { + contractAddressInput = ( + + props.onUpdate({ key: 'contractAddress', value: e.target.value }) + } + name="contractAddress" + placeholder="contract address" + className="flex-1 block mx-2 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm disabled:bg-gray-200 disabled:cursor-not-allowed" + /> + ) + } + + let tokenIdInput = '' + if (props.contractType === 'ERC1155') { + tokenIdInput = ( + + props.onUpdate({ key: 'tokenId', value: e.target.value }) + } + name="tokenId" + placeholder="tokenId" + className="flex-1 block mx-2 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm disabled:bg-gray-200 disabled:cursor-not-allowed" + /> + ) + } + + let resultCount = '' + if (props.validateAddressArr) { + resultCount =
{props.validateAddressArr.length}
+ } + return ( +
+ {resultCount} +
+ { + props.onUpdate({ key: 'contractType', value: item.id }) + if (item.id === 'ETH') { + props.onUpdate({ key: 'contractAddress', value: '' }) + } + }} + > +
+ {contractAddressInput} + {tokenIdInput} + + props.onUpdate({ key: 'comparator', value: e.target.value }) + } + placeholder="comparator, must be one of <, <=, =, >=, >" + className="flex-1 block mx-2 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + props.onUpdate({ key: 'number', value: e.target.value }) + } + placeholder="number" + className="block w-40 mx-2 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + props.onDelete(props.id)} + /> +
+ ) +} diff --git a/components/NavigationPanel.tsx b/components/NavigationPanel.tsx index 5548b0a..36f8c3f 100644 --- a/components/NavigationPanel.tsx +++ b/components/NavigationPanel.tsx @@ -3,6 +3,8 @@ import { ChatIcon } from '@heroicons/react/outline' import { ArrowSmRightIcon } from '@heroicons/react/solid' import useXmtp from '../hooks/useXmtp' import ConversationsList from './ConversationsList' +import ConversationFilter from './ConversationFilter' + import Loader from './Loader' type NavigationPanelProps = { @@ -17,7 +19,7 @@ const NavigationPanel = ({ onConnect }: NavigationPanelProps): JSX.Element => { const { walletAddress } = useXmtp() return ( -
+
{walletAddress ? ( ) : ( @@ -31,16 +33,16 @@ const NavigationPanel = ({ onConnect }: NavigationPanelProps): JSX.Element => { const NoWalletConnectedMessage: React.FC = ({ children }) => { return ( -
+
@@ -53,9 +55,9 @@ const ConnectButton = ({ onConnect }: ConnectButtonProps): JSX.Element => { return (