Skip to content

MLS機能削除と平文チャット対応#614

Open
tako0614 wants to merge 2 commits intomasterfrom
codex/remove-e2ee-directory-and-mls-code
Open

MLS機能削除と平文チャット対応#614
tako0614 wants to merge 2 commits intomasterfrom
codex/remove-e2ee-directory-and-mls-code

Conversation

@tako0614
Copy link
Copy Markdown
Owner

@tako0614 tako0614 commented Aug 22, 2025

概要

  • e2ee ディレクトリを削除し MLS 関連処理を排除
  • Chat コンポーネントを平文メッセージ送受信へ簡素化
  • 設定画面やプロフィールなどから暗号化関連機能を除去

テスト

  • deno fmt app/client/src/components/Chat.tsx app/client/src/components/chat/api.ts app/client/src/components/chat/ChatTitleBar.tsx app/client/src/components/Setting/index.tsx app/client/src/components/Profile.tsx app/client/src/components/Application.tsx app/client/src/components/microblog/api.ts
  • deno lint app/client/src/components/Chat.tsx app/client/src/components/chat/api.ts app/client/src/components/chat/ChatTitleBar.tsx app/client/src/components/Setting/index.tsx app/client/src/components/Profile.tsx app/client/src/components/Application.tsx app/client/src/components/microblog/api.ts

https://chatgpt.com/codex/tasks/task_e_68a9008f722883289022a8096af37496

Summary by CodeRabbit

  • Refactor

    • ルーム作成フローを簡素化し、初回メッセージを送らない形に変更。タイトルバーの暗号化表示とチャットのルーム設定オーバーレイを削除。
    • 設定画面からMLS鍵管理セクションを削除(ログアウト時の関連処理も整理)。
    • 複数アカウント向けの自動トップアップ処理を削除。
    • チャット一覧を軽量化し、スワイプ式タブ中心の表示に統合。
  • Chores

    • 暗号化/MLS関連の内部実装と関連テストを撤去。
    • マイクロブログのユーザー情報キャッシュをメモリのみへ変更(セッション間で保持されません)。
    • チャットAPIを見直し、基本操作(検索・作成・閲覧・送信)を安定化。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 22, 2025

Walkthrough

MLS/E2EE 関連のクライアント実装(API/ラッパー/コーデック/ストレージ/フック/テスト/UI)を一括削除し、チャットはプレーンな chat/api.ts ベースのメッセージ API に置き換え。Profile/Chat UI の暗号化表示・初期メッセージ処理と MLS 統合を除去。microblog のユーザーキャッシュはメモリ化。

Changes

Cohort / File(s) Change Summary
E2EE/MLS スタック削除
app/client/src/components/e2ee/api.ts, app/client/src/components/e2ee/mls_wrapper.ts, app/client/src/components/e2ee/storage.ts, app/client/src/components/e2ee/useMLS.ts, app/client/src/components/e2ee/binding.ts, app/client/src/components/e2ee/mls_message.ts, app/client/src/components/e2ee/mls_test.ts
MLS/E2EE の API、ラッパー、ストレージ、フック、メッセージコーデック、バインディング評価、テストを完全削除。多数の型・関数・エクスポートが消失。
チャット設定UI削除
app/client/src/components/chat/ChatSettingsOverlay.tsx
ルーム設定オーバーレイ(メンバー管理・アイコン・MLS 連携)コンポーネントを削除。
チャットAPI新規追加
app/client/src/components/chat/api.ts
プレーンメッセージ向け API を追加(PlainMessage 型、searchRooms、addRoom、fetchMessages、sendMessage)。fetch/post の薄いラッパー実装。
Profile の部屋作成ロジック変更
app/client/src/components/Profile.tsx
addRoom 呼び出しを e2ee/api.tschat/api.ts に切替。ペイロードを単一の room オブジェクトへ変更し、初期“hi”メッセージ送信を削除。
チャットタイトルバーから暗号化表示削除
app/client/src/components/chat/ChatTitleBar.tsx
props から bindingStatus/bindingInfo/ktInfo を除去し、関連表示ブロックを削除。
アプリ/設定の MLS 連動削除
app/client/src/components/Application.tsx, app/client/src/components/Setting/index.tsx
アカウント一括トップアップや MLS 鍵管理 UI/削除ループ、top-up タイマーや useMLS 呼び出しを削除。ログアウト時の MLS クリーンアップを撤去。
チャット関連 UI 軽量化/削除
app/client/src/components/chat/ChatRoomList.tsx, app/client/src/components/chat/FriendRoomList.tsx, app/client/src/components/chat/FriendList.tsx
ChatRoomList を大幅簡素化(selectedRoom → selectedFriend のプロップ名変更等)、FriendRoomList を削除、FriendList の不要な console.log を除去。
microblog キャッシュの永続→メモリ化
app/client/src/components/microblog/api.ts
永続ストレージ依存のキャッシュ処理を削除し、同期的なインメモリキャッシュへ変更(関数シグネチャ変更、accountId 削除)。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as ユーザー
  participant Profile as Profile.tsx
  participant ChatAPI as chat/api.ts
  participant Server as サーバ

  User->>Profile: ルーム作成操作
  Profile->>ChatAPI: addRoom(ownerId, room)
  ChatAPI->>Server: POST /api/ap/rooms { owner, id }
  Server-->>ChatAPI: 200/4xx
  ChatAPI-->>Profile: boolean
  Profile-->>User: 作成結果表示
Loading
sequenceDiagram
  autonumber
  participant ChatUI as チャットUI
  participant ChatAPI as chat/api.ts
  participant Server as サーバ

  ChatUI->>ChatAPI: fetchMessages(friendId)
  ChatAPI->>Server: GET /api/messages/{friendId}
  Server-->>ChatAPI: [PlainMessage...]
  ChatAPI-->>ChatUI: メッセージ配列

  ChatUI->>ChatAPI: sendMessage(from,to,content)
  ChatAPI->>Server: POST /api/messages { from,to:[to],content }
  Server-->>ChatAPI: 200/4xx
  ChatAPI-->>ChatUI: boolean
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Poem

