Skip to content

Building Custom UI

If the built-in widget doesn't fit your design, use PulseClient directly to build your own collaboration UI.

When to Go Custom

  • You need the collaboration data in a different layout
  • You want to embed comments inside your existing UI (not a floating panel)
  • You only need some features (e.g., just presence, no comments)

React Example: Comments

tsx
import { PulseClient } from '@gamention/pulse-core';
import type { Thread } from '@gamention/pulse-shared';
import { useEffect, useState } from 'react';

function useThreads(client: PulseClient) {
  const [threads, setThreads] = useState<Thread[]>([]);
  useEffect(() => {
    setThreads(client.state.threads);
    return client.state.on('threads', setThreads);
  }, [client]);
  return threads;
}

function Comments({ client }: { client: PulseClient }) {
  const threads = useThreads(client);
  const [text, setText] = useState('');

  return (
    <div className="comments">
      {threads.map(thread => (
        <div key={thread.id}>
          {thread.comments.map(c => (
            <div key={c.id}>
              <strong>{client.state.getUser(c.userId)?.name}</strong>
              <p>{c.body}</p>
            </div>
          ))}
          <input
            value={text}
            onChange={e => setText(e.target.value)}
            onKeyDown={e => {
              if (e.key === 'Enter' && text.trim()) {
                client.reply(thread.id, text);
                setText('');
              }
            }}
            placeholder="Reply..."
          />
        </div>
      ))}
    </div>
  );
}

React Example: Presence Bar

tsx
import type { PresenceUser } from '@gamention/pulse-shared';

function PresenceBar({ client }: { client: PulseClient }) {
  const [users, setUsers] = useState<PresenceUser[]>([]);
  useEffect(() => {
    setUsers(client.state.presence);
    return client.state.on('presence', setUsers);
  }, [client]);

  return (
    <div className="presence">
      {users.map(({ user, status }) => (
        <div
          key={user.id}
          className={`avatar ${status}`}
          style={{ background: user.color }}
          title={`${user.name} (${status})`}
        >
          {user.avatar ? <img src={user.avatar} /> : user.name[0]}
        </div>
      ))}
    </div>
  );
}

Lifecycle

Always clean up when the component unmounts:

tsx
function App() {
  const [client, setClient] = useState<PulseClient | null>(null);

  useEffect(() => {
    const c = new PulseClient({
      apiKey: 'pk_...',
      token: userToken,
      room: 'my-room',
      endpoint: 'wss://pulse.hire.rest'
    });
    c.connect();
    setClient(c);
    return () => c.disconnect();
  }, []);

  if (!client) return null;

  return (
    <>
      <PresenceBar client={client} />
      <Comments client={client} />
    </>
  );
}

Mixing Widget + Custom UI

You can use <pulse-widget> for most features and only build custom components for specific parts. The widget and PulseClient are independent — just make sure they connect to the same room.

Pulse Collaboration SDK