Conversation
WalkthroughMLS/E2EE 関連のクライアント実装(API/ラッパー/コーデック/ストレージ/フック/テスト/UI)を一括削除し、チャットはプレーンな chat/api.ts ベースのメッセージ API に置き換え。Profile/Chat UI の暗号化表示・初期メッセージ処理と MLS 統合を除去。microblog のユーザーキャッシュはメモリ化。 Changes
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: 作成結果表示
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
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: ハンドル正規化で大文字小文字や末尾スラッシュの差異を吸収
normalizeActorはhttp形式を 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.
📒 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.tsxapp/client/src/components/Profile.tsxapp/client/src/components/chat/api.tsapp/client/src/components/chat/ChatTitleBar.tsxapp/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?)" appapp/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 }しか含まれていませんが、サーバ側ハンドラが新規ルーム作成時にnameやmembers等の追加フィールドを必須とする場合は、これらも含める必要があります。サーバのルータ/ハンドラ定義を確認し、必要に応じて以下のように修正してください。
- 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), });
| { | ||
| id: handle, | ||
| name: handle, | ||
| userName: user.userName, | ||
| domain: getDomain(), | ||
| unreadCount: 0, | ||
| type: "group", | ||
| members: [handle, me], | ||
| }, |
There was a problem hiding this comment.
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.
| { | |
| 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.
There was a problem hiding this comment.
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.tsxのChatRoomListProps定義からshowAdsとonCreateRoomを削除してください。- 呼び出し元で該当 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 のonSelect→onSelectFriend置換について
ChatRoomListPropsのプロパティ名だけを変更しても、以下の箇所に影響が出ます。全ての参照を合わせてリネームが必要です。•
app/client/src/components/chat/ChatRoomList.tsx
- Props 定義内の
onSelect→onSelectFriend- JSX 内の
props.onSelect→props.onSelectFriend呼び出し- コンポーネント呼び出し箇所 (例:
<button onClick={() => props.onSelect(id)}>…</button>)•
app/client/src/components/Chat.tsx
<ChatRoomList onSelect={…} → onSelectFriend={…}- コールバック引数名の一致 (
onSelect⇒onSelectFriend)その他、
rg -nP '\bonSelect\b' appで見つかった以下ファイルは対象外です。
ServerList.tsxのonSelect(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.
📒 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.tsxapp/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)
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
💡 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*\(' appLength of output: 90
Profile.tsx の呼び出し箇所で渡されている引数を確認します。
🏁 Script executed:
#!/bin/bash
# Profile.tsx の該当箇所(240–260行目)を表示
sed -n '240,260p' app/client/src/components/Profile.tsxLength 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.
| 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.
| 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 []; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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"; |
There was a problem hiding this comment.
SwipeTabs の default import はコンパイルエラー(named export を使用)
../ui/SwipeTabs.tsx は export 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.
| 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.
| <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> |
There was a problem hiding this comment.
SwipeTabs の props 仕様不一致(value/onChange/tabs → index/onIndexChange/children)
SwipeTabs は index と onIndexChange を受け取り、子要素を横スワイプで切り替える実装です。現在の 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.
概要
テスト
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.tsdeno 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.tshttps://chatgpt.com/codex/tasks/task_e_68a9008f722883289022a8096af37496
Summary by CodeRabbit
Refactor
Chores