← ユースケース

入力中表示とオンライン状態つきのチャット

サポートチャット、マーケットプレイスのメッセージ、チームのルーム:すぐ届くメッセージ、ブラウザ間の「入力中…」表示、常に正しいオンライン一覧。

チャットは、ユーザーが遅延を一番強く感じる場所です。1 秒遅れて表示されるメッセージや、いつまでも出ない「入力中…」は、プロダクトを放置されたように見せます。サポートチャット、マーケットプレイスの買い手と売り手のメッセージ、チームのルーム、ゲーム内チャットに必要なのは同じ 3 つ:すぐ届くメッセージ、入力中表示、そして正直なオンライン一覧です。

ユーザーにとっての価値

  • 送った瞬間に、ルームの全員にメッセージが表示されます。再読み込みは不要です。
  • 誰かが返信を書いている間は「Ann が入力中…」と表示されます。
  • いま会話にいる人の名前の横に緑の丸がつきます。

使う機能

  • プレゼンスチャンネル(presence-)。ルームのメンバー一覧をすべてのノードで正しく保ちます。
  • クライアントイベント(client-*、Laravel Echo の whisper)。入力中表示をサーバーを通さずブラウザ間で送ります。
  • ソケットの除外(socket_id、Laravel の toOthers())。送信者に自分のメッセージが二重に届きません。
  • Webhook(member_added、member_removed)。メンバーの出入りにサーバーが反応したいときに。

ステップ 1:ルームを認可する

会話のメンバーだけがそのチャンネルに参加できます。認可ルートで判定し、メンバー一覧に表示するユーザー情報を返します:

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const room = channel_name.replace(/^presence-room-/, '');
  if (!(await db.rooms.isMember(room, req.user.id))) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(socket_id, channel_name, {
    user_id: String(req.user.id),
    user_info: { name: req.user.name, avatar: req.user.avatarUrl },
  }));
});

ステップ 2:参加、メンバー一覧、入力中表示

アプリの設定で クライアントイベントを許可 をオンにしてから:

const room = pusher.subscribe('presence-room-42');

room.bind('pusher:subscription_succeeded', (members) => {
  members.each((m) => online.add(m.id, m.info));
});
room.bind('pusher:member_added', (m) => online.add(m.id, m.info));
room.bind('pusher:member_removed', (m) => online.remove(m.id));

// Typing: at most one event every 2 seconds while the user writes
let last = 0;
input.addEventListener('input', () => {
  if (Date.now() - last < 2000) return;
  last = Date.now();
  room.trigger('client-typing', { name: me.name });
});
room.bind('client-typing', ({ name }) => typing.show(name, 3000));

room.bind('message', (m) => messages.append(m));

ステップ 3:メッセージはサーバー経由で送る

メッセージはサーバーを通すことで、保存とモデレーションを済ませてから配信できます。送信者のソケット ID を渡せば、すでにメッセージを表示している送信者のタブには送られません:

app.post('/rooms/:id/messages', requireLogin, async (req, res) => {
  const msg = await db.messages.create({ room: req.params.id, user: req.user.id, text: req.body.text });
  await pusher.trigger('presence-room-' + req.params.id, 'message', msg, { socket_id: req.body.socket_id });
  res.json(msg);
});

ブラウザはリクエストに pusher.connection.socket_id を含めます。

Laravel の場合

// routes/channels.php
Broadcast::channel('room.{id}', function ($user, $id) {
    return $user->rooms()->whereKey($id)->exists() ? ['id' => $user->id, 'name' => $user->name] : false;
});

// In the controller
broadcast(new MessageSent($message))->toOthers();
Echo.join(`room.${roomId}`)
  .here((users) => online.set(users))
  .joining((user) => online.add(user))
  .leaving((user) => online.remove(user))
  .listen('MessageSent', (e) => messages.append(e.message))
  .listenForWhisper('typing', (e) => typing.show(e.name, 3000));

Echo.join(`room.${roomId}`).whisper('typing', { name: me.name });

ヒント

  • プレゼンスチャンネルのメンバーは最大 100 人です。ライブ配信のチャットのような大きなルームはライブオークションと大人数の視聴者を参照してください。
  • クライアントイベントはブラウザ側で間引きましょう。接続ごとにレート制限があり、1 件ごとに 1 メッセージとして数えます。
  • ルームを離れた人に知らせるには、アプリ内通知とプッシュを組み合わせます。