# 画面設計書 65-ノート一覧画面

## 概要

本ドキュメントは、Ghost管理画面における「ノート一覧画面（Notes/Feed）」の設計仕様を記述したものである。この画面では、フォローしているアカウントからの短文投稿（Note）の一覧表示と、自分自身のノート投稿が可能。

### 本画面の処理概要

ノート一覧画面は、ActivityPubネットワーク上の短文投稿（Note）を表示・投稿するためのフィードビューである。Twitterのタイムラインに近いUIで、フォローしているアカウントからの投稿をリアルタイムで閲覧でき、自分自身もノートを投稿できる。

**業務上の目的・背景**：Ghostの長文記事（Article）に加えて、短いメッセージやアップデートを発信するための機能として提供されている。ActivityPub対応により、Mastodon等の他のフェディバースサービスのユーザーとも相互にやり取りが可能。フォロワーとのカジュアルなコミュニケーションや、記事公開のお知らせ、日常的な更新など、多様な用途に使用される。

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

**主要な操作・処理内容**：
1. フォロー中アカウントのノート一覧表示
2. 新規ノートの投稿（FeedInput / NewNoteModal）
3. ノートへのいいね・リポスト・返信操作
4. 無限スクロールによる追加コンテンツ読み込み
5. おすすめプロフィールの表示（SuggestedProfiles）
6. ノート詳細画面への遷移

**画面遷移**：サイドバーからアクセス。ノートクリックでノート詳細画面へ遷移。プロフィールアイコンクリックでプロフィール画面へ遷移。

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

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 57 | ActivityPub | 主機能 | 短文投稿（ノート）の一覧表示・投稿 |

## 画面種別

一覧画面 + 投稿フォーム

## URL/ルーティング

- メインパス: `/ghost/#/activitypub/notes`
- ページタイトル: 「Notes」

## 入出力項目

### ノート投稿入力

| 項目名 | データ型 | 入力/出力 | 説明 |
|--------|----------|----------|------|
| ノート本文 | string | 入力 | 投稿するノートのテキスト |

## 表示項目

### ノート一覧項目

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| アバター | 画像 | 投稿者のアバター画像 |
| 投稿者名 | テキスト | アカウント表示名 |
| ハンドル | テキスト | @username@domain形式 |
| ノート本文 | テキスト/リッチテキスト | ノートの内容 |
| 添付画像 | 画像 | 添付されたメディア（存在する場合） |
| いいね数 | 数値 | likeCount |
| リポスト数 | 数値 | repostCount |
| 返信数 | 数値 | replyCount |
| 投稿日時 | テキスト | 相対時間表示 |
| いいね済みフラグ | アイコン | 自分がいいねしているか |
| リポスト済みフラグ | アイコン | 自分がリポストしているか |

### 投稿フォーム（FeedInput）

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| ユーザーアバター | 画像 | ログインユーザーのアバター |
| テキストエリア | 入力欄 | ノート本文入力（クリックでモーダル表示） |
| 投稿ボタン | ボタン | ノート投稿を実行 |

### おすすめプロフィール（SuggestedProfiles）

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| プロフィールカード | カード | おすすめアカウントのリスト |
| アバター | 画像 | アカウントのアバター |
| 名前 | テキスト | アカウント表示名 |
| ハンドル | テキスト | @username@domain形式 |
| フォローボタン | ボタン | フォロー/フォロー解除 |

## イベント仕様

### 1-FeedInput クリック

1. FeedInput コンポーネントをクリック
2. NewNoteModal が開く
3. テキスト入力欄にフォーカス
4. ノート本文を入力

### 2-ノート投稿

1. モーダルまたはFeedInputで投稿ボタンをクリック
2. 楽観的更新：UIに即時反映（pending状態）
3. ActivityPub API経由でNote投稿を実行
4. 成功時：pending状態を解除、正式なIDで更新
5. 失敗時：UIからpending投稿を削除、エラートースト表示

### 3-ノートクリック

1. FeedItemをクリック
2. `navigate('/notes/{encodedPostId}')` を実行
3. ノート詳細画面へ遷移

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

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

### 5-いいね操作

1. FeedItemのいいねボタンをクリック
2. 楽観的更新：いいね数をインクリメント/デクリメント
3. ActivityPub API経由でlike/unlike操作を実行
4. 成功時：キャッシュ更新
5. 失敗時：UIをロールバック

### 6-リポスト操作

1. FeedItemのリポストボタンをクリック
2. 楽観的更新：リポスト数をインクリメント/デクリメント
3. ActivityPub API経由でannounce/unannounce操作を実行
4. 成功時：キャッシュ更新
5. 失敗時：UIをロールバック

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

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

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

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

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

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

## メッセージ仕様

