# 機能設計書 39-入力状態表示

## 概要

本ドキュメントは、Chatwootにおける会話の入力状態表示（タイピングインジケーター）機能の設計仕様を記載する。ユーザーがメッセージを入力中であることをリアルタイムで他の参加者に通知する機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポートのチャットコミュニケーションにおいて、相手が入力中であることを知ることで、より自然な対話体験を提供できる。顧客は「エージェントが対応中である」ことを視覚的に確認でき、エージェントは「顧客が返信を書いている」ことを把握できる。これにより、無駄な追加メッセージの送信を防ぎ、円滑なコミュニケーションを実現する。

**機能の利用シーン**：
- エージェントが返信を入力している際に、顧客に「入力中」インジケーターを表示
- 顧客がメッセージを入力している際に、エージェントに「入力中」インジケーターを表示
- プライベートノートを入力している際に、他のエージェントのみに通知（顧客には非表示）

**主要な処理内容**：
1. タイピング状態の切り替え：toggle_typing_statusアクションでON/OFFを切り替え
2. WebSocketでのリアルタイム通知：ActionCableを通じて関係者に即座に通知
3. プライベートフラグ：is_privateフラグにより、プライベートノート入力時は顧客に通知しない

**関連システム・外部連携**：
- ActionCable / WebSocket：リアルタイム通知
- Rails Dispatcher：イベントの発行

**権限による制御**：
- 会話へのアクセス権限（ConversationPolicy#show?）を持つユーザーのみがタイピング状態を送信可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | メッセージ入力時の自動送信、インジケーター表示 |
| 116 | Widget メッセージ画面 | 主画面 | ウィジェットでの入力状態表示 |

## 機能種別

リアルタイム通知 / WebSocket

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| account_id | Integer | Yes | アカウントID（URLパラメータ） | 存在チェック |
| id | Integer | Yes | 会話のdisplay_id（URLパラメータ） | 存在チェック |
| typing_status | String | Yes | タイピング状態（"on" または "off"） | "on"/"off"のみ |
| is_private | Boolean | No | プライベートノート入力中フラグ | true/false |

### 入力データソース

- URLパラメータ: account_id, id (conversation display_id)
- リクエストボディ: typing_status, is_private

## 出力仕様

### 出力データ

#### HTTPレスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| - | - | 200 OK（ボディなし） |

#### WebSocket通知データ（CONVERSATION_TYPING_ON/OFF）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| conversation | Object | 会話のpush_event_data |
| user | Object | 入力中ユーザーのpush_event_data |
| is_private | Boolean | プライベートノート入力中フラグ |
| account_id | Integer | アカウントID |

### 出力先

- HTTPレスポンス: head :ok
- WebSocket: ActionCableで関係者に配信

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ account_id, conversation_idの検証

2. 会話取得・認可チェック
   └─ ConversationPolicy#show?

3. TypingStatusManagerの生成
   └─ 会話、ユーザー、パラメータを渡す

4. タイピング状態の切り替え
   └─ toggle_typing_status
       ├─ typing_status = 'on' → CONVERSATION_TYPING_ON イベント発行
       └─ typing_status = 'off' → CONVERSATION_TYPING_OFF イベント発行

5. イベント配信
   └─ Rails Dispatcher → ActionCableListener
       └─ typing_event_listener_tokens で配信先を決定
       └─ ActionCableBroadcastJob.perform_later

6. レスポンス生成
   └─ head :ok
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[会話取得]
    B --> C{認可チェック}
    C -->|失敗| D[403 Forbidden]
    C -->|成功| E[TypingStatusManager生成]

    E --> F{typing_status}
    F -->|on| G[CONVERSATION_TYPING_ON発行]
    F -->|off| H[CONVERSATION_TYPING_OFF発行]

    G --> I[ActionCableListener]
    H --> I

    I --> J[配信先トークン取得]
    J --> K{is_private?}
    K -->|Yes| L[エージェントのみに配信]
    K -->|No| M[エージェント+顧客に配信]

    L --> N[WebSocket配信]
    M --> N
    N --> O[200 OK]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-39-1 | プライベート通知 | is_private=trueの場合、顧客には通知しない | プライベートノート入力時 |
| BR-39-2 | 自己除外 | 入力中のユーザー自身には通知しない | 常時 |
| BR-39-3 | インボックスメンバー限定 | 通知先はインボックスのメンバーと顧客のみ | 常時 |

### 計算ロジック

- 配信先トークン: `(user_tokens + contact_inbox_token) - current_user_token`

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| toggle_typing_status | なし | - | DBへの書き込みなし |

**注意**: 本機能はDBへの書き込みを行わない。タイピング状態はリアルタイムでWebSocket経由で配信されるのみ。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | 会話が存在しない | 正しいconversation_idを指定 |
| 403 | Forbidden | 会話へのアクセス権限がない | 権限を持つユーザーでアクセス |

