From 6759d7522fb99f191f125c3a9c24b51e1914c04b Mon Sep 17 00:00:00 2001 From: Odafe Aror Date: Sun, 10 Mar 2024 08:26:59 +0000 Subject: [PATCH] refactor to have app context --- frontend/src/app/page.tsx | 14 +- frontend/src/app/providers.tsx | 3 +- .../src/components/web3/chat-messages.tsx | 123 ++-------- .../components/web3/new-chatroom-button.tsx | 86 ++----- frontend/src/components/web3/send-message.tsx | 36 +-- frontend/src/config/environment.ts | 10 + frontend/src/context/app-context.tsx | 222 ++++++++++++++++++ 7 files changed, 291 insertions(+), 203 deletions(-) create mode 100644 frontend/src/context/app-context.tsx diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3a73d5e..5d24124 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,16 +1,20 @@ 'use client' -import { useEffect } from 'react' +import { useContext, useEffect } from 'react' +import { AppContext } from '@/context/app-context' import { useInkathon } from '@scio-labs/use-inkathon' import { toast } from 'react-hot-toast' +import { Spinner } from '@/components/ui/spinner' import { ChatMessages } from '@/components/web3/chat-messages' import { ConnectButton } from '@/components/web3/connect-button' import { HomePageTitle } from './components/home-page-title' export default function HomePage() { + const { isAppLoading } = useContext(AppContext) + // Display `useInkathon` error messages (optional) const { error } = useInkathon() useEffect(() => { @@ -18,6 +22,14 @@ export default function HomePage() { toast.error(error.message) }, [error]) + // Connection Loading Indicator + if (isAppLoading) + return ( +
+ +
+ ) + return ( <>
diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index cd0f0f5..51c59ef 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react' +import { AppProvider } from '@/context/app-context' import { getDeployments } from '@/deployments/deployments' import { UseInkathonProvider } from '@scio-labs/use-inkathon' @@ -15,7 +16,7 @@ export default function ClientProviders({ children }: PropsWithChildren) { defaultChain={env.defaultChain} deployments={getDeployments()} > - {children} + {children} ) } diff --git a/frontend/src/components/web3/chat-messages.tsx b/frontend/src/components/web3/chat-messages.tsx index e87993d..468b441 100644 --- a/frontend/src/components/web3/chat-messages.tsx +++ b/frontend/src/components/web3/chat-messages.tsx @@ -1,121 +1,38 @@ 'use client' -import { FC, useEffect, useState } from 'react' +import { FC, useContext } from 'react' -import { ContractIds } from '@/deployments/deployments' -import ChatroomContract from '@inkathon/contracts/typed-contracts/contracts/chatroom' -import { - contractQuery, - decodeOutput, - useInkathon, - useRegisteredContract, - useRegisteredTypedContract, -} from '@scio-labs/use-inkathon' -import toast from 'react-hot-toast' +import { AppContext } from '@/context/app-context' +import { useInkathon } from '@scio-labs/use-inkathon' +import { BsChatSquareQuote } from 'react-icons/bs' import { Card, CardContent } from '@/components/ui/card' import { ScrollArea } from '@/components/ui/scroll-area' import { Spinner } from '@/components/ui/spinner' -import { Message, MessageProps } from './message' +import { Message } from './message' import { SendMessage } from './send-message' export const ChatMessages: FC = () => { - const { api, activeChain, activeAccount, activeSigner } = useInkathon() - const { contract, address: contractAddress } = useRegisteredContract(ContractIds.Chatroom) - const { typedContract } = useRegisteredTypedContract(ContractIds.Chatroom, ChatroomContract) - const [fetchIsLoading, setFetchIsLoading] = useState() - const [activeChatroom, setActiveChatroom] = useState(false) - const [chatroomId, setChatroomId] = useState('5CqRGE6QMZUxh8anBchE69P8gt3sojPtwNQkmpKwWPz9yPRB') - const [messages, setMessages] = useState([]) - - // fetch chatroom - const fetchChatroom = async () => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') - return - } - - setFetchIsLoading(true) - - try { - const result = await contractQuery(api, activeAccount.address, contract, 'getChatroom', {}, [ - chatroomId, - ]) - const { output, isError, decodedOutput } = decodeOutput(result, contract, 'getChatroom') - if (isError) throw new Error(decodedOutput) - console.log('chatroom: ', output) - fetchMessages() - setActiveChatroom(true) - } catch (e) { - console.error(e) - toast.error('Error while loading chatroom. Try again…') - setFetchIsLoading(false) - } finally { - setFetchIsLoading(false) - } - } - - useEffect(() => { - console.log('chatroom status before', activeChatroom) - fetchChatroom() - console.log('chatroom status', activeChatroom) - }, [api, activeAccount, contract, activeSigner]) - - // const fetchGreeting = async () => { - // if (!contract || !typedContract || !api) return - - // setFetchIsLoading(true) - // try { - // const result = await contractQuery(api, '', contract, 'greet') - // const { output, isError, decodedOutput } = decodeOutput(result, contract, 'greet') - // if (isError) throw new Error(decodedOutput) - // setGreeterMessage(output) - - // // Alternatively: Fetch it with typed contract instance - // const typedResult = await typedContract.query.greet() - // console.log('Result from typed contract: ', typedResult.value) - // } catch (e) { - // console.error(e) - // toast.error('Error while fetching greeting. Try again…') - // setGreeterMessage(undefined) - // } finally { - // setFetchIsLoading(false) - // } - // } - - // Fetch messages - const fetchMessages = async () => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') - return - } - - try { - const result = await contractQuery(api, activeAccount.address, contract, 'getMessages', {}, [ - chatroomId, - ]) - const { output, isError, decodedOutput } = decodeOutput(result, contract, 'getMessages') - if (isError) throw new Error(decodedOutput) - console.log('messages: ', output) - setMessages(output) - } catch (e) { - console.error(e) - toast.error('Error while loading chatroom. Try again…') - setFetchIsLoading(false) - } finally { - setFetchIsLoading(false) - } - } + const { activeChain } = useInkathon() + const { isChatroomLoading, isChatroomActive, messages, isMessagesLoading } = + useContext(AppContext) + console.log('ischatroomactive', isChatroomActive) // Connection Loading Indicator - if (fetchIsLoading || !activeChatroom) + if (isChatroomLoading) return ( -
+
-
- Connecting to {activeChain?.name} ({activeChain?.rpcUrls?.[0]}) -
+
Connecting to chatrooms
+
+ ) + if (!isChatroomLoading && !isChatroomActive) + // No chatroom found + return ( +
+ +
No chatroom found
) diff --git a/frontend/src/components/web3/new-chatroom-button.tsx b/frontend/src/components/web3/new-chatroom-button.tsx index d18f4ea..0ceacef 100644 --- a/frontend/src/components/web3/new-chatroom-button.tsx +++ b/frontend/src/components/web3/new-chatroom-button.tsx @@ -1,16 +1,10 @@ -import { useState } from 'react' +import { useContext, useState } from 'react' -import { ContractIds } from '@/deployments/deployments' -import ChatroomContract from '@inkathon/contracts/typed-contracts/contracts/chatroom' -import { - useInkathon, - useRegisteredContract, - useRegisteredTypedContract, -} from '@scio-labs/use-inkathon' -import toast from 'react-hot-toast' +import { AppContext } from '@/context/app-context' +import { useInkathon, useRegisteredContract } from '@scio-labs/use-inkathon' import { FiChevronDown } from 'react-icons/fi' -import { contractTxWithToast } from '@/utils/contract-tx-with-toast' +import { env } from '@/config/environment' import { Button } from '../ui/button' import { @@ -22,81 +16,31 @@ import { import { Input } from '../ui/input' const NewChatRoomButton = () => { - const { api, activeAccount, activeSigner } = useInkathon() - const { contract, address: contractAddress } = useRegisteredContract(ContractIds.Chatroom) - const { typedContract } = useRegisteredTypedContract(ContractIds.Chatroom, ChatroomContract) - const [chatroomId, setChatroomId] = useState('5CqRGE6QMZUxh8anBchE69P8gt3sojPtwNQkmpKwWPz9yPRB') + const { activeAccount } = useInkathon() const [participantId, setParticipantId] = useState() - - const supportedChains: any[] = [ - { - name: 'Aleph Zero', - description: 'Lorem ipsum', - }, - { - name: 'Accurast', - description: 'Lorem ipsum', - }, - ] + const { isChatroomActive, createChatroom, deleteChatroom, inviteFriends } = useContext(AppContext) const handleCreateChat = async (chain: any) => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') - return - } - - try { - await contractTxWithToast(api, activeAccount.address, contract, 'createChatroom', {}, []) - } catch (e) { - console.error(e) - } finally { - // fetchMessages() + if (chain.name == 'Aleph Zero') { + await createChatroom() + } else if (chain.name == 'Accurast') { + await createChatroom() } } const handleDeleteChatroom = async () => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') - return - } - - // TODO include check that caller must be owner - - try { - await contractTxWithToast(api, activeAccount.address, contract, 'deleteChatroom', {}, [ - chatroomId, - ]) - } catch (e) { - console.error(e) - } finally { - // fetchMessages() - } + await deleteChatroom() } const handleInviteFriends = async () => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') - return - } + if (!participantId) return - // TODO include check that caller must be owner - console.log('invite', chatroomId, participantId) - - try { - await contractTxWithToast(api, activeAccount.address, contract, 'invite', {}, [ - chatroomId, - participantId, - ]) - } catch (e) { - console.error(e) - } finally { - // fetchMessages() - } + await inviteFriends([participantId]) } return (
- {api ? ( // TODO get value of active chatroom + {activeAccount && isChatroomActive ? ( // { className="no-scrollbar max-h-[40vh] w-full min-w-[20rem] overflow-scroll rounded-2xl" > {/* Supported Chains */} - {supportedChains.map((chain) => ( + {env.chatroomChains.map((chain) => ( { - const { api, activeAccount, activeSigner } = useInkathon() const { contract, address: contractAddress } = useRegisteredContract(ContractIds.Chatroom) - const { typedContract } = useRegisteredTypedContract(ContractIds.Chatroom, ChatroomContract) - const [chatroomId, setChatroomId] = useState('5CqRGE6QMZUxh8anBchE69P8gt3sojPtwNQkmpKwWPz9yPRB') + const { chatroomId, sendMessage } = useContext(AppContext) const form = useForm>({ resolver: zodResolver(formSchema), @@ -36,23 +29,12 @@ export const SendMessage: FC = () => { const { register, reset, handleSubmit } = form // send a message - const sendMessage: SubmitHandler> = async ({ newMessage }) => { - if (!activeAccount || !contract || !activeSigner || !api) { - toast.error('Wallet not connected. Try again…') + const handleSendMessage: SubmitHandler> = async ({ newMessage }) => { + toast.error('Please type a message') + if (newMessage == '') { return } - - try { - await contractTxWithToast(api, activeAccount.address, contract, 'sendMessage', {}, [ - chatroomId, - newMessage, - ]) - reset() - } catch (e) { - console.error(e) - } finally { - // refresh messages - } + await sendMessage(newMessage) } // if (!api) return null @@ -66,7 +48,7 @@ export const SendMessage: FC = () => { {/* */} {/* */}
diff --git a/frontend/src/config/environment.ts b/frontend/src/config/environment.ts index 82d4eec..f3ed9c9 100644 --- a/frontend/src/config/environment.ts +++ b/frontend/src/config/environment.ts @@ -12,4 +12,14 @@ export const env = { defaultChain: process.env.NEXT_PUBLIC_DEFAULT_CHAIN!, supportedChains: getSupportedChains(), + chatroomChains: [ + { + name: 'Aleph Zero', + description: 'Lorem ipsum', + }, + { + name: 'Accurast', + description: 'Lorem ipsum', + }, + ], } diff --git a/frontend/src/context/app-context.tsx b/frontend/src/context/app-context.tsx new file mode 100644 index 0000000..0db1c49 --- /dev/null +++ b/frontend/src/context/app-context.tsx @@ -0,0 +1,222 @@ +'use client' + +import { createContext, useEffect, useState } from 'react' + +import { ContractIds } from '@/deployments/deployments' +import { + contractQuery, + decodeOutput, + useInkathon, + useRegisteredContract, +} from '@scio-labs/use-inkathon' +import toast from 'react-hot-toast' + +import { MessageProps } from '@/components/web3/message' +import { contractTxWithToast } from '@/utils/contract-tx-with-toast' + +type AppContextProps = { + isChatroomActive: boolean + getMessages: (chatroomId: string) => Promise + sendMessage: (newMessage: string) => Promise + createChatroom: () => Promise + deleteChatroom: () => Promise + inviteFriends: (participants: string[]) => Promise + refreshMessages: () => Promise + messages: MessageProps[] + chatroomId: string | null + isAppLoading: boolean + isChatroomLoading: boolean + isMessagesLoading: boolean +} + +const defaultData: AppContextProps = { + isChatroomActive: false, + getMessages: async (chatroomId: string) => {}, + sendMessage: async (newMessage: string) => {}, + createChatroom: async () => {}, + deleteChatroom: async () => {}, + inviteFriends: async (participants: string[]) => {}, + refreshMessages: async () => {}, + messages: [], + chatroomId: null, + isAppLoading: true, + isChatroomLoading: true, + isMessagesLoading: true, +} +export const AppContext = createContext(defaultData) + +export function AppProvider({ children }: { children: React.ReactNode }) { + // app state + const [isChatroomActive, setIsChatroomActive] = useState(false) + const [chatroomId, setChatroomId] = useState('5CqRGE6QMZUxh8anBchE69P8gt3sojPtwNQkmpKwWPz9yPRB') + const [messages, setMessages] = useState([]) + + // loading state + const [isAppLoading, setIsAppLoading] = useState(true) + const [isChatroomLoading, setIsChatroomLoading] = useState(true) + const [isMessagesLoading, setIsMessagesLoading] = useState(true) + + // web3 state + const { api, activeAccount, activeSigner } = useInkathon() + const { contract, address: contractAddress } = useRegisteredContract(ContractIds.Chatroom) + + // functions + + // fetch chatroom + const fetchChatroom = async () => { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again… chatroom') + return + } + + setIsChatroomLoading(true) + + try { + const result = await contractQuery(api, activeAccount.address, contract, 'getChatroom', {}, [ + chatroomId, + ]) + const { output, isError, decodedOutput } = decodeOutput(result, contract, 'getChatroom') + if (isError) throw new Error(decodedOutput) + if (!(Object.keys(output).length === 0 && output.constructor === Object)) { + // TODO change contract to output None and check for null here + setIsChatroomActive(true) + getMessages() + } + } catch (e) { + console.error(e) + toast.error('Error while loading chatroom. Try again…') + setIsChatroomLoading(false) + } finally { + setIsChatroomLoading(false) + } + } + + useEffect(() => { + fetchChatroom() + }, [api, activeAccount, contract, activeSigner]) + + // Fetch messages + const getMessages = async () => { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again… mesasges') + return + } + + setIsMessagesLoading(true) + + try { + const result = await contractQuery(api, activeAccount.address, contract, 'getMessages', {}, [ + chatroomId, + ]) + const { output, isError, decodedOutput } = decodeOutput(result, contract, 'getMessages') + if (isError) throw new Error(decodedOutput) + console.log('messages: ', output) + setMessages(output) + } catch (e) { + console.error(e) + toast.error('Error while loading chatroom. Try again…') + setIsMessagesLoading(false) + } finally { + setIsMessagesLoading(false) + } + } + + async function refreshMessages() { + await getMessages() + } + + // send a message + async function sendMessage(newMessage: string) { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again…') + return + } + + try { + await contractTxWithToast(api, activeAccount.address, contract, 'sendMessage', {}, [ + chatroomId, + newMessage, + ]) + } catch (e) { + console.error(e) + } finally { + // refresh messages + } + } + + async function createChatroom() { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again…') + return + } + + try { + await contractTxWithToast(api, activeAccount.address, contract, 'createChatroom', {}, []) + } catch (e) { + console.error(e) + } finally { + // fetchMessages() + } + } + + async function deleteChatroom() { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again…') + return + } + + // TODO include check that caller must be owner + + try { + await contractTxWithToast(api, activeAccount.address, contract, 'deleteChatroom', {}, [ + chatroomId, + ]) + } catch (e) { + console.error(e) + } finally { + // fetchMessages() + } + } + + async function inviteFriends(participants: string[]) { + if (!activeAccount || !contract || !activeSigner || !api) { + toast.error('Wallet not connected. Try again…') + return + } + + // TODO include check that caller must be owner + console.log('invite', chatroomId, participants[0]) + + try { + await contractTxWithToast(api, activeAccount.address, contract, 'invite', {}, [ + chatroomId, + participants[0], + ]) + } catch (e) { + console.error(e) + } finally { + // fetchMessages() + } + } + + return ( + + {children} + + ) +}