| メッセージ種別 | メッセージ内容 | 表示条件 |
|---------------|---------------|----------|
| 情報 | The Feed is the stream of thoughts and bite-sized updates from people you follow in the Social Web. | フィードが空の場合 |
| 情報 | It's looking a little empty right now but once the people you follow start posting, their updates will show up here. | 空フィード時の説明 |
| ボタン | Write your first note | 空フィード時のアクションボタン |

## 例外処理

| 例外条件 | 処理内容 |
|---------|---------|
| API エラー | AppError コンポーネントでエラーコード・ステータスコードを表示 |
| 投稿失敗 | pending投稿をUIから削除、エラートースト表示 |
| ネットワークエラー | ローディング表示のまま、またはエラー画面表示 |

## 備考

- ノート（Note）は短文投稿で、Article（長文記事）とは別のActivityPubオブジェクトタイプ
- フィードはリスト4番目の位置にSuggestedProfilesが挿入される
- 無限スクロールは約75%スクロール位置で次ページ取得開始
- 投稿には楽観的更新（optimistic update）パターンが採用されている
- pending状態の投稿は `isPendingActivity` でチェックされ、視覚的に区別される

---

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

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

### 推奨読解順序

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

ノートフィードはActivityオブジェクトの配列で、各Activityには投稿者（actor）とコンテンツ（object）が含まれる。

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

**読解のコツ**: Noteタイプの投稿は `object.type === 'Note'` で判定される。Articleとの違いを意識する。

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

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

**主要処理フロー**:
1. **11行目**: `useFeedForUser` でフィードデータ取得
2. **14行目**: activitiesの構築（プレースホルダー含む）
3. **16行目**: `useUserDataForUser` でユーザー情報取得
4. **22-29行目**: FeedListコンポーネントへのprops渡し

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

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

**主要処理フロー**:
- **33-61行目**: IntersectionObserverによる無限スクロール
- **74行目**: FeedInputコンポーネントの配置
- **75-108行目**: FeedItemの一覧レンダリング
- **101-103行目**: 4番目の位置にSuggestedProfiles挿入

#### Step 4: 投稿フォームを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | feed-input.tsx | `apps/activitypub/src/views/feed/components/feed-input.tsx` | FeedInputコンポーネント |

**主要処理フロー**:
- クリックでNewNoteModalを開く
- ユーザーアバターと入力欄の表示

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

```
Feed (feed.tsx)
    │
    ├─ useFeedForUser
    │      └─ GET /feed
    │
    ├─ useUserDataForUser
    │      └─ GET /users/index
    │
    └─ FeedList (feed-list.tsx)
           │
           ├─ Layout
           │
           ├─ FeedInput
           │      └─ NewNoteModal
           │             └─ 投稿処理
           │
           ├─ FeedItem[] (一覧)
           │      ├─ APAvatar
           │      ├─ FeedItemStats
           │      │      ├─ いいねボタン
           │      │      ├─ リポストボタン
           │      │      └─ 返信ボタン
           │      │
           │      └─ onClick → navigate('/notes/{postId}')
           │
           ├─ SuggestedProfiles (index=3)
           │      └─ おすすめアカウントリスト
           │
           └─ IntersectionObserver
                  └─ fetchNextPage
```

### データフロー図

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

ページロード ────────▶ useFeedForUser
                            │
                            └─ GET /feed ──────────────▶ Activity[]

ノート投稿 ──────────▶ 楽観的更新
                            │
                            ├─ UI即時反映（pending）
                            │
                            └─ POST /notes ────────────▶ 成功: ID更新
                                                        失敗: UI削除

いいね ─────────────▶ 楽観的更新
                            │
                            ├─ likeCount +/- 1
                            │
                            └─ POST/DELETE /like ──────▶ 成功: キャッシュ更新
                                                        失敗: ロールバック
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| feed.tsx | `apps/activitypub/src/views/feed/feed.tsx` | ソース | Feedメインコンポーネント |
| feed-list.tsx | `apps/activitypub/src/views/feed/components/feed-list.tsx` | ソース | 一覧表示コンポーネント |
| feed-input.tsx | `apps/activitypub/src/views/feed/components/feed-input.tsx` | ソース | 投稿入力コンポーネント |
| suggested-profiles.tsx | `apps/activitypub/src/views/feed/components/suggested-profiles.tsx` | ソース | おすすめプロフィール |
| feed-item.tsx | `apps/activitypub/src/components/feed/feed-item.tsx` | ソース | フィードアイテム |
| new-note-modal.tsx | `apps/activitypub/src/components/modals/new-note-modal.tsx` | ソース | ノート投稿モーダル |
| use-activity-pub-queries.ts | `apps/activitypub/src/hooks/use-activity-pub-queries.ts` | ソース | ActivityPub APIフック |
| routes.tsx | `apps/activitypub/src/routes.tsx` | ソース | ルーティング定義 |