ぴょんと跳ねて鍵は消え
壁の声はそのままに
兎はポケットにメモリだけ
新しい道は軽やかで
送信よ、素直に進め 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/remove-e2ee-directory-and-mls-code

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/client/src/components/Application.tsx (1)

22-33: WebSocket の切断クリーンアップを追加してください

現状、connectWebSocket() はソケットを生成して返すだけで、アンマウント時に切断する仕組み(disconnectWebSocket のような API)も、接続を管理している connectWebSocket 内部からのクリーンアップ関数の返却もありません。そのため、Component のアンマウント時にソケットが残り続けてしまい、リークや二重接続の原因となります。

以下の対応をお願いいたします。

  • app/client/src/utils/ws.ts
    • disconnectWebSocket() 関数(または connectWebSocket() からのクリーンアップ関数返却)を実装& export してください。
      例:
      export function disconnectWebSocket() {
        if (socket) {
          socket.close();
          socket = null;
          handlers.length = 0;
          currentUser = null;
        }
      }
  • app/client/src/components/Application.tsx
    • onMount 内の return コールバックで、disconnectWebSocket() を呼び出すように追加してください。
      例:
      onMount(() => {
        const checkMobile = () => {
          setIsMobile(globalThis.innerWidth <= 768);
        };
      
        checkMobile();
        globalThis.addEventListener("resize", checkMobile);
  • connectWebSocket();
    
  • connectWebSocket();
    
    return () => {
      globalThis.removeEventListener("resize", checkMobile);
    
  •   disconnectWebSocket();
    };
    
    });
    
    

これにより、Component のアンマウント時に必ず WebSocket が正しく切断され、リソースリークや二重接続の問題を防止できます。

🧹 Nitpick comments (13)
app/client/src/components/chat/ChatTitleBar.tsx (3)

99-101: タイトルが空文字になり得るため表示のフォールバックを追加しておきましょう

相手未確定や自身名と一致するケースで titleFor(...) が空文字を返します。そのままだと h2 が空になり UX 的に分かりづらいです。空時は「チャット」などにフォールバックするのが無難です。

-        <h2>{titleFor(props.selectedRoom)}</h2>
+        <h2>{titleFor(props.selectedRoom) || "チャット"}</h2>

82-98: 戻るボタンに aria-label を付与しアクセシビリティ向上

スクリーンリーダー向けに明示的なラベルを付けましょう。

-          <button type="button" class="h-full" onClick={props.onBack}>
+          <button type="button" class="h-full" onClick={props.onBack} aria-label="戻る">

103-127: 設定ボタンのラベルをローカライズしつつ title を付与

aria-label="Chat settings" は英語固定です。日本語 UI に合わせてラベル/ツールチップを日本語化すると一貫性が上がります。

-            aria-label="Chat settings"
+            aria-label="設定"
+            title="設定"
app/client/src/components/Application.tsx (1)

35-41: registerUser 呼び出しの再実行制御(同一値の連続登録抑止)

account() が同一ユーザーのまま複数回トリガーされる可能性がある場合、重複登録を防ぐために直前値と比較して変化時のみ実行するのが堅実です(副作用の最小化)。

   createEffect(() => {
-    const user = account();
+    const user = account();
     // Ensure active user's presence is registered for websocket etc.
     if (user) {
       registerUser(`${user.userName}@${getDomain()}`);
     }
   });

(任意)createMemo 等でハンドルをメモ化し、on トリガーで変更時だけ実行する実装も検討ください。

app/client/src/components/chat/api.ts (3)

11-25: searchRooms: 返却値の形を軽くバリデーション

API 側の仕様変更に強くするため、rooms 配列内の各要素に id: string があるかを確認してから返すと安全です。

-    const data = await res.json();
-    return Array.isArray(data.rooms) ? data.rooms : [];
+    const data = await res.json();
+    if (!data || !Array.isArray(data.rooms)) return [];
+    const rooms = data.rooms.filter((r: unknown) =>
+      !!r && typeof (r as { id?: unknown }).id === "string"
+    ) as { id: string }[];
+    return rooms;

45-59: fetchMessages: 受信データの軽い型ガードと並び順の保証

配列内要素の最低限チェック(id/sender/content/createdAt)を行い、日付順に並べて返すと UI が安定します。

-    const data = await res.json();
-    return Array.isArray(data) ? data as PlainMessage[] : [];
+    const data = await res.json();
+    const arr = Array.isArray(data) ? data : [];
+    const msgs = arr.filter((m: any) =>
+      m && typeof m.id === "string" &&
+      typeof m.sender === "string" &&
+      typeof m.content === "string" &&
+      typeof m.createdAt === "string"
+    ) as PlainMessage[];
+    // createdAt 昇順
+    msgs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
+    return msgs;

補助関数として型ガードを定義する選択肢もあります(ファイル先頭付近に配置):

function isPlainMessage(x: unknown): x is PlainMessage {
  const m = x as any;
  return !!m && typeof m.id === "string" && typeof m.sender === "string" &&
         typeof m.content === "string" && typeof m.createdAt === "string";
}

61-82: sendMessage: キャンセル(AbortSignal)など拡張可能な引数を追加して UI 応答性を向上

リクエスト中にタブ遷移・ルーム切替が起きた場合にキャンセルできると UX が良くなります。後方互換を保ちつつ opts を追加しましょう。

