# 画面設計書 64-ActivityPubリーダー画面

## 概要

本ドキュメントは、Ghost管理画面における「ActivityPubリーダー画面（Reader）」の設計仕様を記述したものである。この画面では、フォローしているアカウントやトピック別の長文投稿（Article）を閲覧できる。

### 本画面の処理概要

ActivityPubリーダー画面は、フェディバース（ActivityPubネットワーク）上のコンテンツを閲覧するためのリーダービューである。フォローしているアカウントの投稿や、トピック別のDiscovery Feedを一覧表示し、記事を選択するとモーダルで詳細表示できる。

**業務上の目的・背景**：Ghostはソーシャルウェブ（Fediverse）への対応を進めており、ActivityPub対応のパブリッシャーやMastodon等の他サービスからのコンテンツを直接Ghost管理画面内で閲覧できる。これにより、他のパブリッシャーの動向を把握したり、インスピレーションを得たりすることが可能になる。また、トピック別のフィードにより、特定のテーマに関するコンテンツを効率的に発見できる。

**画面へのアクセス方法**：Ghost管理画面のサイドバーから「Social」セクションへアクセスし、「Reader」を選択。URL は `/ghost/#/activitypub/reader`。

**主要な操作・処理内容**：
1. フォロー中アカウントの投稿一覧表示（Following）
2. トピック別Discovery Feedの表示（Topic Filter）
3. 記事のモーダル表示（Reader Modal）
4. 無限スクロールによる追加コンテンツ読み込み
5. 投稿へのいいね・リポスト・返信操作

**画面遷移**：サイドバーからアクセス。記事クリックでReaderモーダル表示。プロフィールアイコンクリックでプロフィール画面へ遷移。

**権限による表示制御**：ActivityPub機能が有効化されている場合にのみ表示される。スタッフユーザーがアクセス可能。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 57 | ActivityPub | 主機能 | ActivityPubフィードの閲覧 |

## 画面種別

一覧画面 + モーダル詳細表示

## URL/ルーティング

- メインパス: `/ghost/#/activitypub/reader`
- 記事詳細: `/ghost/#/activitypub/reader/:postId`
- ページタイトル: 「Reader」

## 入出力項目

### フィルター入力

| 項目名 | データ型 | 入力/出力 | 説明 |
|--------|----------|----------|------|
| トピック | Topic | 入力 | 表示するトピック（following/ghost/tech/culture等） |

## 表示項目

### 投稿一覧項目

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| アバター | 画像 | 投稿者のアバター画像 |
| 投稿者名 | テキスト | アカウント表示名 |
| ハンドル | テキスト | @username@domain形式 |
| 投稿タイトル | テキスト | 記事タイトル（Article type） |
| 投稿本文プレビュー | テキスト | 本文の冒頭部分 |
| フィーチャー画像 | 画像 | 記事のアイキャッチ画像（存在する場合） |
| いいね数 | 数値 | likeCount |
| リポスト数 | 数値 | repostCount |
| 返信数 | 数値 | replyCount |
| 投稿日時 | テキスト | 相対時間または日付表示 |

### トピックフィルター

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| Following | タブ | フォロー中アカウントの投稿 |
| Ghost | タブ | Ghost関連トピック |
| Tech | タブ | テクノロジートピック |
| Culture | タブ | カルチャートピック |
| その他トピック | タブ | 動的に取得されるトピック |

### Readerモーダル

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| 記事タイトル | テキスト | 記事の完全なタイトル |
| 記事本文 | リッチテキスト | 記事の全文 |
| 投稿者情報 | コンポーネント | アバター、名前、ハンドル |
| アクションボタン | ボタン群 | いいね、リポスト、返信、共有 |

## イベント仕様

### 1-トピック切り替え

1. TopicFilterのタブをクリック
2. `onTopicChange` コールバックで `topic` 状態を更新
3. 「following」の場合は `useInboxForUser` を使用
4. それ以外の場合は `useDiscoveryFeedForUser` を使用（topic パラメータ付き）
5. 新しいフィードデータを取得・表示

### 2-記事クリック

1. FeedItem をクリック
2. `navigate('/reader/{encodedPostId}')` を実行
3. URLパラメータ（postId）の変化を検知
4. `isReaderOpen` を `true` に設定
5. Reader モーダルを開く
6. 対象記事の詳細データを取得・表示

### 3-モーダルクローズ

1. モーダル外クリックまたは閉じるボタンクリック
2. `goBack()` または `navigate('/reader')` を実行
3. `isReaderOpen` を `false` に設定
4. モーダルを閉じる

### 4-無限スクロール

1. IntersectionObserver がロードトリガー位置（リスト75%地点）を検知
2. `hasNextPage` が true かつ `isFetchingNextPage` が false の場合
3. `fetchNextPage` を実行
4. 次ページのデータを取得
5. 既存リストに追加表示

### 5-いいね操作

1. FeedItemのいいねボタンをクリック
2. ActivityPub API経由でlike/unlike操作を実行
3. 楽観的更新でUIを即時反映
4. API成功時：キャッシュ更新
5. API失敗時：UIをロールバック、エラートースト表示

### 6-プロフィール遷移

1. 投稿者アバターまたは名前をクリック
2. `handleProfileClick` 関数を実行
3. `/profile/{handle}` へ遷移

## データベース更新仕様

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

この画面からの直接的なデータベース更新はなし。ActivityPub APIを経由したリモートサーバーとの通信が行われる。

| 操作（イベント） | 対象 | 操作種別 | 概要 |
|----------------|------|---------|------|
| フィード取得 | ActivityPub API | GET | フィードデータの取得 |
| いいね | ActivityPub API | POST/DELETE | Like/Unlike操作 |
| リポスト | ActivityPub API | POST/DELETE | Announce操作 |

## メッセージ仕様

| メッセージ種別 | メッセージ内容 | 表示条件 |
|---------------|---------------|----------|
| 情報 | Your Reader is empty | フォロー中でフィードが空の場合 |
| 情報 | Start following publishers to see their long-form posts here. | 空フィード時の説明 |
| 情報 | Nothing here yet | トピック別フィードが空の場合 |
| 情報 | Explore other topics for more content. | トピック空時の説明 |
| ボタン | Find accounts to follow | 空フィード時のアクションボタン |

## 例外処理

| 例外条件 | 処理内容 |
|---------|---------|
| API エラー | AppError コンポーネントでエラーコード・ステータスコードを表示 |
| トピック 404 | 空の配列として扱い、「Nothing here yet」を表示 |
| ネットワークエラー | ローディング表示のまま、またはエラー画面表示 |

## 備考

- トピック「following」はInbox APIを使用、それ以外はDiscovery Feed APIを使用
- フィードは無限スクロールで、約75%スクロール位置で次ページ取得開始
- 記事詳細はフルスクリーンモーダルで表示（高さ: calc(100vh-24px)）
- トピックはサーバーから動的に取得される（`useTopicsForUser`）
- ActivityPub機能は Labs 設定で有効化が必要な場合がある

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

ActivityPubフィードで扱うデータはActivityオブジェクトで、投稿者（actor）とコンテンツ（object）を含む。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | activitypub.ts | `apps/admin-x-framework/src/api/activitypub.ts` | Activity型、ActorProperties型の定義 |
| 1-2 | activitypub.ts | `apps/activitypub/src/api/activitypub.ts` | Post型、Account型の定義 |

**読解のコツ**: Activity型はActivityPub仕様に基づいており、`type`（Create/Announce等）、`actor`（投稿者）、`object`（コンテンツ）の構造を持つ。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | inbox.tsx | `apps/activitypub/src/views/inbox/inbox.tsx` | Inboxコンポーネント（8-36行目） |

**主要処理フロー**:
1. **9行目**: トピック状態（デフォルト: 'following'）
2. **11-12行目**: フォローイングとディスカバリーフィードのクエリ
3. **14-15行目**: トピックに応じたクエリデータの選択
4. **17-18行目**: トピック404エラーの判定
5. **23行目**: activitiesデータの構築

#### Step 3: 一覧表示コンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | inbox-list.tsx | `apps/activitypub/src/views/inbox/components/inbox-list.tsx` | InboxListコンポーネント（23-194行目） |

**主要処理フロー**:
- **32-38行目**: パラメータとトピックの取得
- **44-72行目**: IntersectionObserverによる無限スクロール
- **88-117行目**: FeedItemの一覧レンダリング
- **163-189行目**: Readerモーダルの制御

#### Step 4: フィードAPIフックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | use-activity-pub-queries.ts | `apps/activitypub/src/hooks/use-activity-pub-queries.ts` | useInboxForUser, useDiscoveryFeedForUser |

**主要処理フロー**:
- `useInboxForUser`: `/inbox` エンドポイントからフォロー中フィードを取得
- `useDiscoveryFeedForUser`: `/discover/{topic}` エンドポイントからトピック別フィードを取得

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

```
Inbox (inbox.tsx)
    │
    ├─ useInboxForUser (Following)
    │      └─ GET /inbox
    │
    ├─ useDiscoveryFeedForUser (Topics)
    │      └─ GET /discover/{topic}
    │
    └─ InboxList (inbox-list.tsx)
           │
           ├─ Layout
           │
           ├─ TopicFilter
           │      └─ useTopicsForUser
           │
           ├─ FeedItem[] (一覧)
           │      ├─ APAvatar
           │      ├─ FeedItemStats
           │      └─ onClick → navigate('/reader/{postId}')
           │
           ├─ IntersectionObserver
           │      └─ fetchNextPage
           │
           └─ Dialog (Reader Modal)
                  └─ Reader (reader.tsx)
                         ├─ 記事詳細表示
                         └─ アクションボタン
```

### データフロー図

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

ページロード ────────▶ topic === 'following'
                            │
                            ├─ Yes ──▶ useInboxForUser
                            │             └─ GET /inbox ───────▶ Activity[]
                            │
                            └─ No ───▶ useDiscoveryFeedForUser
                                          └─ GET /discover/{topic} ─▶ Activity[]

トピック切り替え ───▶ setTopic(newTopic)
                            │
                            └─ クエリ再実行 ──────────────▶ 新しいフィード表示

記事クリック ───────▶ navigate('/reader/{postId}')
                            │
                            └─ isReaderOpen = true
                                   │
                                   └─ Reader Modal表示
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| inbox.tsx | `apps/activitypub/src/views/inbox/inbox.tsx` | ソース | Inboxメインコンポーネント |
| inbox-list.tsx | `apps/activitypub/src/views/inbox/components/inbox-list.tsx` | ソース | 一覧表示コンポーネント |
| reader.tsx | `apps/activitypub/src/views/inbox/components/reader.tsx` | ソース | 記事詳細モーダル |
| feed-item.tsx | `apps/activitypub/src/components/feed/feed-item.tsx` | ソース | フィードアイテムコンポーネント |
| topic-filter.tsx | `apps/activitypub/src/components/topic-filter.tsx` | ソース | トピックフィルターコンポーネント |
| use-activity-pub-queries.ts | `apps/activitypub/src/hooks/use-activity-pub-queries.ts` | ソース | ActivityPub APIフック |
| routes.tsx | `apps/activitypub/src/routes.tsx` | ソース | ルーティング定義 |