### リトライ仕様

- WebSocket配信はJobs経由で非同期実行されるため、Sidekiqのリトライ機構に従う

## トランザクション仕様

- DBトランザクションは使用しない（DB書き込みなし）

## パフォーマンス要件

- リアルタイム性が重要なため、即座にWebSocket通知を配信
- ActionCableBroadcastJobは非同期で実行

## セキュリティ考慮事項

- ConversationPolicy#show?による会話アクセス権限チェック
- プライベートノート入力時は顧客に通知しない（is_private制御）
- 入力者自身には通知しない（current_user_tokenの除外）

## 備考

- タイピング状態はメモリ/セッションに保存されず、リアルタイムイベントとして配信されるのみ
- フロントエンドは一定時間（通常数秒）後に自動的に「off」を送信するべき
- オフライン時やWebSocket切断時は通知されない

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: イベント型を理解する

まず、タイピングイベントの定義を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.rb | `lib/events/types.rb` | CONVERSATION_TYPING_ON/OFFの定義 |

**読解のコツ**: Events::Typesモジュールに全イベント型が定義されている。

**主要処理フロー**:
- **29行目**: CONVERSATION_TYPING_ON = 'conversation.typing_on'
- **30行目**: CONVERSATION_TYPING_OFF = 'conversation.typing_off'

#### Step 2: コントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | toggle_typing_statusアクション |

**主要処理フロー**:
- **106-110行目**: toggle_typing_status
  - TypingStatusManager生成
  - toggle_typing_status呼び出し
  - head :ok返却

#### Step 3: TypingStatusManagerを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | typing_status_manager.rb | `app/services/conversations/typing_status_manager.rb` | タイピング状態管理サービス |

**主要処理フロー**:
- **6-10行目**: initialize - 会話、ユーザー、パラメータの保持
- **12-15行目**: trigger_typing_event - Dispatcherでイベント発行
- **17-24行目**: toggle_typing_status
  - typing_status='on' → CONVERSATION_TYPING_ON
  - typing_status='off' → CONVERSATION_TYPING_OFF

#### Step 4: ActionCableListenerを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | action_cable_listener.rb | `app/listeners/action_cable_listener.rb` | WebSocket配信リスナー |

**主要処理フロー**:
- **93-107行目**: conversation_typing_on
  - 会話、アカウント、ユーザー情報取得
  - typing_event_listener_tokensで配信先決定
  - broadcastで配信
- **109-123行目**: conversation_typing_off
  - 同様の処理
- **182-185行目**: typing_event_listener_tokens
  - インボックスメンバー + 顧客 - 入力者本人

### プログラム呼び出し階層図

```
Api::V1::Accounts::ConversationsController#toggle_typing_status
    │
    ├─ conversation (before_action)
    │      └─ Current.account.conversations.find_by!
    │      └─ authorize @conversation, :show?
    │
    ├─ Conversations::TypingStatusManager.new
    │      └─ @conversation, current_user, params
    │
    └─ toggle_typing_status
           │
           └─ trigger_typing_event(event, is_private)
                  │
                  └─ Rails.configuration.dispatcher.dispatch
                         │
                         ├─ CONVERSATION_TYPING_ON
                         └─ CONVERSATION_TYPING_OFF
                                │
                                └─ ActionCableListener
                                       │
                                       ├─ conversation_typing_on
                                       └─ conversation_typing_off
                                              │
                                              └─ typing_event_listener_tokens
                                              └─ broadcast
                                                     │
                                                     └─ ActionCableBroadcastJob
                                                            │
                                                            └─ WebSocket配信
```

### データフロー図

```
[入力]                    [処理]                         [出力]

typing_status   ───▶ ConversationsController  ───▶ 200 OK
is_private              │
                        ▼
              TypingStatusManager
                        │
                        ▼
                    Dispatcher
                        │
                        ▼
              ActionCableListener
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
    typing_event_listener_tokens   broadcast
              │                   │
              ▼                   ▼
    (members + contact)    ActionCableBroadcastJob
     - current_user              │
                                 ▼
                           WebSocket
                                 │
                           ┌─────┴─────┐
                           ▼           ▼
                      Agents       Contact
                      (Dashboard)  (Widget)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | コントローラー | toggle_typing_status（106-110行目） |
| typing_status_manager.rb | `app/services/conversations/typing_status_manager.rb` | サービス | タイピング状態管理 |
| action_cable_listener.rb | `app/listeners/action_cable_listener.rb` | リスナー | WebSocket配信（93-123行目） |
| types.rb | `lib/events/types.rb` | 定義 | イベント型定義（29-30行目） |
| conversation_policy.rb | `app/policies/conversation_policy.rb` | ポリシー | アクセス権限制御 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（150行目） |
