Skip to main content

Overview

The Chat Applications examples demonstrate how to build messaging interfaces with Yoopta Editor. Two implementations are provided: Slack-style for team collaboration and WhatsApp-style for personal messaging.

Slack Chat

Team messaging with channels and threads

Social Media Chat

WhatsApp/Instagram-style personal messaging

Slack-Style Chat

Features

  • Text formatting (bold, italic, strikethrough)
  • Code snippets with syntax highlighting
  • @Mentions and emoji support
  • Lists and quotes
  • Channel list sidebar
  • Message threads
  • User presence indicators
  • Timestamp formatting
  • Reactions/emoji responses
  • Message editing
  • File attachments
  • Read receipts

Implementation

Slack Chat Layout

import { useState } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';

function SlackChat() {
  const [selectedChannel, setSelectedChannel] = useState('general');
  const [messages, setMessages] = useState<Message[]>([]);

  return (
    <div className="flex h-screen">
      {/* Channel Sidebar */}
      <aside className="w-64 border-r bg-neutral-900 text-white">
        <div className="p-4 font-semibold border-b border-neutral-800">
          Workspace Name
        </div>
        <ChannelList
          channels={channels}
          selected={selectedChannel}
          onSelect={setSelectedChannel}
        />
      </aside>

      {/* Main Chat Area */}
      <main className="flex-1 flex flex-col">
        {/* Channel Header */}
        <div className="border-b p-4">
          <h2 className="font-semibold"># {selectedChannel}</h2>
        </div>

        {/* Messages */}
        <ScrollArea className="flex-1 p-4">
          <MessageList messages={messages} />
        </ScrollArea>

        {/* Message Input */}
        <div className="border-t p-4">
          <MessageComposer onSend={handleSendMessage} />
        </div>
      </main>
    </div>
  );
}

Message Bubble Component

import { useMemo } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { SLACK_PLUGINS, SLACK_MARKS } from './config';

function MessageBubble({ message }: { message: Message }) {
  // Create read-only editor for each message
  const previewEditor = useMemo(() => {
    return createYooptaEditor({
      plugins: SLACK_PLUGINS,
      marks: SLACK_MARKS,
      readOnly: true,
      value: message.content,
    });
  }, [message.content]);

  return (
    <div className="flex gap-3 px-5 py-2.5 hover:bg-neutral-100">
      {/* Avatar */}
      <div className="w-9 h-9 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center text-white font-semibold">
        {message.author.avatar}
      </div>

      {/* Content */}
      <div className="flex-1">
        <div className="flex items-baseline gap-2">
          <span className="font-semibold text-sm">
            {message.author.name}
          </span>
          <span className="text-xs text-neutral-500">
            {formatTime(message.timestamp)}
          </span>
        </div>

        {/* Message rendered with Yoopta */}
        <div className="text-sm">
          <YooptaEditor editor={previewEditor} style={{ width: '100%' }} />
        </div>

        {/* Reactions */}
        {message.reactions && (
          <div className="flex gap-1 mt-1">
            {message.reactions.map((reaction) => (
              <button
                key={reaction.emoji}
                className="px-2 py-0.5 rounded border hover:bg-blue-50 text-xs"
              >
                {reaction.emoji} {reaction.count}
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

Message Composer

import { useMemo, useRef } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { withMentions } from '@yoopta/mention';
import { withEmoji } from '@yoopta/emoji';
import { Button } from '@/components/ui/button';
import { Send, Bold, Italic, Code, AtSign, Smile } from 'lucide-react';

function MessageComposer({ onSend }: { onSend: (content: YooptaContentValue) => void }) {
  const editor = useMemo(() => {
    return withEmoji(
      withMentions(
        createYooptaEditor({
          plugins: SLACK_PLUGINS,
          marks: SLACK_MARKS,
        })
      )
    );
  }, []);

  const handleSend = () => {
    const content = editor.getEditorValue();
    onSend(content);
    
    // Clear editor
    editor.setEditorValue({});
    editor.focus();
  };

  return (
    <div className="border rounded-lg">
      {/* Formatting Toolbar */}
      <div className="flex gap-1 p-2 border-b">
        <Button
          size="sm"
          variant="ghost"
          onClick={() => Marks.toggle(editor, { type: 'bold' })}
        >
          <Bold className="h-4 w-4" />
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => Marks.toggle(editor, { type: 'italic' })}
        >
          <Italic className="h-4 w-4" />
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => editor.toggleBlock('Code')}
        >
          <Code className="h-4 w-4" />
        </Button>
      </div>

      {/* Editor */}
      <div className="p-3">
        <YooptaEditor
          editor={editor}
          placeholder="Message #general"
          style={{ minHeight: 60 }}
        />
      </div>

      {/* Send Button */}
      <div className="flex justify-end p-2 border-t">
        <Button onClick={handleSend}>
          <Send className="h-4 w-4 mr-2" />
          Send
        </Button>
      </div>
    </div>
  );
}

Slack Plugin Configuration

import Paragraph from '@yoopta/paragraph';
import Lists from '@yoopta/lists';
import Code from '@yoopta/code';
import Blockquote from '@yoopta/blockquote';
import Link from '@yoopta/link';
import Mention from '@yoopta/mention';
import { Bold, Italic, Strike, CodeMark } from '@yoopta/marks';

export const SLACK_PLUGINS = [
  Paragraph,
  Lists.BulletedList,
  Lists.NumberedList,
  Code.extend({
    options: {
      languages: ['javascript', 'typescript', 'python', 'bash'],
    },
  }),
  Blockquote,
  Link,
  Mention,
];

export const SLACK_MARKS = [Bold, Italic, Strike, CodeMark];

WhatsApp-Style Chat

Features

  • Distinct styling for sent/received messages
  • Bubble tails for conversation flow
  • Message status indicators (sent, delivered, read)
  • Timestamp display
  • Image and video sharing
  • File attachments
  • Voice messages
  • Link previews
  • Emoji reactions
  • Reply to messages
  • Forward messages
  • Delete for everyone

Implementation

Chat Interface

import { useState } from 'react';
import { Phone, Video, MoreVertical } from 'lucide-react';

function SocialMediaChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [currentUser] = useState(CURRENT_USER);

  return (
    <div className="h-screen flex flex-col">
      {/* Chat Header */}
      <div className="border-b p-4 flex items-center justify-between bg-white">
        <div className="flex items-center gap-3">
          <div className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-semibold">
            JD
          </div>
          <div>
            <div className="font-semibold">John Doe</div>
            <div className="text-xs text-neutral-500">Online</div>
          </div>
        </div>
        
        <div className="flex gap-2">
          <Button variant="ghost" size="sm">
            <Phone className="h-5 w-5" />
          </Button>
          <Button variant="ghost" size="sm">
            <Video className="h-5 w-5" />
          </Button>
          <Button variant="ghost" size="sm">
            <MoreVertical className="h-5 w-5" />
          </Button>
        </div>
      </div>

      {/* Messages */}
      <ScrollArea className="flex-1 p-4 bg-[#efeae2]">
        <div className="space-y-2">
          {messages.map((message, index) => {
            const isOwn = message.senderId === currentUser.id;
            const showAvatar = !isOwn && (
              index === messages.length - 1 ||
              messages[index + 1]?.senderId !== message.senderId
            );
            
            return (
              <ChatBubble
                key={message.id}
                message={message}
                isOwn={isOwn}
                showAvatar={showAvatar}
              />
            );
          })}
        </div>
      </ScrollArea>

      {/* Input */}
      <div className="border-t p-4 bg-white">
        <ChatInput onSend={handleSendMessage} />
      </div>
    </div>
  );
}

Chat Bubble Component

function ChatBubble({
  message,
  isOwn,
  showAvatar,
}: {
  message: ChatMessage;
  isOwn: boolean;
  showAvatar: boolean;
}) {
  const previewEditor = useMemo(() => {
    return createYooptaEditor({
      plugins: CHAT_PLUGINS,
      marks: CHAT_MARKS,
      readOnly: true,
      value: message.content,
    });
  }, [message.content]);

  return (
    <div className={cn('flex gap-2 max-w-[80%]', isOwn ? 'ml-auto flex-row-reverse' : 'mr-auto')}>
      {/* Avatar for received messages */}
      {!isOwn && (
        <div
          className={cn(
            'w-9 h-9 rounded-full flex items-center justify-center text-xs font-semibold text-white',
            showAvatar ? 'visible' : 'invisible',
            'bg-gradient-to-br from-violet-500 to-purple-600'
          )}
        >
          {message.sender.avatar}
        </div>
      )}

      {/* Message Bubble */}
      <div className={cn('flex flex-col', isOwn ? 'items-end' : 'items-start')}>
        <div
          className={cn(
            'relative px-4 py-2.5 rounded-2xl',
            isOwn
              ? 'bg-blue-500 text-white rounded-br-md'
              : 'bg-white rounded-bl-md'
          )}
        >
          {/* Message Content */}
          <div className="text-sm">
            <YooptaEditor editor={previewEditor} style={{ width: '100%' }} />
          </div>

          {/* Reactions */}
          {message.reactions && message.reactions.length > 0 && (
            <div className={cn('absolute -bottom-3 flex gap-0.5', isOwn ? 'right-2' : 'left-2')}>
              {message.reactions.map((reaction) => (
                <span
                  key={reaction.emoji}
                  className="px-1.5 py-0.5 rounded-full bg-white border text-xs shadow-sm"
                >
                  {reaction.emoji}
                </span>
              ))}
            </div>
          )}
        </div>

        {/* Timestamp and Status */}
        <div className={cn('flex items-center gap-1 mt-1 px-2', 'text-xs text-neutral-500')}>
          <span>{formatTime(message.timestamp)}</span>
          {isOwn && <MessageStatus status={message.status} />}
        </div>
      </div>
    </div>
  );
}

Chat Input Component

import { ImageCommands } from '@yoopta/image';
import { FileCommands } from '@yoopta/file';
import { Paperclip, Image, Send } from 'lucide-react';

function ChatInput({ onSend }: { onSend: (content: YooptaContentValue) => void }) {
  const editor = useMemo(() => {
    return withEmoji(
      createYooptaEditor({
        plugins: CHAT_INPUT_PLUGINS,
        marks: CHAT_MARKS,
      })
    );
  }, []);

  const handleSend = () => {
    const content = editor.getEditorValue();
    if (Object.keys(content).length > 0) {
      onSend(content);
      editor.setEditorValue({});
    }
  };

  const handleImageUpload = async (file: File) => {
    const url = await uploadImage(file);
    ImageCommands.insertImage(editor, { src: url });
  };

  return (
    <div className="flex gap-2 items-end">
      {/* Attachment buttons */}
      <div className="flex gap-1">
        <Button variant="ghost" size="sm">
          <Paperclip className="h-5 w-5" />
        </Button>
        <label>
          <input
            type="file"
            accept="image/*"
            className="hidden"
            onChange={(e) => {
              const file = e.target.files?.[0];
              if (file) handleImageUpload(file);
            }}
          />
          <Button variant="ghost" size="sm" asChild>
            <span>
              <Image className="h-5 w-5" />
            </span>
          </Button>
        </label>
      </div>

      {/* Editor */}
      <div className="flex-1 border rounded-3xl p-3 bg-white">
        <YooptaEditor
          editor={editor}
          placeholder="Type a message..."
          style={{ minHeight: 24 }}
        />
      </div>

      {/* Send button */}
      <Button onClick={handleSend} className="rounded-full" size="icon">
        <Send className="h-5 w-5" />
      </Button>
    </div>
  );
}

Message Status Component

import { Check, CheckCheck } from 'lucide-react';

function MessageStatus({ status }: { status: 'sent' | 'delivered' | 'read' }) {
  if (status === 'sent') {
    return <Check className="w-3 h-3 text-neutral-400" />;
  }
  if (status === 'delivered') {
    return <CheckCheck className="w-3 h-3 text-neutral-400" />;
  }
  return <CheckCheck className="w-3 h-3 text-blue-500" />;
}

Chat Plugin Configuration

import Paragraph from '@yoopta/paragraph';
import Image from '@yoopta/image';
import Video from '@yoopta/video';
import File from '@yoopta/file';
import Link from '@yoopta/link';
import { Bold, Italic, Strike } from '@yoopta/marks';

export const CHAT_PLUGINS = [
  Paragraph,
  Image,
  Video,
  File,
  Link,
];

export const CHAT_INPUT_PLUGINS = [
  Paragraph,
  Image.extend({
    options: {
      async onUpload(file) {
        const url = await uploadToCDN(file);
        return { src: url };
      },
    },
  }),
  File,
];

export const CHAT_MARKS = [Bold, Italic, Strike];

Source Code

Slack Chat Source

View Slack-style implementation

Social Media Chat Source

View WhatsApp-style implementation

Use Cases

Team Collaboration

Build Slack-like team messaging platforms

Customer Support

Create live chat support systems

Social Messaging

Build WhatsApp-style personal messaging apps

Community Forums

Create discussion boards and forums

Next Steps

Collaboration

Add real-time collaboration with Yjs

Mentions

Learn about @mentions implementation

Build docs developers (and LLMs) love