Skip to content

Virtuoso Message List Examples - Grouped Messages

This example showcases a chat that groups consecutive messages from the same user. The ItemContent component receives the nextData and prevData props, which allow you to determine the position of the message in the conversation. Based on the position, the message is styled differently to indicate the start, middle, or end of a message group.

You can use similar approach to render the users’ avatars only once per group, or to display the message timestamp at the top of the group.

import { useState } from 'react'
import {
  VirtuosoMessageList,
  VirtuosoMessageListLicense,
  VirtuosoMessageListProps,
  DataWithScrollModifier,
  VirtuosoMessageListMethods,
} from '@virtuoso.dev/message-list'
import { randTextRange } from '@ngneat/falso'

interface Message {
  key: string
  text: string
  user: 'me' | 'other'
}

let idCounter = 0

function randomMessage(user: Message['user']): Message {
  return { user, key: `${idCounter++}`, text: randTextRange({ min: user === 'me' ? 20 : 100, max: 200 }) }
}

export default function App() {
  const [data, setData] = useState<DataWithScrollModifier<Message>>(() => {
    return {
      data: Array.from({ length: 20 }, (_, index) => {
        const author = ['me', 'other'][index % 4 ? 0 : 1]
        // biome-ignore lint/suspicious/noExplicitAny: this is an example
        return randomMessage(author as any)
      }),
      scrollModifier: {
        type: 'item-location',
        location: {
          index: 'LAST',
          align: 'end',
        },
      },
    }
  })
  return (
    <div className="tall-example" style={{ height: '100%', fontSize: '70%' }}>
      <VirtuosoMessageListLicense licenseKey="">
        <VirtuosoMessageList<Message, null>
          data={data}
          style={{ height: '500px', fontSize: '80%' }}
          computeItemKey={({ data }) => data.key}
          ItemContent={({ data, nextData, prevData }) => {
            let groupType = 'none'
            if (nextData && nextData.user === data.user) {
              if (prevData && prevData.user === data.user) {
                groupType = 'middle'
              } else {
                groupType = 'top'
              }
            } else if (prevData && prevData.user === data.user) {
              groupType = 'bottom'
            }

            const borderRadiusStyle = {
              none: '1rem',
              top: '1rem 1rem 0.3rem 0.3rem',
              middle: '0.3rem',
              bottom: '0.3rem 0.3rem 1rem 1rem',
            }[groupType]

            const paddingBottomStyle = {
              none: '2rem',
              top: '0.2rem',
              middle: '0.2rem',
              bottom: '1rem',
            }[groupType]

            return (
              <div style={{ paddingBottom: paddingBottomStyle, display: 'flex' }}>
                <div
                  style={{
                    maxWidth: '50%',
                    marginLeft: data.user === 'me' ? 'auto' : undefined,
                    backgroundColor: data.user === 'me' ? 'var(--background)' : 'var(--alt-background)',
                    color: data.user === 'me' ? 'var(--foreground)' : 'var(--foreground)',
                    border: '1px solid var(--border)',
                    borderRadius: borderRadiusStyle,
                    padding: '1rem',
                  }}
                >
                  {data.text}
                </div>
              </div>
            )
          }}
        />
      </VirtuosoMessageListLicense>
      <button
        onClick={() => {
          setData((current) => {
            const myMessage = randomMessage('me')
            return {
              data: [...(current?.data ?? []), myMessage],
              scrollModifier: {
                type: 'auto-scroll-to-bottom',
                autoScroll: 'smooth',
              },
            }
          })
        }}
      >
        Send message
      </button>

      <button
        onClick={() => {
          setData((current) => {
            const myMessage = randomMessage('other')
            return {
              data: [...(current?.data ?? []), myMessage],
              scrollModifier: {
                type: 'auto-scroll-to-bottom',
                autoScroll: 'smooth',
              },
            }
          })
        }}
      >
        Receive message
      </button>
    </div>
  )
}