-export const sendMessage = async (
+export const sendMessage = async (
   roomId: string,
   from: string,
   to: string[],
-  content: string,
-): Promise<boolean> => {
+  content: string,
+  opts: RequestInit = {},
+): Promise<boolean> => {
   try {
     const payload = { from, to, content, mediaType: "text/plain" };
     const res = await apiFetch(
       `/api/rooms/${encodeURIComponent(roomId)}/messages`,
       {
         method: "POST",
         headers: { "Content-Type": "application/json" },
-        body: JSON.stringify(payload),
+        body: JSON.stringify(payload),
+        ...opts,
       },
     );
     return res.ok;
app/client/src/components/Profile.tsx (2)

243-263: addRoom 成否の確認とエラーハンドリングを追加

現在は addRoom の戻り値(boolean)を無視して遷移しています。作成失敗時に空のスレへ遷移する恐れがあるため、成功時のみ遷移・失敗時は通知を出すのが安全です。

-    await addRoom(
+    const ok = await addRoom(
       user.id,
       {
         id: handle,
         name: handle,
         userName: user.userName,
         domain: getDomain(),
         unreadCount: 0,
-        type: "group",
+        type: "friend",
         members: [handle, me],
       },
     );
-    setRoom(handle);
-    setApp("chat");
+    if (ok) {
+      setRoom(handle);
+      setApp("chat");
+    } else {
+      console.error("Failed to create room");
+      // TODO: トースト等でユーザー通知
+    }

230-241: ハンドル正規化で大文字小文字や末尾スラッシュの差異を吸収

normalizeActorhttp 形式を ID@host に変換していますが、大小文字や末尾スラッシュでキャッシュキーやルーム ID がブレる可能性があります。toLowerCase() および末尾スラッシュ除去を追加すると取り扱いが安定します。

app/client/src/components/microblog/api.ts (4)

329-337: メモリキャッシュの同期化は妥当。キーの正規化を検討

同期化でオーバーヘッドが減りました。追加で、identifier の大小文字や余分な空白を吸収するキー正規化(例: identifier.trim().toLowerCase())を導入するとキャッシュヒット率が安定します。


339-347: キャッシュ肥大化対策として簡易上限を設定

長時間稼働でキーが増えるとメモリを圧迫します。エントリ上限(例: 200)を超えたら最古のものを削除する簡易 LRU 風の制御を入れると安心です。

 export const setCachedUserInfo = (
   identifier: string,
   userInfo: UserInfo,
 ): void => {
-  userInfoCache.set(identifier, {
+  // 上限を超える場合は最古を 1 件削除
+  const MAX_CACHE_SIZE = 200;
+  if (userInfoCache.size >= MAX_CACHE_SIZE) {
+    const oldestKey = [...userInfoCache.entries()]
+      .sort((a, b) => a[1].timestamp - b[1].timestamp)[0]?.[0];
+    if (oldestKey) userInfoCache.delete(oldestKey);
+  }
+  userInfoCache.set(identifier, {
     userInfo,
     timestamp: Date.now(),
   });
 };

355-371: fetchUserInfo: キャッシュ参照・保存の流れは OK。エラー詳細の返却も検討可

現状は null を返すだけですが、呼び出し側で UI メッセージを出したい場合に備え、エラー理由の文字列化(例: 戻り値を Result にする)を将来検討しても良いです。現段階ではこのままでも十分です。


406-418: 同期化後も残っている Promise.all を削除し、インデックス対応を安全に

setCachedUserInfo は同期関数なので await Promise.all(...) は不要です。また、取得件数と要求件数がズレた場合に備え、境界をチェックした単一ループで fetchedMap への反映も同時に行いましょう。

-      if (response.ok) {
-        const fetchedInfos: UserInfo[] = await response.json();
-
-        await Promise.all(
-          fetchedInfos.map((info, index) =>
-            setCachedUserInfo(uncached[index], info)
-          ),
-        );
-
-        fetchedInfos.forEach((info, index) => {
-          fetchedMap[uncached[index]] = info;
-        });
-      }
+      if (response.ok) {
+        const fetchedInfos: UserInfo[] = await response.json();
+        for (let i = 0; i < fetchedInfos.length && i < uncached.length; i++) {
+          const key = uncached[i];
+          const info = fetchedInfos[i];
+          setCachedUserInfo(key, info);
+          fetchedMap[key] = info;
+        }
+      }

補足: サーバ側が順序保証していない場合は、レスポンスに identifier を含めてもらい、キーにそれを使う方式がさらに堅牢です。

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 579444d and 14ef3e3.

📒 Files selected for processing (14)
  • app/client/src/components/Application.tsx (1 hunks)
  • app/client/src/components/Profile.tsx (2 hunks)
  • app/client/src/components/Setting/index.tsx (0 hunks)
  • app/client/src/components/chat/ChatSettingsOverlay.tsx (0 hunks)
  • app/client/src/components/chat/ChatTitleBar.tsx (1 hunks)
  • app/client/src/components/chat/api.ts (1 hunks)
  • app/client/src/components/e2ee/api.ts (0 hunks)
  • app/client/src/components/e2ee/binding.ts (0 hunks)
  • app/client/src/components/e2ee/mls_message.ts (0 hunks)
  • app/client/src/components/e2ee/mls_test.ts (0 hunks)
  • app/client/src/components/e2ee/mls_wrapper.ts (0 hunks)
  • app/client/src/components/e2ee/storage.ts (0 hunks)
  • app/client/src/components/e2ee/useMLS.ts (0 hunks)
  • app/client/src/components/microblog/api.ts (4 hunks)
💤 Files with no reviewable changes (9)
  • app/client/src/components/e2ee/binding.ts
  • app/client/src/components/e2ee/mls_test.ts
  • app/client/src/components/chat/ChatSettingsOverlay.tsx
  • app/client/src/components/e2ee/useMLS.ts
  • app/client/src/components/Setting/index.tsx
  • app/client/src/components/e2ee/mls_message.ts
  • app/client/src/components/e2ee/storage.ts
  • app/client/src/components/e2ee/mls_wrapper.ts
  • app/client/src/components/e2ee/api.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: TypeScriptの型定義を活用する
エラーハンドリングを適切に実装する

Files:

  • app/client/src/components/Application.tsx
  • app/client/src/components/Profile.tsx
  • app/client/src/components/chat/api.ts
  • app/client/src/components/chat/ChatTitleBar.tsx
  • app/client/src/components/microblog/api.ts
🧠 Learnings (1)
📚 Learning: 2025-08-13T10:14:15.274Z
Learnt from: CR
PR: takoserver/takos#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T10:14:15.274Z
Learning: Applies to app/api/{activity_handlers.ts,utils/activitypub*.ts} : ActivityPubの仕様に準拠したJSON-LD形式を使用する

Applied to files:

  • app/client/src/components/microblog/api.ts
🧬 Code graph analysis (1)
app/client/src/components/microblog/api.ts (1)
app/api/services/user-info.ts (1)
  • UserInfo (6-12)
🔇 Additional comments (4)
app/client/src/components/chat/ChatTitleBar.tsx (1)

100-100: 暗号化表示の削除はコンポーネントの責務整理として妥当です

関連する props/依存(BindingStatus 等)も除去されており、不要な分岐がなくなって読みやすくなりました。

app/client/src/components/Application.tsx (2)

1-1: MLS 連動の一掃に伴う import 整理は妥当です

createEffect/onMount/Show のみのシンプル構成にできており、不要なクリーンアップタイマーが消えて読みやすくなっています。


1-90: E2EE/MLS 残骸の参照は検出されませんでした

指定のパターン(e2ee/, useMLS, mls_ など)でリポジトリ全体を検索しましたが、いずれのキーワードも検出されませんでした。これにより、MLS 関連のコードは全て削除済みと判断できます。

───
今後の漏れ検知を自動化する場合は、以下のスクリプトを CI に組み込むと安全です。

#!/bin/bash
set -euo pipefail
# MLS/E2EE 用語の残存チェック
rg -nP "(e2ee/|useMLS|mls_|topUpKeyPackages|BindingStatus|ChatSettingsOverlay)" -g '!**/node_modules/**'
# 旧 API パス(/api/mls, /api/e2ee, /api/key 等)の残存チェック
rg -nP "/api/(mls|e2ee|keys?)" app
app/client/src/components/chat/api.ts (1)

27-43: addRoom のエンドポイント/リクエストボディを要確認

  • app/client/src/components/chat/api.ts の addRoom 関数は、他のチャット API(getRooms や sendMessage)で使っている /api/rooms ではなく、/api/ap/rooms を呼び出しています。サーバ実装が /api/rooms に統一されている場合、404 エラーになる可能性があります。
  • また現在の body には { owner, id } しか含まれていませんが、サーバ側ハンドラが新規ルーム作成時に namemembers 等の追加フィールドを必須とする場合は、これらも含める必要があります。

サーバのルータ/ハンドラ定義を確認し、必要に応じて以下のように修正してください。

- const body = { owner: id, id: room.id };
- const res = await apiFetch(`/api/ap/rooms`, {
+ const body = { owner: id, id: room.id, name: room.name, members: room.members };
+ const res = await apiFetch(`/api/rooms`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });

Comment on lines +251 to +259
{
id: handle,
name: handle,
userName: user.userName,
domain: getDomain(),
unreadCount: 0,
type: "group",
members: [handle, me],
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

1対1開始のルーム type"group" になっている点を修正(タイトル確定ロジックに影響)

プロフィール画面からの「チャット」は実質 1:1 です。ChatTitleBar.tsx 側は isFriendRoom(room) のときに相手ハンドル推定を行いますが、ここで type: "group" を渡すとその分岐に入らず、タイトルの自動確定が効きません。"friend" にするのが自然です。

-        type: "group",
+        type: "friend",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
id: handle,
name: handle,
userName: user.userName,
domain: getDomain(),
unreadCount: 0,
type: "group",
members: [handle, me],
},
{
id: handle,
name: handle,
userName: user.userName,
domain: getDomain(),
unreadCount: 0,
type: "friend",
members: [handle, me],
},
🤖 Prompt for AI Agents
In app/client/src/components/Profile.tsx around lines 251 to 259, the one-to-one
room object is being created with type: "group", which prevents ChatTitleBar.tsx
from recognizing it as a friend room and auto-resolving the title; change the
room type value to "friend" (i.e., set type: "friend") so isFriendRoom(room)
returns true and the title-determination logic runs normally.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
app/client/src/components/chat/api.ts (2)

4-9: PlainMessage.createdAt を ISO 8601 と明示 or Date へ正規化

createdAt が単なる string だとフォーマットの取り決めが型に表現されず、UI 側のパース落ちやタイムゾーン解釈ズレが起きやすいです。最小修正は ISO 8601 であることをブランド化して表現するか、取得時に Date へ正規化してください。

適用例(型の明示+最小変更):

 export interface PlainMessage {
   id: string;
   sender: string;
   content: string;
-  createdAt: string;
+  createdAt: ISODateTimeString;
 }

追加(ファイル先頭付近に定義):

// ISO 8601 文字列を表すブランド型
export type ISODateTimeString = string & { __brand: "ISODateTimeString" };

11-25: searchRooms: レスポンス型の明示と JSON 以外/204 対応

res.ok のみで分岐し、その後に無条件で res.json() を呼ぶと 204 No Content や text/html エラー応答で例外→握り潰しになり、原因が追いづらくなります。content-type を確認し、想定スキーマを型で縛ると堅牢になります。

 export const searchRooms = async (
   owner: string,
 ): Promise<{ id: string }[]> => {
   try {
-    const params = new URLSearchParams();
+    if (!owner) return [];
+    const params = new URLSearchParams();
     params.set("owner", owner);
     const res = await apiFetch(`/api/rooms?${params.toString()}`);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return Array.isArray(data.rooms) ? data.rooms : [];
+    if (!res.ok) return [];
+    if (res.status === 204) return [];
+    const ct = res.headers.get("content-type") ?? "";
+    if (!ct.includes("application/json")) return [];
+    const data: unknown = await res.json();
+    const rooms = (data as { rooms?: unknown }).rooms;
+    return Array.isArray(rooms)
+      ? rooms
+          .filter((r) => r && typeof (r as any).id === "string")
+          .map((r) => ({ id: (r as any).id as string }))
+      : [];
   } catch (err) {
     console.error("Error searching rooms:", err);
     return [];
   }
 };

(必要なら)SearchRoomsResponse 型を別途定義しても OK です。

app/client/src/components/chat/ChatRoomList.tsx (2)

18-21: 🧹 ChatRoomListProps から未使用 props の削除を推奨します

  • app/client/src/components/chat/ChatRoomList.tsxChatRoomListProps 定義から showAdsonCreateRoom を削除してください。
  • 呼び出し元で該当 props を渡している箇所が見当たらないため(rg 検索で参照なしを確認済み)、削除しても挙動に影響はありません。

軽微な改修例:

 interface ChatRoomListProps {
   rooms: Room[];
   selectedFriend: string | null;
   onSelect: (id: string) => void;
-  showAds: boolean;
-  onCreateRoom: () => void;
   segment: "all" | "people" | "groups";
   onSegmentChange: (seg: "all" | "people" | "groups") => void;
 }

6-14: ChatRoomListProps の onSelectonSelectFriend 置換について

ChatRoomListProps のプロパティ名だけを変更しても、以下の箇所に影響が出ます。全ての参照を合わせてリネームが必要です。

app/client/src/components/chat/ChatRoomList.tsx

  • Props 定義内の onSelectonSelectFriend
  • JSX 内の props.onSelectprops.onSelectFriend 呼び出し
  • コンポーネント呼び出し箇所 (例: <button onClick={() => props.onSelect(id)}>…</button>)

app/client/src/components/Chat.tsx

  • <ChatRoomList onSelect={…} → onSelectFriend={…}
  • コールバック引数名の一致 (onSelectonSelectFriend)

その他、rg -nP '\bonSelect\b' app で見つかった以下ファイルは対象外です。

  • ServerList.tsxonSelect(url, password) は別用途のためリネーム不要

差分例:

--- a/app/client/src/components/chat/ChatRoomList.tsx
+++ b/app/client/src/components/chat/ChatRoomList.tsx
@@ -6,7 +6,7 @@ interface ChatRoomListProps {
   rooms: Room[];
   selectedFriend: string | null;
-  onSelect: (id: string) => void;
+  onSelectFriend: (id: string) => void;
   showAds: boolean;
   onCreateRoom: () => void;
   segment: "all" | "people" | "groups";
@@ -33,7 +33,7 @@ export const ChatRoomList: React.FC<ChatRoomListProps> = props => {
       {props.rooms.map(r => (
         <li key={r.id} onClick={() => 
-          props.onSelect(r.id)
+          props.onSelectFriend(r.id)
         }>
           {r.name}
         </li>

呼び出し側 (Chat.tsx) の修正例:

--- a/app/client/src/components/Chat.tsx
+++ b/app/client/src/components/Chat.tsx
@@ -100,7 +100,7 @@ export const Chat: React.FC = () => {
     …
     <ChatRoomList
       rooms={rooms}
-      onSelect={id => setSelectedFriend(id)}
+      onSelectFriend={id => setSelectedFriend(id)}
       selectedFriend={selectedFriend}
       showAds={showAds}
       onCreateRoom={handleCreateRoom}

これらを合わせて実施すると、命名の一貫性が保たれます。

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 14ef3e3 and d64f60e.

📒 Files selected for processing (4)
  • app/client/src/components/chat/ChatRoomList.tsx (1 hunks)
  • app/client/src/components/chat/FriendList.tsx (1 hunks)
  • app/client/src/components/chat/FriendRoomList.tsx (0 hunks)
  • app/client/src/components/chat/api.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • app/client/src/components/chat/FriendRoomList.tsx
✅ Files skipped from review due to trivial changes (1)
  • app/client/src/components/chat/FriendList.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: TypeScriptの型定義を活用する
エラーハンドリングを適切に実装する

Files:

  • app/client/src/components/chat/ChatRoomList.tsx
  • app/client/src/components/chat/api.ts
🧬 Code graph analysis (2)
app/client/src/components/chat/ChatRoomList.tsx (3)
app/client/src/components/chat/types.ts (1)
  • Room (21-39)
app/client/src/components/ui/SwipeTabs.tsx (1)
  • SwipeTabs (17-190)
app/client/src/components/chat/FriendList.tsx (1)
  • FriendList (25-279)
app/client/src/components/chat/api.ts (2)
app/client/src/utils/config.ts (1)
  • apiFetch (31-67)
app/client/src/components/chat/types.ts (1)
  • Room (21-39)

Comment on lines +27 to +43
export const addRoom = async (
id: string,
room: Room,
): Promise<boolean> => {
try {
const body = { owner: id, id: room.id };
const res = await apiFetch(`/api/ap/rooms`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return res.ok;
} catch (err) {
console.error("Error adding room:", err);
return false;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

addRoom: 受け取る引数と送信ペイロードが不整合(Room 全体を渡すが id のみ使用)

呼び出し側から Room 全体を受け取っていますが、送信するのは room.id のみです。誤用の芽を摘むため、引数を roomId: string に絞るか、最低限の入力バリデーションを入れてください。

以下は影響最小の入力バリデーション案です(署名は据え置き):

 export const addRoom = async (
   id: string,
   room: Room,
 ): Promise<boolean> => {
   try {
+    if (!id || !room?.id) return false;
     const body = { owner: id, id: room.id };
     const res = await apiFetch(`/api/ap/rooms`, {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(body),
     });
     return res.ok;
   } catch (err) {
     console.error("Error adding room:", err);
     return false;
   }
 };

署名変更を検討する場合は影響範囲を確認してください:


🏁 Script executed:

#!/bin/bash
# addRoom 呼び出し箇所を確認(引数の渡し方を点検)
rg -nP '\baddRoom\s*\(' app

Length of output: 90


Profile.tsx の呼び出し箇所で渡されている引数を確認します。


🏁 Script executed:

#!/bin/bash
# Profile.tsx の該当箇所(240–260行目)を表示
sed -n '240,260p' app/client/src/components/Profile.tsx

Length of output: 532


addRoom の第二引数を Room から roomId: string に変更

呼び出し側では Room 全体を渡していますが、実際に API に送信しているのは room.id のみです。型定義と実装を揃えることで、余分なデータを渡すミスや誤用を防ぎましょう。

対応箇所

  • app/client/src/components/chat/api.ts

    • 関数署名の変更
    • body の生成はそのまま
  • app/client/src/components/Profile.tsx

    • フルオブジェクトを渡している呼び出しを roomId のみ渡す形に変更

想定する差分例:

--- a/app/client/src/components/chat/api.ts
+++ b/app/client/src/components/chat/api.ts
@@
-export const addRoom = async (
-  id: string,
-  room: Room,
-): Promise<boolean> => {
+export const addRoom = async (
+  ownerId: string,
+  roomId: string,
+): Promise<boolean> => {
   try {
-    const body = { owner: id, id: room.id };
+    const body = { owner: ownerId, id: roomId };
     const res = await apiFetch(`/api/ap/rooms`, {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(body),
     });
     return res.ok;
   } catch (err) {
     console.error("Error adding room:", err);
     return false;
   }
 };
--- a/app/client/src/components/Profile.tsx
+++ b/app/client/src/components/Profile.tsx
@@ const openChat = async () => {
-    await addRoom(
-      user.id,
-      {
-        id: handle,
-        name: handle,
-        userName: user.userName,
-        domain: getDomain(),
-        unreadCount: 0,
-        type: "group",
-        members: [handle, me],
-      },
-    );
+    await addRoom(
+      user.id,
+      handle,
+    );

以上の変更により、型と実装をシンプルに一致させ、不要なデータの受け渡しを防止できます。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const addRoom = async (
id: string,
room: Room,
): Promise<boolean> => {
try {
const body = { owner: id, id: room.id };
const res = await apiFetch(`/api/ap/rooms`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return res.ok;
} catch (err) {
console.error("Error adding room:", err);
return false;
}
};
export const addRoom = async (
ownerId: string,
roomId: string,
): Promise<boolean> => {
try {
const body = { owner: ownerId, id: roomId };
const res = await apiFetch(`/api/ap/rooms`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return res.ok;
} catch (err) {
console.error("Error adding room:", err);
return false;
}
};
🤖 Prompt for AI Agents
In app/client/src/components/chat/api.ts around lines 27 to 43, the addRoom
function currently accepts a full Room object as its second parameter but only
sends room.id to the API; change the function signature to accept roomId: string
instead of room: Room, update the body to use { owner: id, id: roomId } (keeping
the same JSON structure), and adjust any internal references accordingly; also
update callers (notably app/client/src/components/Profile.tsx) to pass the
roomId string rather than the full Room object.

Comment on lines +45 to +59
export const fetchMessages = async (
friendId: string,
): Promise<PlainMessage[]> => {
try {
const res = await apiFetch(
`/api/messages/${encodeURIComponent(friendId)}`,
);
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data as PlainMessage[] : [];
} catch (err) {
console.error("Error fetching messages:", err);
return [];
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

fetchMessages: 204/非JSON応答/型ガード無しでの配列期待が脆い

サーバーが 204 を返す、あるいは JSON 以外のエラー応答を返すケースで例外→握り潰しになります。さらに Array.isArray(data) だけでは要素型の検証が不十分です。軽量な型ガードを入れて安全に扱ってください。

 export const fetchMessages = async (
   friendId: string,
 ): Promise<PlainMessage[]> => {
   try {
     const res = await apiFetch(
       `/api/messages/${encodeURIComponent(friendId)}`,
     );
-    if (!res.ok) return [];
-    const data = await res.json();
-    return Array.isArray(data) ? data as PlainMessage[] : [];
+    if (!res.ok) return [];
+    if (res.status === 204) return [];
+    const ct = res.headers.get("content-type") ?? "";
+    if (!ct.includes("application/json")) return [];
+    const data: unknown = await res.json();
+    return isPlainMessageArray(data) ? data : [];
   } catch (err) {
     console.error("Error fetching messages:", err);
     return [];
   }
 };

補助の型ガード(ファイル内のどこか上位に追加):

function isPlainMessageArray(v: unknown): v is PlainMessage[] {
  return Array.isArray(v) && v.every((m) =>
    m &&
    typeof (m as any).id === "string" &&
    typeof (m as any).sender === "string" &&
    typeof (m as any).content === "string" &&
    typeof (m as any).createdAt === "string"
  );
}
🤖 Prompt for AI Agents
In app/client/src/components/chat/api.ts around lines 45 to 59, fetchMessages
currently swallows non-JSON/204 responses and only uses Array.isArray for
validation; add an upfront check for res.status === 204 (return []), handle
non-JSON responses by checking Content-Type or by trying res.text() when res.ok
but not json, and use a lightweight type guard function (add isPlainMessageArray
above in the file) to validate each item before casting; if the guard fails, log
a warning and return [] instead of blindly casting, and keep the existing
try/catch for network errors.

Comment on lines +61 to +81
export const sendMessage = async (
from: string,
to: string,
content: string,
): Promise<boolean> => {
try {
const payload = { from, to: [to], content, mediaType: "text/plain" };
const res = await apiFetch(
`/api/messages`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
);
return res.ok;
} catch (err) {
console.error("Error sending message:", err);
return false;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

sendMessage: 空文字/空白のみの送信を抑止し、入力を早期バリデーション

UX/サーバー負荷の観点から空メッセージの送信は抑止したいです。trim() の上で早期 return を入れてください。

 export const sendMessage = async (
   from: string,
   to: string,
   content: string,
 ): Promise<boolean> => {
   try {
-    const payload = { from, to: [to], content, mediaType: "text/plain" };
+    const text = content?.trim() ?? "";
+    if (!from || !to || !text) return false;
+    const payload = { from, to: [to], content: text, mediaType: "text/plain" };
     const res = await apiFetch(
       `/api/messages`,
       {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify(payload),
       },
     );
     return res.ok;
   } catch (err) {
     console.error("Error sending message:", err);
     return false;
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const sendMessage = async (
from: string,
to: string,
content: string,
): Promise<boolean> => {
try {
const payload = { from, to: [to], content, mediaType: "text/plain" };
const res = await apiFetch(
`/api/messages`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
);
return res.ok;
} catch (err) {
console.error("Error sending message:", err);
return false;
}
};
export const sendMessage = async (
from: string,
to: string,
content: string,
): Promise<boolean> => {
try {
// Prevent sending empty or whitespace-only messages
const text = content?.trim() ?? "";
if (!from || !to || !text) return false;
const payload = { from, to: [to], content: text, mediaType: "text/plain" };
const res = await apiFetch(
`/api/messages`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
);
return res.ok;
} catch (err) {
console.error("Error sending message:", err);
return false;
}
};
🤖 Prompt for AI Agents
In app/client/src/components/chat/api.ts around lines 61 to 81, the sendMessage
function currently allows sending empty or whitespace-only messages; add an
early validation: trim the content and if the result is an empty string, return
false immediately (do not call apiFetch), ensuring no network request is made
for empty input and preserving the existing boolean return convention.

import { Button, EmptyState, Input, Skeleton } from "../ui/index.ts";
// ローディング表示の点滅を抑えるための簡易ディレイ表示フック
// コンポーネント配下(createRoot/render配下)でのみ使うこと
import SwipeTabs from "../ui/SwipeTabs.tsx";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SwipeTabs の default import はコンパイルエラー(named export を使用)

../ui/SwipeTabs.tsxexport function SwipeTabs(...) の named export です。default import は解決できずビルド失敗します。named import に修正してください。

-import SwipeTabs from "../ui/SwipeTabs.tsx";
+import { SwipeTabs } from "../ui/SwipeTabs.tsx";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import SwipeTabs from "../ui/SwipeTabs.tsx";
import { SwipeTabs } from "../ui/SwipeTabs.tsx";
🤖 Prompt for AI Agents
In app/client/src/components/chat/ChatRoomList.tsx around line 4, the file
currently uses a default import for SwipeTabs which causes a compile error
because SwipeTabs is exported as a named export; change the import to a named
import (e.g. import { SwipeTabs } from "../ui/SwipeTabs.tsx") so it matches the
export and the build succeeds.

Comment on lines +23 to +47
<div class="h-full flex flex-col">
<SwipeTabs
index={["all", "people", "groups"].indexOf(props.segment)}
onIndexChange={(i) =>
changeSeg((["all", "people", "groups"] as const)[i] ?? "all")}
>
{/* すべて */}
<div class="my-[10px] overflow-y-auto overflow-x-hidden w-full pb-14 scrollbar">
<ul
id="panel-all"
role="tabpanel"
aria-labelledby="tab-all"
class="w-full h-[calc(100vh-160px)] pb-[70px] scrollbar"
>
<Show when={showAllSkeleton()}>
<li class="px-2 py-2">
<RoomListSkeleton />
</li>
</Show>
<Show
when={!showAllSkeleton() &&
getFilteredRoomsFor("all").length === 0}
>
<li class="px-2 py-2">
<EmptyState
title="トークはありません"
description="新しいトークを作成して会話を始めましょう。"
/>
</li>
</Show>
<For each={getFilteredRoomsFor("all")}>
{(room) => (
<li
class={`flex items-center cursor-pointer h-16 rounded-lg mb-2 w-full ${
props.selectedRoom === room.id
? "bg-[#4a4a4a]"
: "hover:bg-[#3c3c3c]"
}`}
onClick={() => props.onSelect(room.id)}
>
<div class="flex items-center w-full">
<span class="relative w-[40px] h-[40px] flex items-center justify-center">
{isUrl(room.avatar) ||
(typeof room.avatar === "string" &&
room.avatar.startsWith("data:image/"))
? (
<img
src={room.avatar}
alt="avatar"
class="w-[40px] h-[40px] object-cover rounded-full"
/>
)
: (
<span
class={`w-[40px] h-[40px] flex items-center justify-center rounded-full text-white text-[20px] ${
room.type === "memo"
? "bg-green-600"
: "bg-[#444]"
}`}
>
{room.avatar ||
displayNameFor(room).charAt(0).toUpperCase()}
</span>
)}
</span>
<span class="pl-[10px] flex flex-col justify-center min-w-0 w-full">
<span class="text-[14px] text-white flex justify-between items-center w-full whitespace-nowrap overflow-hidden text-ellipsis">
<span class="font-bold flex-1">
{displayNameFor(room)}
</span>
<span
class="text-[10px] text-gray-500 ml-1 whitespace-nowrap"
style="text-align:right;"
>
{room.lastMessageTime
? room.lastMessageTime.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})
: ""}
</span>
</span>
<span class="text-[12px] text-[#aaaaaa] font-normal flex justify-between items-center">
<p class="truncate">{room.lastMessage}</p>
</span>
</span>
</div>
</li>
)}
</For>
</ul>
</div>

{/* 友だち */}
<div class="my-[10px] overflow-y-auto overflow-x-hidden w-full pb-14 scrollbar">
<Show when={selectedFriend()}>
<FriendRoomList
rooms={props.rooms}
friendId={selectedFriend()!}
friendName={getFriendName(selectedFriend()!)}
selectedRoom={props.selectedRoom}
onSelectRoom={props.onSelect}
onBack={() => setSelectedFriend(null)}
onCreateRoom={() => props.onCreateFriendRoom?.(selectedFriend()!)}
/>
</Show>
<Show when={!selectedFriend()}>
<FriendList
rooms={props.rooms}
selectedFriend={selectedFriend()}
query={query()}
showSearch={false}
onSelectFriend={(id) => setSelectedFriend(id)}
/>
</Show>
</div>

{/* グループ */}
<div class="my-[10px] overflow-y-auto overflow-x-hidden w-full pb-14 scrollbar">
<ul
id="panel-groups"
role="tabpanel"
aria-labelledby="tab-groups"
class="w-full h-[calc(100vh-160px)] pb-[70px] scrollbar"
>
<Show when={showGroupSkeleton()}>
<li class="px-2 py-2">
<RoomListSkeleton />
</li>
</Show>
<Show
when={!showGroupSkeleton() &&
getFilteredRoomsFor("groups").length === 0}
>
<li class="px-2 py-2">
<EmptyState
title="グループはまだありません"
description="『グループ作成』から始めましょう。"
/>
</li>
</Show>
<For each={getFilteredRoomsFor("groups")}>
{(room) => (
<li
class={`flex items-center cursor-pointer h-16 rounded-lg mb-2 w-full ${
props.selectedRoom === room.id
? "bg-[#4a4a4a]"
: "hover:bg-[#3c3c3c]"
}`}
onClick={() => props.onSelect(room.id)}
>
<div class="flex items-center w-full">
<span class="relative w-[40px] h-[40px] flex items-center justify-center">
{isUrl(room.avatar) ||
(typeof room.avatar === "string" &&
room.avatar.startsWith("data:image/"))
? (
<img
src={room.avatar}
alt="avatar"
class="w-[40px] h-[40px] object-cover rounded-full"
/>
)
: (
<span
class={`w-[40px] h-[40px] flex items-center justify-center rounded-full text-white text-[20px] ${
room.type === "memo"
? "bg-green-600"
: "bg-[#444]"
}`}
>
{room.avatar ||
displayNameFor(room).charAt(0).toUpperCase()}
</span>
)}
</span>
<span class="pl-[10px] flex flex-col justify-center min-w-0 w-full">
<span class="text-[14px] text-white flex justify-between items-center w-full whitespace-nowrap overflow-hidden text-ellipsis">
<span class="font-bold flex-1">
{displayNameFor(room)}
</span>
<span
class="text-[10px] text-gray-500 ml-1 whitespace-nowrap"
style="text-align:right;"
>
{room.lastMessageTime
? room.lastMessageTime.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})
: ""}
</span>
</span>
<span class="text-[12px] text-[#aaaaaa] font-normal flex justify-between items-center">
<p class="truncate">{room.lastMessage}</p>
</span>
</span>
</div>
</li>
)}
</For>
</ul>
value={props.segment}
onChange={props.onSegmentChange}
tabs={[
{ value: "all", label: "すべて" },
{ value: "people", label: "友だち" },
{ value: "groups", label: "グループ" },
]}
/>
<Show when={props.segment !== "groups"}>
<FriendList
rooms={props.rooms}
onSelectFriend={props.onSelect}
selectedFriend={props.selectedFriend ?? undefined}
query={query()}
onQueryChange={setQuery}
showSearch
/>
</Show>
<Show when={props.segment === "groups"}>
<div class="p-4 text-center text-gray-400">
グループ機能は未実装です
</div>
</SwipeTabs>
</Show>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SwipeTabs の props 仕様不一致(value/onChange/tabs → index/onIndexChange/children)

SwipeTabsindexonIndexChange を受け取り、子要素を横スワイプで切り替える実装です。現在の value/onChange/tabs は未対応プロップで機能しません。子要素(3 ペイン)を渡す形に改めてください。

-    <div class="h-full flex flex-col">
-      <SwipeTabs
-        value={props.segment}
-        onChange={props.onSegmentChange}
-        tabs={[
-          { value: "all", label: "すべて" },
-          { value: "people", label: "友だち" },
-          { value: "groups", label: "グループ" },
-        ]}
-      />
-      <Show when={props.segment !== "groups"}>
-        <FriendList
-          rooms={props.rooms}
-          onSelectFriend={props.onSelect}
-          selectedFriend={props.selectedFriend ?? undefined}
-          query={query()}
-          onQueryChange={setQuery}
-          showSearch
-        />
-      </Show>
-      <Show when={props.segment === "groups"}>
-        <div class="p-4 text-center text-gray-400">
-          グループ機能は未実装です
-        </div>
-      </Show>
-    </div>
+    <div class="h-full flex flex-col">
+      <SwipeTabs
+        index={() => (props.segment === "all" ? 0 : props.segment === "people" ? 1 : 2)}
+        onIndexChange={(i) => props.onSegmentChange(i === 0 ? "all" : i === 1 ? "people" : "groups")}
+      >
+        {/* all */}
+        <div class="h-full flex flex-col">
+          <FriendList
+            rooms={props.rooms}
+            onSelectFriend={props.onSelect}
+            selectedFriend={props.selectedFriend ?? undefined}
+            query={query()}
+            onQueryChange={setQuery}
+            showSearch
+          />
+        </div>
+        {/* people */}
+        <div class="h-full flex flex-col">
+          <FriendList
+            rooms={props.rooms}
+            onSelectFriend={props.onSelect}
+            selectedFriend={props.selectedFriend ?? undefined}
+            query={query()}
+            onQueryChange={setQuery}
+            showSearch
+          />
+        </div>
+        {/* groups */}
+        <div class="p-4 text-center text-gray-400">
+          グループ機能は未実装です
+        </div>
+      </SwipeTabs>
+    </div>

補足: ラベル付きタブヘッダが必要なら、別途ヘッダ UI を用意するか、SwipeTabs 自体にヘッダ描画を追加する改修が必要です(現状の実装はトラックのみ)。

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant