# 機能設計書 50-ウォッチリスト表示

## 概要

本ドキュメントは、Wikipedia iOSアプリにおけるウォッチリスト表示機能の設計を記載する。ユーザーが監視対象として登録した記事の変更履歴を一覧表示する機能である。

### 本機能の処理概要

ウォッチリスト表示機能は、WatchlistCoordinatorを通じてSpecial:Watchlistページへのナビゲーションを提供し、WatchlistControllerを通じて個別記事のウォッチ/ウォッチ解除操作を管理する。

**業務上の目的・背景**：Wikipediaの編集者やコントリビューターが、関心のある記事の変更を追跡できるようにする。荒らし対策や記事品質の維持に貢献するため、変更を監視する機能が必要とされる。

**機能の利用シーン**：プロファイル画面やサイドメニューから「Watchlist」をタップしてウォッチリスト画面に遷移。記事を追加した際のトーストからも直接アクセス可能。記事ページや差分画面からウォッチ/ウォッチ解除を実行。

**主要な処理内容**：
1. Special:Watchlistページへのナビゲーション
2. 記事のウォッチ（有効期限指定可能）
3. 記事のウォッチ解除
4. ウォッチリストのフィルタリング

**関連システム・外部連携**：MediaWiki API（action=query&list=watchlist、action=watch）。WMFWatchlistDataControllerでAPI通信を管理。NSUserActivityでディープリンクナビゲーション。

**権限による制御**：ウォッチリスト機能はログインが必須。未ログイン状態でウォッチ操作を試みるとログイン画面を表示。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | Special:Watchlist | 主画面 | ウォッチリストのWebView表示 |
| - | 記事画面 | 関連画面 | ウォッチ/ウォッチ解除操作 |
| - | 差分画面 | 関連画面 | ウォッチ/ウォッチ解除操作 |

## 機能種別

データ表示 / 監視管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| pageTitle | String | Yes | 監視対象のページタイトル | - |
| siteURL | URL | Yes | サイトURL | 有効なWikimediaプロジェクトURL |
| expiry | WMFWatchlistExpiryType | No | ウォッチの有効期限 | enum値 |

### 入力データソース

- ユーザー操作（ウォッチボタン）
- NSUserActivity（ディープリンク）
- MWKDataStore（サイトURL）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| WMFWatchlist | struct | ウォッチリストアイテムの配列 |
| WMFWatchlist.Item | struct | 個別のウォッチリストアイテム |
| watched | Bool | ウォッチ成功フラグ |
| unwatched | Bool | ウォッチ解除成功フラグ |

### 出力先

- WebView（Special:Watchlist表示）
- トースト通知（操作結果）
- WatchlistControllerDelegate（操作成功通知）

## 処理フロー

### 処理シーケンス

```
1. WatchlistCoordinator.start()
   ├─ primarySiteURL から Special:Watchlist URL生成
   ├─ NSUserActivity生成
   └─ wmf_navigate(to:) でナビゲーション

2. WatchlistController.watch()
   ├─ authStateIsPermanent確認
   │      └─ false → ログイン画面表示
   ├─ WikimediaProject変換
   ├─ WatchlistFunnelログ
   └─ presentChooseExpiryActionSheet()
          └─ 有効期限選択 → dataController.watch()
                 ├─ 成功 → delegate通知 + トースト表示
                 └─ 失敗 → エラー評価 → 再試行/エラー表示

3. WatchlistController.unwatch()
   ├─ authStateIsPermanent確認
   ├─ WikimediaProject変換
   ├─ WatchlistFunnelログ
   └─ dataController.unwatch()
          ├─ 成功 → delegate通知 + トースト表示
          └─ 失敗 → エラー評価

4. WMFWatchlistDataController.fetchWatchlist()
   ├─ onWatchlistProjects()でプロジェクト取得
   ├─ filterSettings適用
   └─ 各プロジェクトに対してAPI呼び出し（並列）
          └─ 結果マージ → WMFWatchlist返却
```

### フローチャート

