SOROCHAT: How to build a simple chat Dapp using create-soroban-dapp
Tags: soroban, blockchain, react, createsorobandapp
Published: 2023-12-11 15:04:03

Good morning!

In this tutorial we will create a simple chat dapp on Soroban Testnet ([LIVE VERSION HERE 🕺](https://sorochat.vercel.app/)). We will be using the create-soroban-dapp boilerplate and we will cover the smart contract implementation, the deployment using the deploy script provided and finally how to set up and connect the front-end.

What we should manage to build:

![Preview of final product](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2kb1oq1ykcunetcrm26j.png)


You can check the final source code in [my repo](https://github.com/benjaminsalon/sorochat).

## First step: Initiate the boilerplate

We will initiate a new soroban react dapp project using the @create-soroban-dapp script.

Go to the directory you want to put the project in and run 

```bash
npx create-soroban-dapp@latest
```

Then follow along the script to set the name of your project, your favorite package manager and whether you want to have your dependencies installed by the script or manually.

Arriving on the project, you can personalize the project with your own info and project name. You can follow the `Soroban-Dapp-Instructions.md` for this. To change the displayed title you can go in `src/components/home/HomePageTitle.tsx`.

After this you are all set to start working on your dapp. You can run the development server to see the initial state of the dapp.

## Contract creation

You can start by copy pasting the existing greeting contract folder to have a point to start.

Then change the repository name:

![Change Repository Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2arp09wohhuaowwdxw1s.png)

And the package name and author in the `cargo.toml` file:

![Change Author and Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqdimnianaqbkar1er8u.png)

### Implement the contract

So we have this very simple Greeting contract template to start from, it's often a good idea to start from an existing code if you are not too familiar with the language or the environment to see where to write what:

```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Symbol, symbol_short, String};

const TITLE: Symbol = symbol_short!("TITLE");


#[contract]
pub struct TitleContract;

#[contractimpl]
impl TitleContract {

    pub fn set_title(env: Env, title: String) {
                env.storage().instance().set(&TITLE, &title)
    }

    pub fn read_title(env: Env) -> String {
        env.storage().instance().get(&TITLE)
            .unwrap_or(String::from_slice(&env, "Default Title"))
    }
    
} 

mod test;
```
Now let's reflect on what we want in our contract.

### Storage Items

#### What is a chat?

First we need to define what is the base data type we will use for representing a chat. 

Nowadays, every messaging app is built around having a "conversation" with someone composed of an ordered list of "messages". These conversations being sorted by destination's user we are having conversation with.

So here we can decide that we will represent every message by a struct containing a string and a from address representing the address that sent the chat. And every conversation can be a list of messages so the rust type `Vec<Message>`:

```rust
#[contracttype]
#[derive(Clone, Debug)]
pub struct Message {
    msg: String,
    from: Address,
}

type Conversation = Vec<Message>;
```

#### How to store a chat

We want to have every conversation sorted by destination address in order to have something close to the intuitive UI we are all so used to.

We can think of storing the conversations in a Map where the key is a tuple `(address, address)` and the value is a conversation i.e. `Vec<Message>`.

Let's add this line to define the key we will use to retrieve and write the storage. A good practice is to define a DataKey enum to store all the keys used in the contract:

```rust
#[derive(Clone)]
#[contracttype]
pub struct ConversationsKey(pub Address, pub Address);

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Conversations(ConversationsKey),
}
```

Don't forget to add the necessary imports
```rust
#![no_std]
use soroban_sdk::{contracttype, Address};
```

Then every time we want to access the conversations between address1 and address2 we will write

```rust
let key = DataKey::Conversations(ConversationsKey(address1,address2));

// To read
if let Some(conversation) = env.storage().instance().get::<_, Conversation>(&key) { /* do things */ }

// To write
env.storage().instance().set(&key.clone(), &conversation);
```
#### Little trick for helping us in the front-end

A good thing to have when doing full stack development is to be able to think a bit forward in the process and anticipate what could help us build the front-end more easily. 

When we will want to retrieve the chats for a given address it will be difficult to do it with only the conversations mapping. We could use another mapping to store for each user, what are the chats they have already started with someone.

We can add an additional key to our DataKey enum:

```rust
type ConversationsInitiated = Vec<Address>;

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Conversations(ConversationsKey),
    ConversationsInitiated(Address) // We add this line
}
```

### Define the contract struct and implementation

Let's now remove this greeting contract to replace it with our chat contract, so that we now have in our `lib.rs` file:

```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, vec, Address, Env, String, Vec};

#[derive(Clone)]
#[contracttype]
pub struct ConversationsKey(pub Address, pub Address);

#[contracttype]
#[derive(Clone, Debug)]
pub struct Message {
    msg: String,
    from: Address,
}

type Conversation = Vec<Message>;

type ConversationsInitiated = Vec<Address>;

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Conversations(ConversationsKey),
    ConversationsInitiated(Address),
}

#[contract]
pub struct ChatContract;

#[contractimpl]
impl ChatContract { }

mod test;
```

### Implementing our first function: Write a message

Now, we are getting towards the functional implementation of the contract. Let's implement the write_message function.

Let's have a look at it

```rust
pub fn write_message(env: Env, from: Address, to: Address, msg: String) {
        // Ensure that the message sender is the from address
        from.require_auth();

        // First we need to retrieve the possibly already existing conversation between from and to
        let key = DataKey::Conversations(ConversationsKey(from.clone(), to.clone()));

        // We want to update the Conversation Initiated storage if it's the first time we have a conversation between from and to
        let conversation_exists =
            env.storage().instance().has(&key) && env.storage().instance().has(&key);
        if !conversation_exists {
            update_conversations_initiated(&env, from.clone(), to.clone())
        }

        // Then we can retrieve the conversation
        let mut conversation = env
            .storage()
            .instance()
            .get::<_, Conversation>(&key)
            .unwrap_or(vec![&env]);

        // Then we can add a new message to the conversation
        let new_message = Message {
            msg,
            from: from.clone(),
        };
        conversation.push_back(new_message);

        // And we don't forget to set the state storage with the new value ON BOTH SIDES if not conversation to self
        env.storage().instance().set(&key.clone(), &conversation);
        if from != to {
            let key_other_side = DataKey::Conversations(ConversationsKey(to.clone(), from.clone()));
            env.storage()
                .instance()
                .set(&key_other_side.clone(), &conversation);
        }
    }
```

First we are ensuring that the `from` address correspond to the actual sender of the transaction to avoid impersonating. This is done via the `require_auth` method:

```rust
from.require_auth();
```

Then we are checking if the conversation exists with the `has()` method which returns a boolean:

```rust
let conversation_exists =
            env.storage().instance().has(&key) && env.storage().instance().has(&key);
```

Then if the conversation does not exist already we add it to the list of the initiated adresses for both addresses `from` and `to`:

```rust
if !conversation_exists {
            update_conversations_initiated(&env, from.clone(), to.clone())
        }
```

The implementation of the helper method `update_conversations_initiated` is the following:

```rust
pub fn update_conversations_initiated(env: &Env, from: Address, to: Address) {
    let mut conversations_initiated_from = env
        .storage()
        .instance()
        .get::<_, ConversationsInitiated>(&DataKey::ConversationsInitiated(from.clone()))
        .unwrap_or(vec![&env]);
    conversations_initiated_from.push_back(to.clone());
    env.storage().instance().set(
        &DataKey::ConversationsInitiated(from.clone()),
        &conversations_initiated_from,
    );

    // If we are sending chat to ourselves, we don't want to have two different conversations
    if from != to {
        let mut conversations_initiated_to = env
            .storage()
            .instance()
            .get::<_, ConversationsInitiated>(&DataKey::ConversationsInitiated(to.clone()))
            .unwrap_or(vec![&env]);
        conversations_initiated_to.push_back(from.clone());
        env.storage().instance().set(
            &DataKey::ConversationsInitiated(to.clone()),
            &conversations_initiated_to,
        );
    }
}
```
In this helper method we are retrieving the list of the initiated conversations from both the `from` and `to` addresses to `push_back` the new conversation address to the `Vec` and then set it back in the storage. You will notice the condition `if from != to` for the second action. It is because, in the case where we are sending a message to ourselves (it's allowed here) we will have `from` equal to `from`and it will result in putting twice the `from` address to the list of initiated conversations, and we want to avoid this.

Then we are retrieving the conversation from the storage
```rust
let key = DataKey::Conversations(ConversationsKey(from, to));
let mut conversation = env
            .storage()
            .instance()
            .get::<_, Conversation>(&key)
            .unwrap_or(vec![&env]);
```

We can see that we are using the DataKey enum to retrieve the conversation that we are interested in. 
The get function is returning an Option that we can unwrap or provide a value if the Option is None. Here the option is none if there is not a conversation initiated, in this situation we provide an empty Vec using the [vec! macro](https://docs.rs/soroban-sdk/20.0.0/soroban_sdk/macro.vec.html).

Then we create the new message and we add it to the conversation:

```rust
let new_message = Message {
      msg,
      from: from.clone(),
};
conversation.push_back(new_message);
```

Finally, we put the conversation back in the storage. We once again check if we are not sending a message to ourselves so that we don't do the useless action of setting the same storage twice:

```rust
env.storage().instance().set(&key.clone(), &conversation);
if from != to {
      let key_other_side = DataKey::Conversations(ConversationsKey(to.clone(), from.clone()));
      env.storage()
      .instance()
      .set(&key_other_side.clone(), &conversation);
}
```

### Add functions to read the state of the contract

We now need to add some getter function to retrieve the data on the frontend, we add one for each storage type:

```rust
pub fn read_conversation(env: Env, from: Address, to: Address) -> Conversation {
        let key = DataKey::Conversations(ConversationsKey(from.clone(), to.clone()));
        let conversation = env
            .storage()
            .instance()
            .get::<_, Conversation>(&key)
            .unwrap_or(vec![&env]);
        conversation
}

pub fn read_conversations_initiated(env: Env, from: Address) -> ConversationsInitiated {
        let key = DataKey::ConversationsInitiated(from);
        let conversations_initiated = env
            .storage()
            .instance()
            .get::<_, ConversationsInitiated>(&key)
            .unwrap_or(vec![&env]);
        conversations_initiated
}
```

I will add here one last function which is here just for the sake of helping us see if we connect well the contract when we start implementing the front-end part:

```rust
pub fn read_title(env: Env) -> String {
   String::from_slice(&env, "Welcome to Sorochat!")
}
``` 

You can notice that it is the same function that the one which was present on the greeting contract. It's on purpose it is to test the connection without having to change the front-end.

### Final contract

We can have a look at the final version of the contract to see if we didn't miss anything:

```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, vec, Address, Env, String, Vec};

#[derive(Clone)]
#[contracttype]
pub struct ConversationsKey(pub Address, pub Address);

#[contracttype]
#[derive(Clone, Debug)]
pub struct Message {
    msg: String,
    from: Address,
}

type Conversation = Vec<Message>;

type ConversationsInitiated = Vec<Address>;

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Conversations(ConversationsKey),
    ConversationsInitiated(Address),
}

pub fn update_conversations_initiated(env: &Env, from: Address, to: Address) {
    let mut conversations_initiated_from = env
        .storage()
        .instance()
        .get::<_, ConversationsInitiated>(&DataKey::ConversationsInitiated(from.clone()))
        .unwrap_or(vec![&env]);
    conversations_initiated_from.push_back(to.clone());
    env.storage().instance().set(
        &DataKey::ConversationsInitiated(from.clone()),
        &conversations_initiated_from,
    );

    // If we are sending chat to ourselves, we don't want to have two different conversations
    if from != to {
        let mut conversations_initiated_to = env
            .storage()
            .instance()
            .get::<_, ConversationsInitiated>(&DataKey::ConversationsInitiated(to.clone()))
            .unwrap_or(vec![&env]);
        conversations_initiated_to.push_back(from.clone());
        env.storage().instance().set(
            &DataKey::ConversationsInitiated(to.clone()),
            &conversations_initiated_to,
        );
    }
}

#[contract]
pub struct ChatContract;

#[contractimpl]
impl ChatContract {
    pub fn write_message(env: Env, from: Address, to: Address, msg: String) {
        // Ensure that the message sender is the from address
        from.require_auth();

        // First we need to retrieve the possibly already existing conversation between from and to
        let key = DataKey::Conversations(ConversationsKey(from.clone(), to.clone()));

        // We want to update the Conversation Initiated storage if it's the first time we have a conversation between from and to
        let conversation_exists =
            env.storage().instance().has(&key) && env.storage().instance().has(&key);
        if !conversation_exists {
            update_conversations_initiated(&env, from.clone(), to.clone())
        }

        // Then we can retrieve the conversation
        let mut conversation = env
            .storage()
            .instance()
            .get::<_, Conversation>(&key)
            .unwrap_or(vec![&env]);

        // Then we can add a new message to the conversation
        let new_message = Message {
            msg,
            from: from.clone(),
        };
        conversation.push_back(new_message);

        // And we don't forget to set the state storage with the new value ON BOTH SIDES if not conversation to self
        env.storage().instance().set(&key.clone(), &conversation);
        if from != to {
            let key_other_side = DataKey::Conversations(ConversationsKey(to.clone(), from.clone()));
            env.storage()
                .instance()
                .set(&key_other_side.clone(), &conversation);
        }
    }

    pub fn read_conversation(env: Env, from: Address, to: Address) -> Conversation {
        let key = DataKey::Conversations(ConversationsKey(from.clone(), to.clone()));
        let conversation = env
            .storage()
            .instance()
            .get::<_, Conversation>(&key)
            .unwrap_or(vec![&env]);
        conversation
    }

    pub fn read_conversations_initiated(env: Env, from: Address) -> ConversationsInitiated {
        let key = DataKey::ConversationsInitiated(from);
        let conversations_initiated = env
            .storage()
            .instance()
            .get::<_, ConversationsInitiated>(&key)
            .unwrap_or(vec![&env]);
        conversations_initiated
    }

    pub fn read_title(env: Env) -> String {
        String::from_slice(&env, "Welcome to Sorochat!")
    }
}

mod test;
```

### Test the contract

It's good practices to write unit tests along the development of your code. I will not dive in this tutorial on how to do testing with Soroban but here are the (non exhaustive) tests I wrote for this contract:

```rust
#![cfg(test)]

use crate::{ChatContract, ChatContractClient, Message};
use soroban_sdk::{testutils::Address as _, vec, Address, Env, String};

#[test]
fn test() {
    let env = Env::default();
    env.mock_all_auths();

    let from = Address::random(&env);
    let to = Address::random(&env);
    let to_2 = Address::random(&env);

    let contract_id = env.register_contract(None, ChatContract);
    let client = ChatContractClient::new(&env, &contract_id);
    let conversation_before = client.read_conversation(&from, &to);
    assert_eq!(conversation_before.len(), 0);

    let conversations_initiated = client.read_conversations_initiated(&from);
    assert_eq!(conversations_initiated, vec![&env]);

    client.write_message(&from, &to, &String::from_slice(&env, "Bonjour l'ami!"));
    let conversation_after = client.read_conversation(&from, &to);
    // log!(&env, "{:?}", conversation_after);
    assert_eq!(conversation_after.len(), 1);
    assert_eq!(
        conversation_after,
        vec![
            &env,
            Message {
                msg: String::from_slice(&env, "Bonjour l'ami!"),
                from: from.clone()
            }
        ]
    );

    let conversations_initiated = client.read_conversations_initiated(&from);
    assert_eq!(conversations_initiated, vec![&env, to.clone()]);
    let conversations_initiated = client.read_conversations_initiated(&to);
    assert_eq!(conversations_initiated, vec![&env, from.clone()]);

    client.write_message(&from, &to_2, &String::from_slice(&env, "Bonjour l'ami!"));
    let conversation_after = client.read_conversation(&from, &to_2);
    assert_eq!(conversation_after.len(), 1);
    assert_eq!(
        conversation_after,
        vec![
            &env,
            Message {
                msg: String::from_slice(&env, "Bonjour l'ami!"),
                from: from.clone()
            }
        ]
    );

    let conversations_initiated = client.read_conversations_initiated(&from);
    assert_eq!(
        conversations_initiated,
        vec![&env, to.clone(), to_2.clone()]
    );
    let conversations_initiated = client.read_conversations_initiated(&to_2);
    assert_eq!(conversations_initiated, vec![&env, from]);
}

```

### Deploy the contract

We will now deploy our contract on Soroban Testnet. `create-soroban-dapp` equips us with a command line script to deploy the contract easily.

In the subdirectory contracts/ run 

```bash
./deploy_on_testnet chat
```

You should see a bunch of information displayed as the script is first:
- Build your contract using the Makefile associated to your contract
- Set a deployer identity using soroban-cli
- Fund the deployer address
- Set the soroban-cli for testnet deployment
- Deploy contract on testnet
- Register contract's address in `contracts/contracts_ids.json` for the front-end to fetch it

Now you can check the `contracts_ids.json` file and you should see the newly deployed contract address written inside.

Congrats you made it with the contract. Now the front-end work.

## Front-end

So all the front-end things are happening in the `src` folder. 

This front end is configured to interact with the greeting contract which only exposes two functions `read_title` and `set_title`, the goal is to inspire from how the connection is done to adapt it to our contract which is exposing three different functions, one write function `write_message` and to read functions `read_conversation` and `read_conversations_initiated`.

We can start by removing the `ChainInfo` component from `index.tsx` to make a bit of room for our project:

```typescript
// src/pages/index.tsx
<div tw="mt-10 flex w-full flex-wrap items-start justify-center gap-4">
     {/* Chain Metadata Information */}
     {/* <ChainInfo /> */} // Commented this line

     {/* Greeter Read/Write Contract Interactions */}
     <GreeterContractInteractions />
</div>
```

Then we will test the connection to the contract deployed on testnet. For this we can just modify the line 55 of `src/components/web3/GreeterContractInteractions.tsx` in `fetchGreeting` from 

```typescript
// src/components/web3/GreeterContractInteractions.tsx 44
      const contract = useRegisteredContract("greeting")
```

to 

```typescript
src/components/web3/GreeterContractInteractions.tsx 44
      const contract = useRegisteredContract("chat")
```

Normally on recompiling, the front-end should display "Welcome to Sorochat!" in the first card. It means that the front-end and the contract are communicating!

### Fetch the intiated conversation list.

Now we will adapt the fetchGreeting to make it able to fetch the list of conversations initiated for the connected user.

Let's check the code:

```typescript
// src/components/web3/GreeterContractInteractions.tsx
// We add this state variable:
const [fetchedConversationsInitiatedList, setConversationsInitiatedList] = useState<Array<string>>([])

// and fetchGreeting on line 44 becomes:
const fetchConversationsInitiated = useCallback(async () => {
    if (!sorobanContext.server) return

    const currentChain = sorobanContext.activeChain?.name?.toLocaleLowerCase()
    // We are now checking for the user to be connected before displaying as we need to know the user's address to fetch their chats
    if (!address) {
      return
    }
    else if (!currentChain) {
      console.log("No active chain")
      toast.error('Wallet not connected. Try again…')
      return
    }
    else {
      const contractAddress = (contracts_ids as Record<string,Record<string,string>>)[currentChain]?.chat;
      setContractAddressStored(contractAddress)
      
      try {
        const result = await contractInvoke({
          contractAddress,
          method: 'read_conversations_initiated',
          args: [new SorobanClient.Address(address).toScVal()],
          sorobanContext
        })
        if (!result) throw new Error("Error while fetching. Try Again")

        // Value needs to be cast into a string as we fetch a ScVal which is not readable as is.
        const conversationsInitiated: Array<string> = SorobanClient.scValToNative(result as SorobanClient.xdr.ScVal) as Array<string>
        setConversationsInitiatedList(conversationsInitiated)
      } catch (e) {
        console.error(e)
        toast.error('Error while fetching list of conversations. Try again…')
        setConversationsInitiatedList([])
      } finally {
        
      }
    }
  },[sorobanContext, address])

  useEffect(() => {void fetchConversationsInitiated()}, [updateFrontend,fetchConversationsInitiated])
```

We can notice here than the behavior is very similar. The useEffect ensures that the function is called at every update on the connected address (`address`). And the function stores the list of the addresses the connected user has already opened a conversation with in the `fetchedConversationsInitiatedList` state variable.

Some important thing to also notice is how we convert normal types to ScVal using the `soroban-client` methods: 

```typescript
args: [new SorobanClient.Address(address).toScVal()],
```

And how we translate back from a ScVal to a native typescript type:

```typescript
const conversationsInitiated: Array<string> = SorobanClient.scValToNative(result as SorobanClient.xdr.ScVal) as Array<string>
```

### Send message functionality

Let's implement the message sending now.

For that we will first adapt the form to take two arguments the message itself and the destination address (the from address being the connected user's address)

Here it comes, this
```typescript
// src/components/web3/GreeterContractInteractions.tsx
<form onSubmit={handleSubmit(updateGreeting)}>
            <Stack direction="row" spacing={2} align="end">
              <FormControl>
                <FormLabel>Update Greeting</FormLabel>
                <Input disabled={updateIsLoading} {...register('newMessage')} />
              </FormControl>
```
becomes this
```typescript
// src/components/web3/GreeterContractInteractions.tsx
<form onSubmit={handleSubmit(sendMessage)}>
            <Stack direction="row" spacing={2} align="end">
              <FormControl>
                <FormLabel>Send new Message</FormLabel>
                <Input disabled={updateIsLoading} {...register('destinationAddress')} />
                <Input disabled={updateIsLoading} placeholder='Message' {...register('newMessage')} />
              </FormControl>
```

If you notice we added a new input to the form and we renamed the updateGreeting function to sendMessage.

So now we need to adapt the `useForm` types. `Use React Form` is a great implementation of react hooks for form handling. For more about the `useForm` pattern check this out: [Use React Form](https://www.react-hook-form.com/).

Check the modifications:

```typescript
// This line:
const { register, handleSubmit } = useForm<UpdateGreetingValues>()
// Becomes this
const { register, handleSubmit } = useForm<NewMessageData>()

// While the UpdateGreetingValues type
type UpdateGreetingValues = { newMessage: string }
// Becomes the NewMessageData type:
type NewMessageData = { newMessage: string, destinationAddress: string }
```

So now that the form is adapted we need to adapt the `updateGreeting` function to call the `write_message` method of the contract. So the `updateGreeting` function becomes:

```typescript
// updateGreeting function becomes this
const sendMessage = async ({ newMessage, destinationAddress }: NewMessageData ) => {
    if (!address) {
      console.log("Address is not defined")
      toast.error('Wallet is not connected. Try again...')
      return
    }
    else if (!server) {
      console.log("Server is not setup")
      toast.error('Server is not defined. Unabled to connect to the blockchain')
      return
    }
    else {
      const currentChain = activeChain?.name?.toLocaleLowerCase()
      if (!currentChain) {
        console.log("No active chain")
        toast.error('Wallet not connected. Try again…')
        return
      }
      else {
        const contractAddress = (contracts_ids as Record<string,Record<string,string>>)[currentChain]?.chat;

        setUpdateIsLoading(true)

        try {
          const result = await contractInvoke({
            contractAddress,
            method: 'write_message',
            args: [new SorobanClient.Address(address).toScVal(),new SorobanClient.Address(destinationAddress).toScVal(), stringToScVal(newMessage)],
            sorobanContext,
            signAndSend: true
          })
          
          if (result) {
            toast.success("New chat successfully published!")
          }
          else {
            toast.error("Chat publishing unsuccessful...")
            
          }
        } catch (e) {
          console.error(e)
          toast.error('Error while sending tx. Try again…')
        } finally {
          setUpdateIsLoading(false)
          toggleUpdate(!updateFrontend)
        } 

        await sorobanContext.connect();
      }
    }
  }
```

#### Display the conversations list

Now that we can write a new message and fetch the updated list of conversation the connected user has initiated we can display the conversation list.

Let's create a new component. Create a new component in a new folder chat in `src/components/chat/ConversationsList.tsx`.

The component will be the following:

```typescript
import { Card } from "@chakra-ui/react";

interface ConversationsListProps {
  conversationsList: Array<string>,
  setDisplayedConversationAddress: (newDestinationAddress: string) => void
}

export default function ConversationsList({conversationsList, setDisplayedConversationAddress}:ConversationsListProps) {

  return (
    <>
    {conversationsList.map((address: string, index: number) => (
      <>
      <Card variant="outline" key={index} p={2} bgColor="whiteAlpha.0" 
          onClick={() => setDisplayedConversationAddress(address)} 
          cursor="pointer"
          _hover={{
            background: "whiteAlpha.100",
          }}
      >
        {address}
      </Card>
      </>
    ))}
    </>
  );
}
```

We can see here the introduction of a new state variable that we will put in the GreeterContractInteractions.tsx file and pass its setter as a prop to the Conversation element:

```typescript
const [conversationDisplayedAddress, setConversationDisplayedAddress] = useState<string>("")
```

This conversationDisplayedAddress will be used for chosing which conversation to display at the screen.

Now let's add this component to the `GreeterContractInteractions.tsx` file. Don't forget to import and replace the first Card

```typescript
// src/components/web3/GreeterContractInteraction.tsx l146
{/* Fetched Greeting */}
        <Card variant="outline" p={4} bgColor="whiteAlpha.100">
          <FormControl>
            <FormLabel>Fetched Greeting</FormLabel>
            <Input
              placeholder={fetchedGreeting}
              disabled={true}
            />
          </FormControl>
        </Card>
```

by the newly created Element, passing the setter and the conversations list as props:

```ts
// src/components/web3/GreeterContractInteraction.tsx l146
<Card variant="outline" p={4} bgColor="whiteAlpha.100">
          <FormControl>
            <FormLabel>Your Chats</FormLabel>
            <ConversationsList conversationsList={fetchedConversationsInitiatedList} setDisplayedConversationAddress={setConversationDisplayedAddress}></ConversationsList>
          </FormControl>
        </Card>
```

Try to send a few messages, you should see something like this on the screen appearing:

![List Displayed with some button and hovering](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6qqsxfjicll0ghywzjt.png)

### Display a conversation

Let's now display a conversation. We will add these two components

The Message one: 

```typescript
// src/components/chat/Message.tsx
import { Box, Text } from "@chakra-ui/react";

export type MessageType = {
    msg: string,
    from: string
}

interface MessageProps {
    message: MessageType,
    userConnected: string
}

export function Message({message,userConnected}:MessageProps) {
    const bg_color = userConnected == message.from ? "whiteAlpha.200" : "whiteAlpha.500";
    return (
        <>
            <Box ml='3' bg={bg_color} borderRadius="12" padding="4" margin="4" position="relative" >
                <Text fontWeight='bold' fontSize={12}>
                {message.from}
                </Text>
                <Text fontSize='sm'>{message.msg}</Text>
            </Box>
        <p></p>
        </>
    );
}
```

And the Conversation one which will rely on the Message Component to display messages:

```ts
import {type MessageType as ContractMessageType, Message} from './Message'

interface ConversationProps {
    conversation: Array<ContractMessageType>,
    destinationAddress: string,
    userConnected: string
  }
  
  export default function Conversation({conversation, destinationAddress,userConnected}:ConversationProps) {
    return (
        destinationAddress ? 
      <div>Conversation with {destinationAddress}
      {conversation.map((message: ContractMessageType, index: number) => (
        <Message key={index} message={message} userConnected={userConnected}/>
      ))}
      </div>
      :
      <div>Choose a conversation or send a message to new destination</div>
    );
  }
  
```

Now we need to implement the function to fetch the conversation from the user's address and the selected address. Add this to the GreeterContractInteraction.tsx file:

```ts
// src/components/web3/GreeterContractInteraction.tsx
const fetchConversation = useCallback(async () => {
    if (!sorobanContext.server) return
    if (!address) {
      console.log("Address is not defined")
      toast.error('Wallet is not connected. Try again...')
      return
    }
    else if (!server) {
      console.log("Server is not setup")
      toast.error('Server is not defined. Unabled to connect to the blockchain')
      return
    }
    else {
      const currentChain = activeChain?.name?.toLocaleLowerCase()
      if (!currentChain) {
        console.log("No active chain")
        toast.error('Wallet not connected. Try again…')
        return
      }
      else if (conversationDisplayedAddress) {
        const contractAddress = (contracts_ids as Record<string,Record<string,string>>)[currentChain]?.chat;
        setConversationIsLoading(true)
        try {
          const result = await contractInvoke({
            contractAddress,
            method: 'read_conversation',
            args: [new SorobanClient.Address(address).toScVal(),new SorobanClient.Address(conversationDisplayedAddress).toScVal()],
            sorobanContext
          })
          if (!result) throw new Error("Error while fetching. Try Again")

          // Value needs to be cast into a string as we fetch a ScVal which is not readable as is.
          // You can check out the scValConversion.tsx file to see how it's done
          console.log("CONVERSATION FETCHED =",SorobanClient.scValToNative(result as SorobanClient.xdr.ScVal))
          const conversation = SorobanClient.scValToNative(result as SorobanClient.xdr.ScVal) as Array<MessageType>
          // const result_string = scvalToString(result as SorobanClient.xdr.ScVal)
          setConversationDisplayed(conversation)
        } catch (e) {
          console.error(e)
          toast.error('Error while fetching conversation. Try again…')
          setConversationDisplayed([])
        } finally {
          setConversationIsLoading(false)
        } 
      }
    }
  }, [conversationDisplayedAddress])

  useEffect(() => {void fetchConversation()}, [conversationDisplayedAddress,updateFrontend,fetchConversation,sorobanContext])

  useEffect(() => {
    setConversationDisplayedAddress("")
  }, [address])
```

We can see that we are introducing two new state variables

```ts
const [conversationDisplayed, setConversationDisplayed] = useState<Array<MessageType>>([])
const [conversationIsLoading, setConversationIsLoading] = useState<boolean>(false)
```

`conversationDisplayed` is the fetched conversation that we will pass in the props of the Conversation element. 

`conversationIsLoading` fills the same role as `updateIsLoading` and is here to display a loading screen while the function fetches the conversation.

Finally we can add the Conversation element in the returned jsx:

```ts
// src/components/web3/GreeterContractInteractions.tsx
       <Card variant="outline" p={4} bgColor="whiteAlpha.100">
            {!conversationIsLoading ?
            <Conversation userConnected={address} destinationAddress={conversationDisplayedAddress} conversation={conversationDisplayed}></Conversation>
            :
            "Loading ..."
            }
        </Card>
```

And we should get something like that:

![Conversation Displayed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auji2zo8extdlz06yks2.png)

### Display only if address is connected

Finally as we need to user to connect, let's only display the connect button if the user is not already connected, to your `GreeterContractInteractions.tsx` file, add this

```ts
return (
  <> {address ?
      </div>
        <-- everything -->
      </div>
      :
      <div></div>}
  </>
)
```
You should have this on refresh:

![User not connected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2d2u6yflp9uwu164svjy.png)

And we should be finished!

## Conclusion

Yeey! You manage to make it to the end. You have built a simple React Dapp on soroban. 

This chat now only awaits for you to make it better with more functionalities like moving the button to send a chat in the conversation zone to me closer to the classical UI we are used to, deleting chats, replying to an existing chat with a new chat.

I already have added some little adjustments out of scope for this tutorial, so be sure to check [the repo](https://github.com/benjaminsalon/sorochat) after being finished!

### Useful Resources I personally used building this dapp:
https://github.com/stellar/soroban-examples/blob/v20.0.0-rc2/token/src/
https://soroban.stellar.org/docs/fundamentals-and-concepts/built-in-types
https://docs.rs/soroban-sdk/20.0.0/soroban_sdk/