Skip to main content

Reactions

A common problem in messaging interfaces is how to handle reactions to messages. Displaying reactions can change the size of the message element, which can displace the rest of the messages in the list, canceling the automatic scroll behavior when new messages come in. To address this, the data.map method has an additional autoscrollToBottomBehavior field in the argument, which lets you specify the necessary behavior if the data mapping causes the list to be no longer at the bottom.

info

The approach above is not exclusive to reactions - the same principle should be applied to any message state change that will change the message height (for example, expanding details, etc).

Live Editor
interface Message {
  key: string
  text: string
  user: 'me' | 'other'
  liked: boolean
}

let idCounter = 0

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

const ItemContent: VirtuosoMessageListProps<Message, null>['ItemContent'] = ({ data }) => {
  const methods = useVirtuosoMethods<Message, {}>()
  return (
    <div
      style={{
        paddingBottom: '2rem',
        display: 'flex',
        flexDirection: data.user === 'me' ? 'row-reverse' : 'row',
      }}
    >
      <div
        style={{
          maxWidth: '80%',
          display: 'flex',
          flexDirection: data.user === 'me' ? 'row-reverse' : 'row',
          alignItems: 'center',
        }}
      >
        <div
          style={{
            backgroundColor: data.user === 'me' ? '#0253B3' : '#F0F0F3',
            borderRadius: '1rem',
            padding: '1rem',
          }}
        >
          {data.text}
          {data.liked ? <div>❤️</div> : ''}
        </div>
        <button
          style={{ appearance: 'none', border: 'none', background: 'transparent', cursor: 'pointer' }}
          onClick={() => {
            methods.data.map((item) => {
                return item.key === data.key ? { ...item, liked: !item.liked } : item
              }, 
            'smooth')
          }}
        >
          {data.liked ? '❤️' : '🤍'}
        </button>
      </div>
    </div>
  )
}

function App() {
  const messages = React.useMemo(() => Array.from({ length: 100 }, () => randomMessage(rand(['me', 'other']))), [])
  const virtuoso = React.useRef<VirtuosoMessageListMethods<Message>>(null)

  return (
    <div class="tall-example" style={{fontSize: '70%'}}>
      <VirtuosoMessageListLicense licenseKey="">
        <VirtuosoMessageList<Message, null>
          ref={virtuoso}
          initialData={messages}
          style={{ height: 800 }}
          computeItemKey={({ data }) => data.key}
          initialLocation={{ index: 'LAST', align: 'end' }}
          ItemContent={ItemContent}
        />
      </VirtualMessageListLicense>
    </div>
  )
}

render(<App />)
Result
Loading...