|
7 | 7 | const level = $derived(page.url.searchParams.get("level") || "system"); |
8 | 8 | const bankId = $derived(page.url.searchParams.get("bank_id") || ""); |
9 | 9 |
|
| 10 | + const addParticipantLink = $derived( |
| 11 | + chatRoomId |
| 12 | + ? `/chat-rooms/${encodeURIComponent(chatRoomId)}/add-participant?level=${encodeURIComponent(level)}${bankId ? `&bank_id=${encodeURIComponent(bankId)}` : ""}` |
| 13 | + : "", |
| 14 | + ); |
| 15 | +
|
10 | 16 | let room = $state<OBPChatRoom | null>(null); |
11 | 17 | let participants = $state<OBPChatRoomParticipant[]>([]); |
12 | 18 | let loading = $state(false); |
13 | 19 | let error = $state<string | null>(null); |
14 | 20 | let successMessage = $state<string | null>(null); |
15 | 21 |
|
16 | | - let showAddForm = $state(false); |
17 | | - let addUserId = $state(""); |
18 | | - let addPermissions = $state("can_send_message"); |
19 | | -
|
20 | 22 | async function fetchRoom() { |
21 | 23 | if (!chatRoomId) return; |
22 | 24 | try { |
|
25 | 27 | `/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}${bankParam}`, |
26 | 28 | ); |
27 | 29 | if (!res.ok) { |
28 | | - const data = await res.json().catch(() => ({})); |
29 | | - throw new Error(data.error || "Failed to fetch chat room"); |
| 30 | + const data = await res.json(); |
| 31 | + if (typeof data.message !== "string") { |
| 32 | + throw new Error( |
| 33 | + `OBP error response missing 'message' field (HTTP ${res.status})`, |
| 34 | + ); |
| 35 | + } |
| 36 | + throw new Error(data.message); |
30 | 37 | } |
31 | 38 | room = await res.json(); |
32 | 39 | } catch (err) { |
33 | | - error = err instanceof Error ? err.message : "Failed to fetch chat room"; |
| 40 | + error = err instanceof Error ? err.message : String(err); |
34 | 41 | room = null; |
35 | 42 | } |
36 | 43 | } |
|
45 | 52 | `/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`, |
46 | 53 | ); |
47 | 54 | if (!res.ok) { |
48 | | - const data = await res.json().catch(() => ({})); |
49 | | - throw new Error(data.error || "Failed to fetch participants"); |
| 55 | + const data = await res.json(); |
| 56 | + if (typeof data.message !== "string") { |
| 57 | + throw new Error( |
| 58 | + `OBP error response missing 'message' field (HTTP ${res.status})`, |
| 59 | + ); |
| 60 | + } |
| 61 | + throw new Error(data.message); |
50 | 62 | } |
51 | 63 | const data = await res.json(); |
52 | | - participants = data.participants || []; |
| 64 | + participants = data.participants; |
53 | 65 | } catch (err) { |
54 | | - error = err instanceof Error ? err.message : "Failed to fetch participants"; |
| 66 | + error = err instanceof Error ? err.message : String(err); |
55 | 67 | participants = []; |
56 | 68 | } finally { |
57 | 69 | loading = false; |
58 | 70 | } |
59 | 71 | } |
60 | 72 |
|
61 | | - async function addParticipant() { |
62 | | - if (!addUserId.trim() || !chatRoomId) return; |
63 | | - error = null; |
64 | | - successMessage = null; |
65 | | - try { |
66 | | - const bankParam = bankId ? `&bank_id=${encodeURIComponent(bankId)}` : ""; |
67 | | - const res = await trackedFetch( |
68 | | - `/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`, |
69 | | - { |
70 | | - method: "POST", |
71 | | - headers: { "Content-Type": "application/json" }, |
72 | | - body: JSON.stringify({ |
73 | | - user_id: addUserId, |
74 | | - permissions: addPermissions.split(",").map((p) => p.trim()).filter(Boolean), |
75 | | - }), |
76 | | - }, |
77 | | - ); |
78 | | - if (!res.ok) { |
79 | | - const data = await res.json().catch(() => ({})); |
80 | | - throw new Error(data.error || "Failed to add participant"); |
81 | | - } |
82 | | - successMessage = "Participant added."; |
83 | | - addUserId = ""; |
84 | | - showAddForm = false; |
85 | | - fetchParticipants(); |
86 | | - } catch (err) { |
87 | | - error = err instanceof Error ? err.message : "Failed to add participant"; |
88 | | - } |
89 | | - } |
90 | | -
|
91 | | - async function refreshJoiningKey() { |
92 | | - if (!chatRoomId) return; |
93 | | - error = null; |
94 | | - successMessage = null; |
95 | | - try { |
96 | | - const endpoint = |
97 | | - level === "bank" && bankId |
98 | | - ? `/proxy/obp/v6.0.0/banks/${encodeURIComponent(bankId)}/chat-rooms/${encodeURIComponent(chatRoomId)}/joining-key` |
99 | | - : `/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/joining-key`; |
100 | | - const res = await trackedFetch(endpoint, { method: "PUT" }); |
101 | | - if (!res.ok) { |
102 | | - const data = await res.json().catch(() => ({})); |
103 | | - throw new Error(data.error || "Failed to refresh joining key"); |
104 | | - } |
105 | | - successMessage = "Joining key refreshed."; |
106 | | - fetchRoom(); |
107 | | - } catch (err) { |
108 | | - error = err instanceof Error ? err.message : "Failed to refresh joining key"; |
109 | | - } |
110 | | - } |
111 | | -
|
112 | | - async function copyJoiningKey() { |
113 | | - if (!room?.joining_key) return; |
114 | | - try { |
115 | | - await navigator.clipboard.writeText(room.joining_key); |
116 | | - successMessage = "Joining key copied."; |
117 | | - } catch { |
118 | | - error = "Could not copy to clipboard."; |
119 | | - } |
120 | | - } |
121 | | -
|
122 | 73 | async function removeParticipant(userId: string) { |
123 | 74 | if (!chatRoomId) return; |
124 | 75 | error = null; |
|
130 | 81 | { method: "DELETE" }, |
131 | 82 | ); |
132 | 83 | if (!res.ok) { |
133 | | - const data = await res.json().catch(() => ({})); |
134 | | - throw new Error(data.error || "Failed to remove participant"); |
| 84 | + const data = await res.json(); |
| 85 | + if (typeof data.message !== "string") { |
| 86 | + throw new Error( |
| 87 | + `OBP error response missing 'message' field (HTTP ${res.status})`, |
| 88 | + ); |
| 89 | + } |
| 90 | + throw new Error(data.message); |
135 | 91 | } |
136 | 92 | successMessage = "Participant removed."; |
137 | 93 | fetchParticipants(); |
138 | 94 | } catch (err) { |
139 | | - error = err instanceof Error ? err.message : "Failed to remove participant"; |
| 95 | + error = err instanceof Error ? err.message : String(err); |
140 | 96 | } |
141 | 97 | } |
142 | 98 |
|
|
171 | 127 | </div> |
172 | 128 |
|
173 | 129 | <div class="flex items-center justify-between mb-4"> |
174 | | - <div> |
175 | | - <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100"> |
176 | | - Chat Room Participants |
177 | | - </h1> |
178 | | - <p class="text-sm text-gray-600 dark:text-gray-400 font-mono mt-1">{chatRoomId}</p> |
179 | | - <p class="text-xs text-gray-500 dark:text-gray-500 mt-0.5"> |
180 | | - Level: {level}{bankId ? ` | Bank: ${bankId}` : ""} |
181 | | - </p> |
182 | | - </div> |
183 | | - <div class="flex items-center gap-3"> |
| 130 | + <p class="text-sm text-gray-600 dark:text-gray-400"> |
| 131 | + <span class="font-mono">{chatRoomId}</span> · Level: {level}{bankId ? ` · Bank: ${bankId}` : ""} |
184 | 132 | {#if participants.length > 0} |
185 | | - <span class="text-sm text-gray-600 dark:text-gray-400"> |
186 | | - Participants: {participants.length} |
187 | | - </span> |
188 | | - {/if} |
189 | | - {#if !room?.is_open_room} |
190 | | - <button |
191 | | - onclick={() => (showAddForm = !showAddForm)} |
192 | | - class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600" |
193 | | - data-testid="add-participant-btn" |
194 | | - > |
195 | | - {showAddForm ? "Cancel" : "Add Participant"} |
196 | | - </button> |
| 133 | + · Participants: {participants.length} |
197 | 134 | {/if} |
198 | | - </div> |
| 135 | + </p> |
| 136 | + {#if !room?.is_open_room} |
| 137 | + <a |
| 138 | + href={addParticipantLink} |
| 139 | + class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600" |
| 140 | + data-testid="add-participant-btn" |
| 141 | + > |
| 142 | + Add Participant |
| 143 | + </a> |
| 144 | + {/if} |
199 | 145 | </div> |
200 | 146 |
|
201 | | -{#if room && !room.is_open_room && room.joining_key} |
202 | | - <div class="mb-4 rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800" data-testid="joining-key-panel"> |
203 | | - <div class="flex flex-wrap items-center gap-3"> |
204 | | - <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Joining Key:</span> |
205 | | - <code class="flex-1 min-w-0 truncate rounded bg-gray-100 px-2 py-1 font-mono text-sm text-gray-900 dark:bg-gray-900 dark:text-gray-100" data-testid="joining-key-value">{room.joining_key}</code> |
206 | | - <button |
207 | | - onclick={copyJoiningKey} |
208 | | - class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600" |
209 | | - data-testid="copy-joining-key-btn" |
210 | | - > |
211 | | - Copy |
212 | | - </button> |
213 | | - <button |
214 | | - onclick={refreshJoiningKey} |
215 | | - class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600" |
216 | | - data-testid="refresh-joining-key-btn" |
217 | | - > |
218 | | - Refresh |
219 | | - </button> |
220 | | - </div> |
221 | | - </div> |
222 | | -{/if} |
223 | | - |
224 | 147 | {#if successMessage} |
225 | 148 | <div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200" data-testid="success-message"> |
226 | 149 | {successMessage} |
|
233 | 156 | </div> |
234 | 157 | {/if} |
235 | 158 |
|
236 | | -{#if showAddForm} |
237 | | - <div class="mb-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800" data-testid="add-participant-form"> |
238 | | - <h2 class="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">Add Participant</h2> |
239 | | - <div class="space-y-3"> |
240 | | - <div> |
241 | | - <label for="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">User ID</label> |
242 | | - <input |
243 | | - type="text" |
244 | | - id="user_id" |
245 | | - bind:value={addUserId} |
246 | | - class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" |
247 | | - data-testid="participant-user-id-input" |
248 | | - /> |
249 | | - </div> |
250 | | - <div> |
251 | | - <label for="permissions" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Permissions (comma-separated)</label> |
252 | | - <input |
253 | | - type="text" |
254 | | - id="permissions" |
255 | | - bind:value={addPermissions} |
256 | | - placeholder="can_send_message, can_delete_message, can_update_room" |
257 | | - class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" |
258 | | - data-testid="participant-permissions-input" |
259 | | - /> |
260 | | - </div> |
261 | | - <button |
262 | | - onclick={addParticipant} |
263 | | - disabled={!addUserId.trim()} |
264 | | - class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600" |
265 | | - data-testid="submit-add-participant" |
266 | | - > |
267 | | - Add |
268 | | - </button> |
269 | | - </div> |
270 | | - </div> |
271 | | -{/if} |
272 | | - |
273 | 159 | {#if loading} |
274 | 160 | <div class="flex items-center justify-center py-8"> |
275 | 161 | <div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div> |
276 | | - <span class="ml-2 text-gray-600 dark:text-gray-400">Loading...</span> |
| 162 | + <span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Loading...</span> |
277 | 163 | </div> |
278 | 164 | {:else if participants.length > 0} |
279 | 165 | <div class="overflow-x-auto"> |
280 | 166 | <table class="w-full border-collapse" data-testid="participants-table"> |
281 | 167 | <thead> |
282 | 168 | <tr class="border-b-2 border-gray-200 dark:border-gray-700"> |
283 | | - <th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">User ID</th> |
284 | | - <th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Permissions</th> |
285 | | - <th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Joined</th> |
286 | | - <th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Muted</th> |
287 | | - <th class="text-right px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Actions</th> |
| 169 | + <th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">User ID</th> |
| 170 | + <th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Permissions</th> |
| 171 | + <th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Joined</th> |
| 172 | + <th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Muted</th> |
| 173 | + <th class="text-right px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Actions</th> |
288 | 174 | </tr> |
289 | 175 | </thead> |
290 | 176 | <tbody> |
291 | 177 | {#each participants as participant (participant.participant_id)} |
292 | 178 | <tr class="border-b border-gray-100 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/50" data-testid="participant-row-{participant.user_id}"> |
293 | | - <td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100"> |
| 179 | + <td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100"> |
294 | 180 | <span class="font-medium">{participant.username || participant.user_id}</span> |
295 | 181 | {#if participant.provider} |
296 | | - <br /><span class="text-gray-500 dark:text-gray-500">{participant.provider}</span> |
| 182 | + <br /><span class="text-xs text-gray-500 dark:text-gray-500">{participant.provider}</span> |
297 | 183 | {/if} |
298 | 184 | {#if participant.username} |
299 | | - <br /><span class="font-mono text-gray-500 dark:text-gray-500">{participant.user_id}</span> |
| 185 | + <br /><span class="text-xs font-mono text-gray-500 dark:text-gray-500">{participant.user_id}</span> |
300 | 186 | {/if} |
301 | 187 | {#if participant.consumer_id} |
302 | | - <br /><span class="text-gray-500">Consumer: {participant.consumer_name || participant.consumer_id}</span> |
| 188 | + <br /><span class="text-xs text-gray-500">Consumer: {participant.consumer_name || participant.consumer_id}</span> |
303 | 189 | {/if} |
304 | 190 | </td> |
305 | | - <td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100"> |
| 191 | + <td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100"> |
306 | 192 | <div class="flex flex-wrap gap-1"> |
307 | 193 | {#each participant.permissions as perm} |
308 | 194 | <span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"> |
|
311 | 197 | {/each} |
312 | 198 | </div> |
313 | 199 | </td> |
314 | | - <td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">{formatDate(participant.joined_at)}</td> |
315 | | - <td class="px-3 py-2 text-xs"> |
| 200 | + <td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100">{formatDate(participant.joined_at)}</td> |
| 201 | + <td class="px-3 py-2 text-sm"> |
316 | 202 | <span class="rounded-full px-2 py-0.5 text-xs font-medium {participant.is_muted |
317 | 203 | ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400' |
318 | 204 | : 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'}"> |
|
336 | 222 | {:else if !error} |
337 | 223 | <div class="rounded-lg bg-gray-100 p-8 text-center dark:bg-gray-800" data-testid="empty-participants"> |
338 | 224 | {#if room?.is_open_room} |
339 | | - <p class="text-lg font-medium text-gray-700 dark:text-gray-300" data-testid="everyone-label"> |
| 225 | + <p class="text-sm font-medium text-gray-700 dark:text-gray-300" data-testid="everyone-label"> |
340 | 226 | This room is Open. Everyone can join. |
341 | 227 | </p> |
342 | 228 | {:else} |
343 | | - <p class="text-lg font-medium text-gray-700 dark:text-gray-300"> |
| 229 | + <p class="text-sm font-medium text-gray-700 dark:text-gray-300"> |
344 | 230 | No participants found |
345 | 231 | </p> |
346 | | - <p class="mt-1 text-gray-600 dark:text-gray-400"> |
| 232 | + <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> |
347 | 233 | Add a participant to this chat room. |
348 | 234 | </p> |
349 | 235 | {/if} |
|
0 commit comments