```mermaid
flowchart TD
    A[ウォッチリスト表示] --> B{WatchlistCoordinator}
    B --> C[Special:Watchlist URL生成]
    C --> D[NSUserActivity生成]
    D --> E[wmf_navigate]
    E --> F[WebView表示]

    G[ウォッチ操作] --> H{WatchlistController.watch}
    H --> I{ログイン済み?}
    I -->|No| J[ログイン画面表示]
    I -->|Yes| K[有効期限選択ActionSheet]
    J --> L[ログイン後に再実行]
    L --> K
    K --> M[dataController.watch API呼び出し]
    M --> N{結果}
    N -->|成功| O[delegate通知]
    O --> P[成功トースト表示]
    P --> Q[View Watchlistボタン]
    N -->|失敗| R[エラー評価]
    R -->|notloggedin| J
    R -->|その他| S[エラー表示]

    T[ウォッチ解除] --> U{WatchlistController.unwatch}
    U --> V{ログイン済み?}
    V -->|No| W[ログイン画面表示]
    V -->|Yes| X[dataController.unwatch API呼び出し]
    X --> Y{結果}
    Y -->|成功| Z[delegate通知 + トースト]
    Y -->|失敗| AA[エラー評価]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-50-01 | ログイン必須 | ウォッチリスト機能はログインが必要 | 全てのウォッチ操作 |
| BR-50-02 | 有効期限選択 | ウォッチ時に6種類の有効期限から選択 | ウォッチ操作時 |
| BR-50-03 | コンテキスト別ログ | 記事/差分画面でログイベントを区別 | 全てのウォッチ操作 |
| BR-50-04 | ログイン後自動再実行 | ログイン後に保留中の操作を自動実行 | ログイン成功時 |
| BR-50-05 | VoiceOver対応 | VoiceOver有効時はトースト表示時間を延長（10秒） | VoiceOver有効時 |
| BR-50-06 | オフラインキャッシュ | ネットワークエラー時はキャッシュから表示 | fetchWatchlist失敗時 |

### 計算ロジック

有効期限タイプ（WMFWatchlistExpiryType）:
- never: 無期限
- oneWeek: 1週間
- oneMonth: 1ヶ月
- threeMonths: 3ヶ月
- sixMonths: 6ヶ月
- oneYear: 1年

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| fetchWatchlist | SharedCacheStore | READ/WRITE | ウォッチリストのキャッシュ |
| loadFilterSettings | UserDefaultsStore | READ | フィルタ設定の読み込み |
| saveFilterSettings | UserDefaultsStore | WRITE | フィルタ設定の保存 |

### テーブル別操作詳細

#### MediaWiki API

| 操作 | エンドポイント | メソッド | パラメータ | 備考 |
|-----|--------------|---------|-----------|------|
| fetchWatchlist | /w/api.php | GET | action=query&list=watchlist | 複数プロジェクト並列 |
| watch | /w/api.php | POST | action=watch&titles=X&expiry=Y | watchトークン必要 |
| unwatch | /w/api.php | POST | action=watch&unwatch=1&titles=X | watchトークン必要 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| notloggedin | 認証エラー | 未ログイン状態でAPI呼び出し | ログイン画面表示後に再実行 |
| mediaWikiServiceUnavailable | サービスエラー | サービス初期化失敗 | エラー表示 |
| failureCreatingRequestURL | URL生成エラー | URL生成失敗 | エラー表示 |
| unexpectedResponse | レスポンスエラー | 期待されたレスポンス形式でない | エラー表示 |
| isInternetConnectionError | ネットワークエラー | オフライン状態 | キャッシュから表示 |

### リトライ仕様

- notloggedinエラー時：ログイン後に自動リトライ（performAfterLoginBlock）
- ネットワークエラー時：キャッシュ使用（自動リトライなし）

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

該当なし（MediaWiki APIは個別リクエスト）

## パフォーマンス要件

- 複数プロジェクトへの並列リクエスト（DispatchGroup使用）
- オフラインキャッシュによる応答性向上
- wllimit=500で一度に取得する件数を制限

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

- 認証状態をauthStateIsPermanentで厳密に確認
- watchトークンを使用したCSRF対策
- ログイン情報はKeychainで安全に管理

## 備考

- WatchlistControllerDelegate: ウォッチ成功（一時的/永続的）、ウォッチ解除成功の3種類のコールバック
- Context enum: .article（記事画面）、.diff（差分画面）でログイベントを区別
- WatchlistFunnel: ウォッチリスト操作の詳細なイベントログ
- WMFWatchlistFilterSettings: プロジェクト、変更タイプ、アクティビティ等のフィルタ設定

---

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

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

### 推奨読解順序

#### Step 1: 画面遷移を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | WatchlistCoordinator.swift | `Wikipedia/Code/WatchlistCoordinator.swift` | ウォッチリスト画面へのナビゲーション |

**読解のコツ**:
- **15-18行目**: 初期化（navigationController, dataStore）
- **20-29行目**: start() - Special:WatchlistへのURL生成とナビゲーション

#### Step 2: ウォッチ/ウォッチ解除操作を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | WatchlistController.swift | `Wikipedia/Code/WatchlistController.swift` | ウォッチ操作のビジネスロジック |

**主要処理フロー**:
- **12-30行目**: WatchlistController初期化、Context enum、デリゲート設定
- **42-66行目**: watch() - ウォッチ処理（認証確認、ActionSheet表示）
- **126-220行目**: presentChooseExpiryActionSheet() - 有効期限選択
- **222-280行目**: unwatch() - ウォッチ解除処理
- **282-324行目**: evaluateServerError() - エラーハンドリング

#### Step 3: データ層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | WMFWatchlistDataController.swift | `WMFData/Sources/WMFData/Data Controllers/Watchlist/WMFWatchlistDataController.swift` | APIコントローラ |
| 3-2 | WMFWatchlistExpiryType.swift | `WMFData/Sources/WMFData/Models/Watchlist/WMFWatchlistExpiryType.swift` | 有効期限enum |

**主要処理フロー**:
- **96-210行目**: fetchWatchlist() - ウォッチリスト取得（複数プロジェクト並列）
- **314-351行目**: watch() - ウォッチAPIコール
- **355-392行目**: unwatch() - ウォッチ解除APIコール
- **50-58行目**: loadFilterSettings/saveFilterSettings - フィルタ設定

#### Step 4: 成功通知を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | WatchlistController.swift | `Wikipedia/Code/WatchlistController.swift` | トースト表示とナビゲーション |

**主要処理フロー**:
- **68-124行目**: displayWatchSuccessMessage() - 成功トーストとウォッチリストへのナビゲーション

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

```
[ウォッチリスト表示]
WatchlistCoordinator
    │
    └─ start()
           │
           ├─ dataStore.primarySiteURL
           ├─ wmf_URL(withTitle: "Special:Watchlist")
           ├─ NSUserActivity.wmf_activity(for:)
           └─ NSUserActivity.wmf_navigate(to:)

[ウォッチ操作]
WatchlistController
    │
    ├─ watch(pageTitle:siteURL:expiry:viewController:authenticationManager:theme:sender:sourceView:sourceRect:)
    │      │
    │      ├─ authStateIsPermanent確認
    │      │      └─ false → wmf_showLoginViewController()
    │      │
    │      ├─ WikimediaProject(siteURL:)
    │      │
    │      ├─ WatchlistFunnel.shared.logAddToWatchlist()
    │      │
    │      └─ presentChooseExpiryActionSheet()
    │             │
    │             ├─ UIAlertController (ActionSheet)
    │             │      ├─ Permanent
    │             │      ├─ 1 week
    │             │      ├─ 1 month
    │             │      ├─ 3 months
    │             │      ├─ 6 months
    │             │      └─ 1 year
    │             │
    │             └─ dataController.watch(title:project:expiry:)
    │                    │
    │                    ├─ 成功
    │                    │      ├─ delegate?.didSuccessfullyWatchPermanently/Temporarily()
    │                    │      └─ displayWatchSuccessMessage()
    │                    │             └─ WMFAlertManager.showBottomAlertWithMessage()
    │                    │
    │                    └─ 失敗
    │                           └─ evaluateServerError()
    │
    └─ unwatch(pageTitle:siteURL:viewController:authenticationManager:theme:)
           │
           ├─ authStateIsPermanent確認
           │
           ├─ WatchlistFunnel.shared.logRemoveWatchlistItem()
           │
           └─ dataController.unwatch(title:project:)
                  │
                  ├─ 成功
                  │      ├─ WMFAlertManager.showBottomAlertWithMessage()
                  │      └─ delegate?.didSuccessfullyUnwatch()
                  │
                  └─ 失敗
                         └─ evaluateServerError()

[データ層]
WMFWatchlistDataController
    │
    ├─ fetchWatchlist()
    │      │
    │      ├─ onWatchlistProjects()
    │      ├─ loadFilterSettings()
    │      ├─ apply(filterSettings:to:)
    │      │
    │      └─ 各プロジェクトに並列リクエスト (DispatchGroup)
    │             │
    │             └─ WMFMediaWikiServiceRequest (GET)
    │                    action=query&list=watchlist
    │
    ├─ watch(title:project:expiry:)
    │      └─ WMFMediaWikiServiceRequest (POST)
    │             action=watch&titles=X&expiry=Y
    │
    └─ unwatch(title:project:)
           └─ WMFMediaWikiServiceRequest (POST)
                  action=watch&unwatch=1&titles=X
```

### データフロー図

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

ウォッチリスト表示要求  ───▶ WatchlistCoordinator.start()
                           │
                           ▼
                    Special:Watchlist URL生成
                           │
                           ▼
                    NSUserActivity.wmf_navigate() ───────▶ WebView表示

ウォッチボタン操作    ───▶ WatchlistController.watch()
                           │
                           ▼
                    {認証状態確認}
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
        未ログイン                  ログイン済み
              │                         │
              ▼                         ▼
        ログイン画面            有効期限ActionSheet
              │                         │
              └──────────┬──────────────┘
                         ▼
                  WMFWatchlistDataController.watch()
                         │
                         ▼
                  MediaWiki API (POST action=watch)
                         │
                         ▼
              ┌──────────┴──────────┐
              ▼                     ▼
           成功                   失敗
              │                     │
              ▼                     ▼
      delegate通知              evaluateServerError()
      トースト表示                  │
                           ┌───────┴───────┐
                           ▼               ▼
                     notloggedin       その他
                           │               │
                           ▼               ▼
                     ログイン画面      エラー表示
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| WatchlistCoordinator.swift | `Wikipedia/Code/WatchlistCoordinator.swift` | ソース | ウォッチリスト画面へのナビゲーション |
| WatchlistController.swift | `Wikipedia/Code/WatchlistController.swift` | ソース | ウォッチ/ウォッチ解除のビジネスロジック |
| WMFWatchlistDataController.swift | `WMFData/Sources/WMFData/Data Controllers/Watchlist/WMFWatchlistDataController.swift` | ソース | API通信コントローラ |
| WMFWatchlistExpiryType.swift | `WMFData/Sources/WMFData/Models/Watchlist/WMFWatchlistExpiryType.swift` | ソース | 有効期限enum定義 |
| WMFWatchlistFilterSettings.swift | `WMFData/Sources/WMFData/Models/Watchlist/WMFWatchlistFilterSettings.swift` | ソース | フィルタ設定モデル |
| WMFWatchlist.swift | `WMFData/Sources/WMFData/Models/Watchlist/WMFWatchlist.swift` | ソース | ウォッチリストモデル |
| WatchlistFunnel.swift | `Wikipedia/Code/WatchlistFunnel.swift` | ソース | イベントログ |
| WMFAlertManager.swift | - | ソース | トースト表示 |
