Discord Clone Using Next.js and Tailwind - Part 3: Channel List
Published: 2024-02-06 19:45:02

Welcome to our series about building a [Discord](https://discord.com/) clone using [Next.js](https://nextjs.org/) and [TailwindCSS](https://tailwindcss.com/). In the previous posts, we covered [setting up the project](https://getstream.io/blog/discord-clone-project-setup/) and [adding the server list](https://getstream.io/blog/discord-clone-server-list/). This one will tackle the channel list that will look like this:

<video controls="true" src="https://stream-blog.s3.amazonaws.com/blog%2Fwp-content%2Fuploads%2F5008a48dac9bd084b99adc04d8fa80f3%2FDiscord-Clone-Part-3-Promo-1080p.mp4">
	Demo of the end result of the project
</video>

We already mentioned different customization options for the [Stream Chat SDK](https://getstream.io/chat/sdk/). In the previous piece about the [server list](https://getstream.io/blog/discord-clone-server-list/), we needed to customize ourselves.

While building the channel list, we can use some of the built-in components to make it easier. Specifically, we’ll customize the `ChannelList` component using the `List` parameter.
This will automatically inject the component we hand to the `List` parameter and handle the properties we need to render the channels list.

We’ll still need to filter the channels by categories, but we will get the rest for free. We can then customize how we render the channels in our custom component.

## Building the Channel List With Categories and Channels

First, we create a new folder called `ChannelList`, where we store all files for this article. Then, we create a new file in that directory called `CustomChannelList.tsx`. We add an empty component here for now to have a look at what the parameters for it look like:

```tsx
const CustomChannelList: React.FC<ChannelListMessengerProps> = (
  props: PropsWithChildren<ChannelListMessengerProps>
) => {
	return <div>List</div>
};
```

We can see that we get handed a `props` parameter for the `ChannelListMessengerProps`. This is a powerful way to access all necessary data without using a custom context. The `props` type has several properties, including a `loading` state, an `error` variable, and more (the full list can be found [here](https://github.com/GetStream/stream-chat-react/blob/16dcc5ddf00aed472cd2de1343e8f353de48373d/src/components/ChannelList/ChannelListMessenger.tsx#L10C28-L10C28)).

For our use case, we will use the `loadedChannels` property (which requires us to set a parameter later on, so keep that in mind). We can take this, combined with the `server` information we can retrieve from the `DiscordContext`, and split the channels into their respective categories.

Let’s first create a helper function to do that:

```tsx
function splitChannelsIntoCategories(
  channels: Channel<DefaultStreamChatGenerics>[],
  server: DiscordServer | undefined
): Map<string, Array<Channel<DefaultStreamChatGenerics>>> {
  const channelsByCategories = new Map<
    string,
    Array<Channel<DefaultStreamChatGenerics>>
  >();
  if (server) {
    const categories = new Set(
      channels
        .filter((channel) => {
          return channel.data?.data?.server === server.name;
        })
        .map((channel) => {
          return channel.data?.data?.category;
        })
    );

    for (const category of Array.from(categories)) {
      channelsByCategories.set(
        category,
        channels.filter((channel) => {
          return (
            channel.data?.data?.server === server.name &&
            channel.data?.data?.category === category
          );
        })
      );
    }
  } else {
    channelsByCategories.set('Direct Messages', channels);
  }
  return channelsByCategories;
}
```

We create a `Map` with the category name as a key. If a server is set, we filter the channels by the category name. If not, we only want to show the direct messages and these channels.

For rendering the UI, there’s a semantic element in HTML for showing different categories with a disclosure indicator: the `details` element ([MDN docs here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)) combined with the `summary` element ([MDN docs here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary)). However, the `summary` element limits customization options so we will opt for a custom-built solution.

We’ll iterate over all keys (being the category names) of the `channelsByCategories` element we created and want to show each channel. To keep the code clean, we’ll create a custom component for this in a second. But first, we think about the properties we need to hand to that component:

1. `category`: the name of the category to render out and show as a title
2. `channels`: each channel will be shown in its line
3. `serverName`: while we don’t render this out, we need to know which server we’re on because when a user wants to add a new channel (which happens inside our component), we need that information

The code to render the list looks like this (we create the `CategoryItem` in the next step) and can be placed inside the `CustomChannelList`:

```tsx
const { server } = useDiscordContext();
const channelsByCategories = splitChannelsIntoCategories(
  props.loadedChannels || [],
  server
);

return (
  <div className='w-64 bg-medium-bg-gray h-full flex flex-col items-start'>
      <div className='w-full'>
        {Array.from(channelsByCategories.keys()).map((category, index) => (
          <CategoryItem
            key={`${category}-${index}`}
            category={category}
            serverName={server?.name || 'Direct Messages'}
            channels={channelsByCategories.get(category) || []}
          />
        ))}
      </div>
    </div>
);
```

We use a custom color for the container (`bg-medium-gray`), so we must extend our `theme` in the `tailwind.config.ts` file to contain that color. While we’re at it, we’ll also add the other colors we need for this article.

This is how the file should look like in the end:

```jsx
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        discord: '#7289da',
        'dark-discord': '#4752c4',
        'dark-bg-gray': '#dfe1e4',
        'medium-gray': '#f0f1f3',
        'light-gray': '#e8eaec',
        'hover-gray': '#cdcfd3',
      },
    },
  },
  plugins: [],
};
export default config;
```

Now, we can finally implement the `CategoryItem`. We create a new folder inside the `ChannelList` directory called `CategoryItem` and add a file called `CategoryItem.tsx`. The first thing we define is a type for the properties that will be handed to the component:

```jsx
type CategoryItemProps = {
  category: string;
  channels: Channel<DefaultStreamChatGenerics>[];
  serverName: string;
};
```

We want to track whether a category is toggled open or closed. Using a state property for each `CategoryItem` instance works perfectly. So, we define an `isOpen` property using a `useState` hook.

Let’s first define the barebone of the component like this:

```tsx
export default function CategoryItem({
  category,
  serverName,
  channels,
}: CategoryItemProps): JSX.Element {
  const [isOpen, setIsOpen] = useState(true);
  return (
    <>
      <div className='flex items-center text-gray-500 mb-2 p-2'>
				{/* Toggle will go here */}
      </div>
    </>
  );
}
```

Now, two things are remaining:

1. Add a UI element that toggles the visibility of the list of channels and has a plus icon to add a new channel to this category on the given server.
2. Render the list of channels that are handed to the component.

We start with the toggle button. We want icons for this that we get from [heroicons](https://heroicons.com/). Let’s create a new file in the `ChannelList` folder called `Icons.tsx` and paste the code for the icons here to have a solid separation:

```tsx
type IconProps = {
  size?: number;
  color?: string;
};

export function PlusIcon({ size = 5, color }: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
      strokeWidth={2}
      stroke='currentColor'
      className={`h-${size} w-${size} ${color ? color : ''}`}
    >
      <path
        strokeLinecap='round'
        strokeLinejoin='round'
        d='M12 4.5v15m7.5-7.5h-15'
      />
    </svg>
  );
}

export function ChevronRight({ size = 5, color }: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
      strokeWidth={2}
      stroke='currentColor'
      className={`h-${size} w-${size} ${color ? color : ''}`}
    >
      <path
        strokeLinecap='round'
        strokeLinejoin='round'
        d='m8.25 4.5 7.5 7.5-7.5 7.5'
      />
    </svg>
  );
}
```

Now we can add the code for the toggle mechanism inside of `CategoryItem` (where we previously had the placeholder comment):

```tsx
<button
  className='flex w-full items-center justify-start'
  onClick={() => setIsOpen((currentValue) => !currentValue)}
>
  <div
    className={`${
      isOpen ? 'rotate-90' : ''
    } transition-all ease-in-out duration-200`}
  >
    <ChevronRight size={4} />
  </div>
  <span className='inline-block uppercase text-sm font-bold  px-2'>
    {category}
  </span>
</button>
```

On the button click, we toggle the `isOpen` state property and rotate the icon to indicate that it’s open. We animate this to give it a nice touch.

To finish this row, we add the code to open the form to create a new channel on the server below the button:

```tsx
<Link
  className='inline-block create-button'
  href={`/?createChannel=true&serverName=${serverName}&category=${category}`}
>
  <PlusIcon size={4} />
</Link>
```

We will implement the form in the next chapter and focus on finishing the `CategoryItem` first. For that, we need to render the channels if the `isOpen` property is set to true.

After the `div` that contains the `button` and the `link` (but inside the `<>` fragment), add the following code:

```tsx
{isOpen && (
    <div>
      {channels.map((channel) => {
        return (
            <CustomChannelPreview
	      key={channel.id}
	      channel={channel}
	      className='w-full'
	    />
	 );
       })}
    </div>
)}
```

Note: We have not yet created the `CustomChannelPreview` element. We will do that in the next chapter, but I want to discuss this more, so stay tuned.

With that, the UI for the `CustomChannelList` is done. We still need to inject it into the built-in Stream component. We can do this in one line of code. 

We head over to `page.tsx`, and inside our `Home` component, we currently use the `ChannelList` that we get for free from the Stream SDK. We can customize it using the `List` parameter, so we do just that. In addition, we also need to set the `sendChannelsToList` property since otherwise, the `loadedChannels` wouldn’t be handed to the `CustomChannelList` component. 

Here is the code:

```tsx
<ChannelList List={CustomChannelList} sendChannelsToList={true} />
```

With that, we can now switch between the servers and toggle the channels in the different categories on each server. 

However, the preview for the channel in the list is not yet customized. Let’s explore how we can do this in the next chapter.

## Building the Channel List Preview

Using the `List` property, we just added our `CustomChannelList` property to the `ChannelList` property. We could use another property on the `ChannelList` called `Preview` to show a custom preview with the channel's name and other options.

This is a perfectly viable option for many applications. It works by setting the `Preview` property like this: `Preview={CustomChannelPreview}`. Inside our `CustomChannelList`, we access a `children` array in the `props`, which we can render inside the DOM like this: `{children}`.

While this would generally work, we are not doing this here. The reason is that we want to render the channels list for each category separately. So, instead, we will create a custom component and hand the `channel` to it inside our `CustomChannelList`.

Let’s create a new file inside the `Channel List` folder and call it `CustomChannelPreview.tsx`.

The UI part is simple. We show a `#` and then the name of the channel. We add a little hover effect. We'll wrap it in a button since we want to react to clicks. 

What is supposed to happen when the user taps a channel? We want to render the selected channel on the right side of our 3-pane-layout. Luckily, there’s another hook we can use for this from the Stream Chat SDK called `useChatContext` ([documentation](https://getstream.io/chat/docs/sdk/react/components/contexts/chat_context/)). From that, we can get a function called `setActiveChannel` that does that.

This is where the power of the Stream components comes into play. On our `page.tsx` we render the currently active channel using the `Channel` component. Using the `setActiveChannel` function from the `ChatContext` automatically updates this with the one the user just clicked.

We use a custom channel preview component that we need to create. Inside the `ChannelList` folder, add a new file called `CustomChannelPreview.tsx` and insert the following code:

```tsx
const CustomChannelPreview = (props: ChannelPreviewUIComponentProps) => {
  const { channel } = props;
  const { setActiveChannel } = useChatContext();
  return (
    <div
      className={`flex items-center mx-2 ${
        props.channel.countUnread() > 0 ? 'channel-container' : ''
      }`}
    >
      <button
        className='w-full flex items-center px-2 hover:bg-gray-200 rounded-md'
        onClick={() => setActiveChannel(channel)}
      >
        <span className='italic text-xl mr-2 text-gray-500'>#</span>
        <span className='text-sm'>
          {channel.data?.name || 'Channel Preview'}
        </span>
      </button>
    </div>
  );
};

export default CustomChannelPreview;
```

What are we doing here?

- We have a container `div` that shows a small dot at the left in case there are unread messages (using the `props.channel.countUnread()` function from the SDK).
- The `button` allows the user to click on the channel, and the `setActiveChannel` function from the chat context allows us to set this one as active in the message list (the right pane in our three-pane-layout).
- We then show the channel name prefixed with a `#` sign.

For the unread indicator to work, we add the `channel-container` CSS class to the `globals.css` file:

```css
.channel-container {
  @apply relative;
}

.channel-container::before {
  @apply block absolute h-2 w-3 -left-4 bg-gray-700 rounded-xl;
  content: '';
}
```

With that, the UI for showing the channel lists is done, and we can already try it and see it working with the selection of channels and rendering the different channels when we click on them.

<video controls="true" src="https://stream-blog.s3.amazonaws.com/blog%2Fwp-content%2Fuploads%2F1fbff1797dce83c40d11732a8d2f13f8%2FDiscord-Clone-Part-3-Channel-List.mp4">
	Demo of channel list.
</video>

## Create a Form to Add Channels to a Server

There are many different ways we can use forms in React in general and Next.js specifically. We went over this process already in [part two](https://getstream.io/blog/discord-clone-server-list/#adding-a-form-to-create-a-new-server) of this series, so we will only briefly touch upon the general principle and focus on the implementation itself.

The `Link` element we added in the `CategoryItem` will put the following properties into the URL:

- `createChannel`: is set to `true` and will trigger the form to be rendered using a `dialog` element
- `serverName`: is set to the server that is currently active, because we want to create the channel on the current server
- `category`: while the user can change this, we want to pre-fill the category name in the form to the one where the create channel button was clicked

We can create a new folder inside the `ChannelList` directory and call it `CreateChannelForm`. We add a file called `CreateChannelForm.tsx` and can start with adding the code to extract the search parameters we just mentioned from the URL:

```tsx
export default function CreateChannelForm(): JSX.Element {
	// Extract the search params 
  const params = useSearchParams();
  const showCreateChannelForm = params.get('createChannel');
  const category = params.get('category');
  const dialogRef = useRef<HTMLDialogElement>(null);
  const router = useRouter();

  useEffect(() => {
    if (showCreateChannelForm && dialogRef.current) {
      dialogRef.current.showModal();
    } else {
      dialogRef.current?.close();
    }
  }, [showCreateChannelForm]);

  return (
    <dialog
      className='absolute py-16 px-20 z-10 space-y-8 rounded-xl serverDialog'
      ref={dialogRef}
    >
      <Link href='/' className='absolute right-8 top-8'>
        <CloseCircle />
      </Link>
      <h2 className='text-3xl font-bold text-gray-600'>Create new server</h2>
    </dialog>
  );
```

We already added the functionality to show the `dialog` using a reference to it with the `useRef` hook.

Next, we add the `form` code itself. We will do that in a two-step process. We can easily ask for the channel name and allow the user to change the category name. However, selecting the users requires more work, so we’ll do that in the second step.

The `form` code itself is not complicated, but for it to work, we must first define the data format. Add this definition to the top of the file:

```tsx
type FormState = {
  channelName: string;
  category: string;
  users: UserObject[];
};
```

Inside the `CreateChannelForm` component, we can create an `initialState` property and initialize a `formData` state variable. We’ll also already define an empty array of users (we’ll load those in the next step) that can be added to the channel:

```tsx
const initialState: FormState = {
  channelName: '',
  category: category ?? '',
  users: [],
};
const [formData, setFormData] = useState<FormState>(initialState);
const [users, setUsers] = useState<UserObject[]>([]);
```

We have the data available, so let’s define the UI. Add the code for the form below the `h2` we already created:

```tsx
<form method='dialog' className='flex flex-col'>
  <label className='labelTitle' htmlFor='channelName'>
    Channel Name
  </label>
  <input
    type='text'
    id='channelName'
    name='channelName'
    value={formData.channelName}
    onChange={(e) =>
      setFormData({ ...formData, channelName: e.target.value })
    }
  />
  <label
    className='labelTitle flex items-center justify-between'
    htmlFor='category'
  >
    Category
  </label>
  <input
    type='text'
    id='category'
    name='category'
    value={formData.category}
    onChange={(e) =>
      setFormData({ ...formData, category: e.target.value })
    }
  />
  <button
    type='submit'
    disabled={buttonDisabled()}
    className={`bg-discord rounded p-3 text-white font-bold uppercase ${
      buttonDisabled() ? 'opacity-50 cursor-not-allowed' : ''
    }`}
    onClick={createClicked}
  >
    Create
  </button>
</form>
```

If you look closely, we use two functions we haven’t defined yet. The first one is the `buttonDisabled()` call. We only want to allow the create button to be clickable when we have the full data to create a new channel. That is a name, a category, and at least two users. 

Add this function to the bottom of the component:

```tsx
function buttonDisabled(): boolean {
    return (
      !formData.channelName || !formData.category || formData.users.length <= 1
    );
  }
```

The second one we need to add is the `createClicked` function. Luckily, we prepared the `createChannel` function in the `DiscordContext` (see [part 1](https://getstream.io/blog/discord-clone-project-setup/)), so it’s a simple API call. After that, we set the form data back to its initial state and replace the current route with the empty route (`/`), which automatically closes the dialog.

Add this function to the file:

```tsx
function createClicked() {
  createChannel(
    client,
    formData.channelName,
    category || 'Category',
    formData.users.map((user) => user.id)
  );
  setFormData(initialState);
  router.replace('/');
}
```

We won’t be able to click that since we don’t have users loaded yet. 

Let’s take a step back and think about that for a second. We want users to see a list of other users to add to the channel. As this requires access to other users’ profiles, we don’t want to handle that client-side.

Luckily, both Next.js and Stream offer the perfect mechanisms for that. Using [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) in Next.js, we can ensure the code is executed on the server with the right access. This is then automatically scoped to the server; nothing can escape client-side. The [Stream Node SDK](https://getstream.io/chat/docs/node/?language=javascript) allows us to [query users](https://getstream.io/chat/docs/node/query_users/?language=javascript) and return these in the API call.

Read the documentation on [Router Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) for more details. We’ll only show the implementation here. Inside the `app` folder, we add a folder `api` and another folder called `users`. Here, we will add a file called `route.ts`.

This works because we can define a `GET` request that will be executed server-side. We can access this route in our frontend component with a `fetch` request to `/api/users`. Here’s the code for our request:

```tsx
export async function GET() {
  const serverClient = StreamChat.getInstance(
    '7cu55d72xtjs',
    process.env.STREAM_CHAT_SECRET
  );
  const response = await serverClient.queryUsers({});
  const data: UserObject[] = response.users
    .filter((user) => user.role !== 'admin')
    .map((user) => {
      return {
        id: user.id,
        name: user.name ?? user.id,
        image: user.image as string,
        online: user.online,
        lastOnline: user.last_active,
      };
    });

  return Response.json({ data });
}
```

This requires us to add the secret from the Stream dashboard into an environment file (`.env.local`). We can grab the secret inside of the dashboard and copy it to the file:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5m8jfej5lup55q0q496v.png)

Now, we can return to the front end and make the call. Inside our `CreateChannelForm` component, we first define a function to load the users from the API we just defined (and wrap it in a `useCallback` to avoid unnecessary re-renders). Then, we call this when the component is mounted (using a `useEffect` hook).

Add this code inside the component:

```tsx
const loadUsers = useCallback(async () => {
  const response = await fetch('/api/users');
  const data = (await response.json())?.data as UserObject[];
  if (data) setUsers(data);
}, []);

useEffect(() => {
  loadUsers();
}, [loadUsers]);
```

The last step is to show the users in our form. We want to add a checkbox for each and the information about them. Let’s create a separate component for this. Add a new file inside the `CreateChannelForm` folder, call it `UserRow.tsx` and add the following code to it:

```tsx
export default function UserRow({
  user,
  userChanged,
}: {
  user: UserObject;
  userChanged: (user: UserObject, checked: boolean) => void;
}): JSX.Element {
  return (
    <div className='flex items-center justify-start w-full space-x-4 my-2'>
      <input
        id={user.id}
        type='checkbox'
        name={user.id}
        className='w-4 h-4 mb-0'
        onChange={(event) => {
          userChanged(user, event.target.checked);
        }}
      ></input>
      <label className='w-full flex items-center space-x-6' htmlFor='users'>
        {user.image && (
          <Image
            src={user.image}
            width={40}
            height={40}
            alt={user.name}
            className='w-8 h-8 rounded-full'
          />
        )}
        {!user.image && <PersonIcon />}
        <p>
          <span className='block text-gray-600'>{user.name}</span>
          {user.lastOnline && (
            <span className='text-sm text-gray-400'>
              Last online: {user.lastOnline.split('T')[0]}
            </span>
          )}
        </p>
      </label>
    </div>
  );
}
```

It is mainly code to show a checkbox and users’ names, images, and the last time they were online. Note that we inject a `userChanged` function that takes the user itself and whether they are checked or not. We’ll use that in the main component to add the user to the `formData` variable.

Open the `CreateChannelForm` again and first define the function `userChanged` at the bottom of the file:

```tsx
function userChanged(user: UserObject, checked: boolean) {
  if (checked) {
    setFormData({
      ...formData,
      users: [...formData.users, user],
    });
  } else {
    setFormData({
      ...formData,
      users: formData.users.filter((thisUser) => thisUser.id !== user.id),
    });
  }
}
```

If `checked` is `true` we add the user to the user list in the `formData` property; if not, we filter it out and remove it from the list.

The last step is to render the users inside of our form. Add this code before the submit button of the form:

```tsx
<h2 className='mb-2 labelTitle'>Add Users</h2>
{users.map((user) => (
  <UserRow user={user} userChanged={userChanged} key={user.id} />
))}
```

We’re not showing the `CreateChannelForm` at the moment. Change that by opening up the `CustomChannelList` and adding it below the `div` container that holds the `CategoryItem`s. The return function of the component should look like this now:

```tsx
return (
  <div className='w-64 bg-medium-gray h-full flex flex-col items-start'>
    <div className='w-full'>
      {Array.from(channelsByCategories.keys()).map((category, index) => (
        <CategoryItem
          key={`${category}-${index}`}
          category={category}
          serverName={server?.name || 'Direct Messages'}
          channels={channelsByCategories.get(category) || []}
        />
      ))}
    </div>
    <CreateChannelForm />
  </div>
);
```

Awesome, we can now add channels into different categories and get a list of users to add to them. This required using many different functionalities around the SDKs. Great work!

Next, we can focus on adding the remaining UI, specifically the top and bottom bars.

## Adding the Top Bar

The top bar will have two purposes. First, it shows the server's name, and second, it expands into a menu on click and shows different server options. Let’s take a look at the result first:

<video controls="true" src="https://stream-blog.s3.amazonaws.com/blog%2Fwp-content%2Fuploads%2Fa6c40e0d7f97fde4c9390c8fdf4e2ed3%2FDiscord-Clone-Part-3-Top-Bar.mp4">
	Demo of the top bar.
</video>

Let’s jump into the implementation. First, we have a few icons that we need to prepare. The arrow on the right of the menu transforms into a close button, and each menu option has an icon. 

We use the great services of [heroicons](https://heroicons.com/) again and first add these icons to the `Icons.tsx` file so that it’s easier for us to integrate them. Add this to the existing code in the file:

```tsx
export function ChevronDown({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
      strokeWidth={1.5}
      stroke='currentColor'
      className={className}
    >
      <path
        strokeLinecap='round'
        strokeLinejoin='round'
        d='M19.5 8.25l-7.5 7.5-7.5-7.5'
      />
    </svg>
  );
}

export function Boost({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M10.5 3.798v5.02a3 3 0 0 1-.879 2.121l-2.377 2.377a9.845 9.845 0 0 1 5.091 1.013 8.315 8.315 0 0 0 5.713.636l.285-.071-3.954-3.955a3 3 0 0 1-.879-2.121v-5.02a23.614 23.614 0 0 0-3 0Zm4.5.138a.75.75 0 0 0 .093-1.495A24.837 24.837 0 0 0 12 2.25a25.048 25.048 0 0 0-3.093.191A.75.75 0 0 0 9 3.936v4.882a1.5 1.5 0 0 1-.44 1.06l-6.293 6.294c-1.62 1.621-.903 4.475 1.471 4.88 2.686.46 5.447.698 8.262.698 2.816 0 5.576-.239 8.262-.697 2.373-.406 3.092-3.26 1.47-4.881L15.44 9.879A1.5 1.5 0 0 1 15 8.818V3.936Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function PersonAdd({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path d='M5.25 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM2.25 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM18.75 7.5a.75.75 0 0 0-1.5 0v2.25H15a.75.75 0 0 0 0 1.5h2.25v2.25a.75.75 0 0 0 1.5 0v-2.25H21a.75.75 0 0 0 0-1.5h-2.25V7.5Z' />
    </svg>
  );
}

export function Gear({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 0 0-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 0 0-2.282.819l-.922 1.597a1.875 1.875 0 0 0 .432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 0 0 0 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 0 0-.432 2.385l.922 1.597a1.875 1.875 0 0 0 2.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 0 0 2.28-.819l.923-1.597a1.875 1.875 0 0 0-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 0 0 0-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 0 0-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 0 0-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 0 0-1.85-1.567h-1.843ZM12 15.75a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function PlusCircle({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25ZM12.75 9a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25V15a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25V9Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function FolderPlus({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M19.5 21a3 3 0 0 0 3-3V9a3 3 0 0 0-3-3h-5.379a.75.75 0 0 1-.53-.22L11.47 3.66A2.25 2.25 0 0 0 9.879 3H4.5a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h15Zm-6.75-10.5a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25v2.25a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25V10.5Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function FaceSmile({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-2.625 6c-.54 0-.828.419-.936.634a1.96 1.96 0 0 0-.189.866c0 .298.059.605.189.866.108.215.395.634.936.634.54 0 .828-.419.936-.634.13-.26.189-.568.189-.866 0-.298-.059-.605-.189-.866-.108-.215-.395-.634-.936-.634Zm4.314.634c.108-.215.395-.634.936-.634.54 0 .828.419.936.634.13.26.189.568.189.866 0 .298-.059.605-.189.866-.108.215-.395.634-.936.634-.54 0-.828-.419-.936-.634a1.96 1.96 0 0 1-.189-.866c0-.298.059-.605.189-.866Zm2.023 6.828a.75.75 0 1 0-1.06-1.06 3.75 3.75 0 0 1-5.304 0 .75.75 0 0 0-1.06 1.06 5.25 5.25 0 0 0 7.424 0Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function Bell({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M5.25 9a6.75 6.75 0 0 1 13.5 0v.75c0 2.123.8 4.057 2.118 5.52a.75.75 0 0 1-.297 1.206c-1.544.57-3.16.99-4.831 1.243a3.75 3.75 0 1 1-7.48 0 24.585 24.585 0 0 1-4.831-1.244.75.75 0 0 1-.298-1.205A8.217 8.217 0 0 0 5.25 9.75V9Zm4.502 8.9a2.25 2.25 0 1 0 4.496 0 25.057 25.057 0 0 1-4.496 0Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function Shield({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M12.516 2.17a.75.75 0 0 0-1.032 0 11.209 11.209 0 0 1-7.877 3.08.75.75 0 0 0-.722.515A12.74 12.74 0 0 0 2.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 0 0 .374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 0 0-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08Zm3.094 8.016a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z'
        clipRule='evenodd'
      />
    </svg>
  );
}

export function Pen({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path d='M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z' />
    </svg>
  );
}

export function SpeakerMuted({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path d='M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06ZM17.78 9.22a.75.75 0 1 0-1.06 1.06L18.44 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06l1.72-1.72 1.72 1.72a.75.75 0 1 0 1.06-1.06L20.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-1.72 1.72-1.72-1.72Z' />
    </svg>
  );
}

export function LeaveServer({
  className = 'h-5 w-5 text-gray-500',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path
        fillRule='evenodd'
        d='M7.5 3.75A1.5 1.5 0 0 0 6 5.25v13.5a1.5 1.5 0 0 0 1.5 1.5h6a1.5 1.5 0 0 0 1.5-1.5V15a.75.75 0 0 1 1.5 0v3.75a3 3 0 0 1-3 3h-6a3 3 0 0 1-3-3V5.25a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3V9A.75.75 0 0 1 15 9V5.25a1.5 1.5 0 0 0-1.5-1.5h-6Zm10.72 4.72a.75.75 0 0 1 1.06 0l3 3a.75.75 0 0 1 0 1.06l-3 3a.75.75 0 1 1-1.06-1.06l1.72-1.72H9a.75.75 0 0 1 0-1.5h10.94l-1.72-1.72a.75.75 0 0 1 0-1.06Z'
        clipRule='evenodd'
      />
    </svg>
  );
}
```

Now, let’s create a new folder inside the `ChannelList` directory and call it `TopBar`. Inside it, we add a file called `ChannelListTopBar.tsx`.

All we need as input for the component is the `serverName`, and we can then show this using an `h2`. We’ll embed this in a `button` that will toggle a state variable (`menuOpen`) and consist of the title and an icon. The icon will either be a chevron pointing downwards (if `menuOpen` is `false`) or a close icon (if `menuOpen` is `true`).

Here’s the code for now:

```tsx
export default function ChannelListTopBar({
  serverName,
}: {
  serverName: string;
}): JSX.Element {
  const [menuOpen, setMenuOpen] = useState(false);

  return (
    <div className='w-full relative'>
      <button
        className={`flex w-full items-center justify-between p-4 border-b-2 ${
          menuOpen ? 'bg-gray-300' : ''
        } border-gray-300 hover:bg-gray-300`}
        onClick={() => setMenuOpen((currentValue) => !currentValue)}
      >
        <h2 className='text-lg font-bold text-gray-700'>{serverName}</h2>
        {menuOpen && <CloseIcon />}
        {!menuOpen && <ChevronDown />}
      </button>
    </div>
  );
}
```

Note that we wrap the button in a `div` container because we must also add the menu. This container has the `relative` property applied because we want to render the menu relative to this container.

For the menu, we’ll take three steps:

1. create the data for the menu items
2. create a UI component to visualize them
3. hook it up in the `ChannelListTopBar`

We start by creating a new file inside the `TopBar` folder and call it `menuItems.tsx`. We define a type `ListRowElement` and then an array of items, with a `name`,  an `icon`, and whether it should have a `bottomBorder` (to distinguish different menu groups).

Also, there is one element (`Invite People`) that is pink and one that is red (`Leave Server`), so we add this to the logic as well.

Here’s the code:

```tsx
export type ListRowElement = {
  name: string;
  icon: JSX.Element;
  bottomBorder?: boolean;
  purple?: boolean;
  red?: boolean;
};

export const menuItems: ListRowElement[] = [
  { name: 'Server Boost', icon: <Boost />, bottomBorder: true },
  {
    name: 'Invite People',
    icon: <PersonAdd />,
    bottomBorder: false,
    purple: true,
  },
  { name: 'Server Settings', icon: <Gear />, bottomBorder: false },
  { name: 'Create Channel', icon: <PlusCircle />, bottomBorder: false },
  { name: 'Create Category', icon: <FolderPlus />, bottomBorder: false },
  { name: 'App Directory', icon: <FaceSmile />, bottomBorder: true },
  { name: 'Notification Settings', icon: <Bell />, bottomBorder: false },
  { name: 'Privacy Settings', icon: <Shield />, bottomBorder: true },
  { name: 'Edit Server Profile', icon: <Pen />, bottomBorder: false },
  { name: 'Hide Muted Channels', icon: <SpeakerMuted />, bottomBorder: true },
  {
    name: 'Leave Server',
    icon: <LeaveServer />,
    bottomBorder: false,
    red: true,
  },
];
```

Next, we want to create a separate component for the UI of each menu item, so let’s create a file called `ChannelListMenuRow.tsx` inside the `TopBar` folder. The UI is straightforward and consists of the name, the icon, a hover effect, and a bottom border (if the element has one).

The code looks like this:

```tsx
export default function ChannelListMenuRow({
  name,
  icon,
  bottomBorder = true,
  purple = false,
  red = false,
}: ListRowElement): JSX.Element {
  return (
    <>
      <p
        className={`flex items-center justify-between p-2 cursor-pointer text-gray-500 ${
          purple ? 'text-dark-discord' : ''
        } ${red ? 'text-red-500' : ''} rounded-md hover:bg-dark-discord ${
          red ? 'hover:bg-red-500' : ''
        } hover:text-white transition-colors ease-in-out duration-200`}
      >
        <span className='text-sm font-medium '>{name}</span>
        {icon}
      </p>
      {bottomBorder && <div className='my-1 mx-2 h-px bg-gray-300' />}
    </>
  );
} 
```

The last step is to include this into the `ChannelListTopBar` if the `menuOpen` property is `true`. Add this after the `button` but inside the `div` container:

```tsx
{menuOpen && (
  <div className='absolute w-full p-2'>
    <div className='w-full bg-white p-2 shadow-lg rounded-md'>
      {menuItems.map((option) => (
        <button
          key={option.name}
          className='w-full'
          onClick={() => setMenuOpen(false)}
        >
          <ChannelListMenuRow {...option} />
        </button>
      ))}
    </div>
  </div>
)}
```

When we save and run the project, we see…nothing. We need to add the `ChannelListTopBar` to the `CustomChannelList`, right before the `div` we added as a container for the `CategoryItem` components:

```tsx
...
<div className='w-64 bg-medium-gray h-full flex flex-col items-start'>
      <ChannelListTopBar serverName={server?.name || 'Direct Messages'} />

      <div className='w-full'>
...
```

The result looks great. We don’t have the functionality hooked up, but that would break the scope of this article because we still need to implement the footer with the user’s profile image and name. 

Let’s do this next.

## The User Bar as the Bottom Bar

The last piece of UI is the bottom bar. It will show the currently logged-in user and microphone, speaker, and settings icons. 

Note: we will implement the video calling and sharing functionality in a later post in this series, presumably part 5 (watch out on our [socials](https://twitter.com/getstream_io) when it drops) but we will prepare the UI nonetheless.

Here is what the result will look like:

<video controls="true" src="https://stream-blog.s3.amazonaws.com/blog%2Fwp-content%2Fuploads%2Fc661b0fcae3d9974c91acc66b8e2393c%2FDiscord-Clone-Part-3-User-Bar.mp4">
	Demo of the user bar.
</video>

First, we will grab the icons (microphone and speaker) from [heroicons](https://heroicons.com/) and add them to the bottom of `Icons.tsx` :

```tsx
export function Mic({ className = 'w-full h-full' }: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path d='M8.25 4.5a3.75 3.75 0 117.5 0v8.25a3.75 3.75 0 11-7.5 0V4.5z' />
      <path d='M6 10.5a.75.75 0 01.75.75v1.5a5.25 5.25 0 1010.5 0v-1.5a.75.75 0 011.5 0v1.5a6.751 6.751 0 01-6 6.709v2.291h3a.75.75 0 010 1.5h-7.5a.75.75 0 010-1.5h3v-2.291a6.751 6.751 0 01-6-6.709v-1.5A.75.75 0 016 10.5z' />
    </svg>
  );
}

export function Speaker({
  className = 'w-full h-full',
}: IconProps): JSX.Element {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      fill='currentColor'
      className={className}
    >
      <path d='M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 001.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06zM18.584 5.106a.75.75 0 011.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 11-1.06-1.06 8.25 8.25 0 000-11.668.75.75 0 010-1.06z' />
      <path d='M15.932 7.757a.75.75 0 011.061 0 6 6 0 010 8.486.75.75 0 01-1.06-1.061 4.5 4.5 0 000-6.364.75.75 0 010-1.06z' />
    </svg>
  );
}
```

Next, we create a new folder called `BottomBar` and add a file `ChannelListBottomBar.tsx`.

To retrieve the user information, we can grab the `client` from the `useChatContext` hook. It has a `user` property that we use to get the `image`, `name`, and `online` information.

Also, we add two properties to track the active state of the microphone and audio (`micActive` and `audioActive`). 

Then, we render the user image (if they have one), name, and online status. We also add buttons for the microphone, speaker, and settings. We configure a slight hover effect, and if the icons are inactive, we turn them red.

Here is the full code for the `ChannelListBottomBar` component:

```tsx
export default function ChannelListBottomBar(): JSX.Element {
  const { client } = useChatContext();
  const [micActive, setMicActive] = useState(false);
  const [audioActive, setAudioActive] = useState(false);

  return (
    <div className='mt-auto p-2 bg-light-gray w-full flex items-center justify-between'>
      <button className='flex items-center space-x-2 p-1 pr-2 rounded-md hover:bg-hover-gray '>
        {client.user?.image && (
          <div
            className={`relative ${client.user?.online ? 'online-icon' : ''}`}
          >
            <Image
              src={client.user?.image ?? 'https://thispersondoesnotexist.com/'}
              alt='User image'
              width={36}
              height={36}
              className='rounded-full'
            />
          </div>
        )}
        <p className='flex flex-col items-start space-y-1'>
          <span className='block text-gray-700 text-sm font-medium -mb-1.5 tracking-tight'>
            {client.user?.name}
          </span>
          <span className='text-xs text-gray-500 inline-block'>
            {client.user?.online ? 'Online' : 'Offline'}
          </span>
        </p>
      </button>
      <button
        className={`w-7 h-7 p-1 flex items-center justify-center relative rounded-lg hover:bg-gray-300 transition-all duration-100 ease-in-out ${
          !micActive ? 'inactive-icon text-red-400' : 'text-gray-700'
        }`}
        onClick={() => setMicActive((currentValue) => !currentValue)}
      >
        <Mic />
      </button>
      <button
        className={`w-7 h-7 p-1 flex items-center justify-center relative rounded-lg hover:bg-gray-300 transition-all duration-100 ease-in-out ${
          !audioActive ? 'inactive-icon text-red-400' : 'text-gray-700'
        }`}
        onClick={() => setAudioActive((currentValue) => !currentValue)}
      >
        <Speaker />
      </button>
      <button className='w-7 h-7 p-1 flex items-center justify-center relative rounded-md hover:bg-gray-300 transition-all duration-100 ease-in-out text-gray-700'>
        <Gear className='w-full h-full' />
      </button>
    </div>
  );
}
```

We perform two small additions. The first is an online indicator if the user is online, which is overlayed at the bottom right of the user image. We can use a pseudo-element (`::after`).

Second, we want to cross the icons with a red line if they are inactive. For that, we added the `inactive-icon` CSS class to them, which we now also add to the `globals.css`:

```css
.online-icon::after {
  @apply block absolute h-4 w-4 bg-green-600 bottom-0 right-0 rounded-full border-2 border-gray-200;
  content: '';
}

.inactive-icon::after {
  @apply block absolute h-full w-0.5 bg-red-400 rotate-45 rounded-xl m-2;
  content: '';
}
```

Lastly, we must add the `ChannelListBottomBar` to the `CustomChannelList`, just below the `CreateChannelForm`. The final code for the `CustomChannelList` component looks like this:

```tsx
const CustomChannelList: React.FC<ChannelListMessengerProps> = (
  props: PropsWithChildren<ChannelListMessengerProps>
) => {
  const { server } = useDiscordContext();
  const channelsByCategories = splitChannelsIntoCategories(
    props.loadedChannels || [],
    server
  );

  return (
    <div className='w-64 bg-medium-gray h-full flex flex-col items-start'>
      <ChannelListTopBar serverName={server?.name || 'Direct Messages'} />

      <div className='w-full'>
        {Array.from(channelsByCategories.keys()).map((category, index) => (
          <CategoryItem
            key={`${category}-${index}`}
            category={category}
            serverName={server?.name || 'Direct Messages'}
            channels={channelsByCategories.get(category) || []}
          />
        ))}
      </div>
      <CreateChannelForm />
      <UserBar />
    </div>
  );
};
```

With this last addition, we are done with implementing the bottom bar and - in fact - the entire channel list.

## Summary

We put a lot together in this blog post. The [first part](https://getstream.io/blog/discord-clone-project-setup/) of this series covered the setup process, and in [part two](https://getstream.io/blog/discord-clone-server-list/), we started doing a lot of custom UI for the server list.

In this one, we learned how to leverage the Stream Chat SDK to make it easy for us to focus on building out our custom UI components while using built-in components. 

We did this using the `ChannelList` and injecting our `CustomChannelList` component inside it. This brings a lot of advantages, and we get a lot of functionality for free.

We added a top bar to the channel list that allows us to expand and show some server options. We then created a beautiful toggle to show channels split into categories. Finally, we showed the user information and prepared the UI for adding voice and video calls later.

Although this article was long, we hope you enjoyed it and learned something from it. [Let us know](https://twitter.com/getstream_io) if you have questions or ideas. You can find the project on [GitHub](https://github.com/GetStream/discord-clone-nextjs).

The next part will cover customizing the message list to conform to the discord style. Stay tuned.