コンテンツにスキップ

検索はプロダクションビルドでのみ利用可能です。 ローカルでテストするには、サイトをビルドしてプレビューしてください。

既読管理

Speakeasy のチャットでは、ユーザーごとの既読状態を管理し、未読メッセージ数をリアルタイムに表示します。


既読状態は user_room_state テーブルで管理されます。

フィールド説明
roomIdUUIDルーム ID(複合PK)
userIdtextユーザー ID(複合PK)
userTypetextshop or customer(複合PK)
lastReadAttimestamp最終既読日時

SELECT
cm.roomId,
COUNT(*)::int AS unreadCount
FROM chat_message cm
LEFT JOIN user_room_state urs
ON cm.roomId = urs.roomId
AND urs.userId = :userId
AND urs.userType = :userType
WHERE cm.roomId IN (:roomIds)
AND (urs.lastReadAt IS NULL OR cm.sentAt > urs.lastReadAt)
GROUP BY cm.roomId
  1. LEFT JOIN により、user_room_state が存在しない場合も含む
  2. lastReadAt IS NULL — 一度も閲覧していないルームは全メッセージが未読
  3. sentAt > lastReadAt — 最終閲覧日時より新しいメッセージをカウント
  4. 結果に含まれないルームは未読数 0
  • ルーム一覧取得時に 1回のバッチクエリ で全ルームの未読数を取得
  • ルームごとに個別クエリを発行しない
  • chat_message(roomId, sentAt) インデックスを使用

await db
.insert(userRoomState)
.values({
roomId,
userId,
userType,
lastReadAt: new Date()
})
.onConflictDoUpdate({
target: [userRoomState.roomId, userRoomState.userId, userRoomState.userType],
set: { lastReadAt: new Date() }
});

送信者のメッセージは自動的に既読として扱われます。

// sendMessage トランザクション内
await db
.insert(userRoomState)
.values({ roomId, userId: senderId, userType: senderType, lastReadAt: now })
.onConflictDoUpdate({ set: { lastReadAt: now } });

配信メッセージは店舗スタッフ側で自動既読になります。

// broadcast queue 処理内
// 店舗ユーザーの lastReadAt を一括更新
await db
.insert(userRoomState)
.values(
targetRoomIds.map((roomId) => ({
roomId,
userId: shopUserId,
userType: 'shop',
lastReadAt: now
}))
)
.onConflictDoUpdate({ set: { lastReadAt: now } });

店舗スタッフは顧客の最終既読日時を取得できます。

Fizz RPC: getCustomerLastReadAt(roomId)
SELECT lastReadAt
FROM user_room_state
WHERE roomId = :roomId
AND userType = 'customer'

┌──────────────────────────┐
│ 顧客A (3) 14:30 │ ← 未読バッジ
│ 最新メッセージ... │
├──────────────────────────┤
│ 顧客B 13:00│ ← 未読なし
│ 最新メッセージ... │
└──────────────────────────┘

ダッシュボードでは未読ありのルームのみを表示します。

getChatRooms({ unreadOnly: true, limit: 5 });

連絡先のチャットタブに未読バッジが表示されます。

[プロフィール] [チャット (2)] ← タブに未読数表示

WebSocket 経由で既読通知がリアルタイムに配信されます。

イベント送信先用途
read_receipt同じルームの参加者既読インジケーター更新
messageルーム参加者未読カウント増加