|
20 | 20 | let debounceTimer: NodeJS.Timeout;
|
21 | 21 |
|
22 | 22 | async function loadMessages() {
|
23 |
| - const { data } = await apiClient.get<{ chats: Chat[] }>('/api/chats'); |
24 |
| - const { data: userData } = await apiClient.get('/api/users'); |
25 |
| - currentUserId = userData.id; |
26 |
| -
|
27 |
| - messages = data.chats.map((c) => { |
28 |
| - const members = c.participants.filter((u) => u.id !== userData.id); |
29 |
| - const memberNames = members.map((m) => m.name ?? m.handle ?? m.ename); |
30 |
| - const avatar = |
31 |
| - members.length > 1 |
32 |
| - ? 'https://cdn.jsdelivr.net/npm/[email protected]/icons/people-fill.svg' |
33 |
| - : members[0].avatarUrl; |
34 |
| - return { |
35 |
| - id: c.id, |
36 |
| - avatar, |
37 |
| - username: c.handle ?? memberNames.join(', '), |
38 |
| - unread: c.latestMessage ? c.latestMessage.isRead : false, |
39 |
| - text: c.latestMessage?.text ?? 'No message yet', |
40 |
| - handle: c.handle ?? memberNames.join(', '), |
41 |
| - name: c.handle ?? memberNames.join(', ') |
42 |
| - }; |
43 |
| - }); |
| 23 | + try { |
| 24 | + const { data } = await apiClient.get<{ chats: Chat[] }>('/api/chats'); |
| 25 | + const { data: userData } = await apiClient.get('/api/users'); |
| 26 | + currentUserId = userData.id; |
| 27 | +
|
| 28 | + // Separate direct messages and group chats |
| 29 | + const directMessages: MessageType[] = []; |
| 30 | + const groupChats: GroupInfo[] = []; |
| 31 | +
|
| 32 | + data.chats.forEach((c) => { |
| 33 | + const members = c.participants.filter((u) => u.id !== userData.id); |
| 34 | + const memberNames = members.map((m) => m.name ?? m.handle ?? m.ename); |
| 35 | + const isGroup = members.length > 1; |
| 36 | +
|
| 37 | + if (isGroup) { |
| 38 | + // This is a group chat |
| 39 | + groupChats.push({ |
| 40 | + id: c.id, |
| 41 | + name: c.handle ?? memberNames.join(', '), |
| 42 | + avatar: '/images/group.png' |
| 43 | + }); |
| 44 | + } |
| 45 | +
|
| 46 | + const avatar = isGroup |
| 47 | + ? '/images/group.png' |
| 48 | + : members[0]?.avatarUrl || |
| 49 | + 'https://cdn.jsdelivr.net/npm/[email protected]/icons/people-fill.svg'; |
| 50 | +
|
| 51 | + directMessages.push({ |
| 52 | + id: c.id, |
| 53 | + avatar, |
| 54 | + username: c.handle ?? memberNames.join(', '), |
| 55 | + unread: c.latestMessage ? !c.latestMessage.isRead : false, |
| 56 | + text: c.latestMessage?.text ?? 'No message yet', |
| 57 | + handle: c.handle ?? memberNames.join(', '), |
| 58 | + name: c.handle ?? memberNames.join(', ') |
| 59 | + }); |
| 60 | + }); |
| 61 | +
|
| 62 | + messages = directMessages; |
| 63 | + groups = groupChats; |
| 64 | + } catch (error) { |
| 65 | + console.error('Failed to load messages:', error); |
| 66 | + } |
44 | 67 | }
|
45 | 68 |
|
46 | 69 | onMount(async () => {
|
47 |
| - await loadMessages(); |
| 70 | + try { |
| 71 | + await loadMessages(); |
48 | 72 |
|
49 |
| - const memberRes = await apiClient.get('/api/members'); |
50 |
| - allMembers = memberRes.data; |
| 73 | + const memberRes = await apiClient.get('/api/members'); |
| 74 | + allMembers = memberRes.data; |
| 75 | + } catch (error) { |
| 76 | + console.error('Failed to initialize messages page:', error); |
| 77 | + } |
51 | 78 | });
|
52 | 79 |
|
53 | 80 | function toggleMemberSelection(id: string) {
|
|
71 | 98 |
|
72 | 99 | try {
|
73 | 100 | if (selectedMembers.length === 1) {
|
74 |
| - await apiClient.post('/api/chats/', { |
| 101 | + // Create direct message |
| 102 | + await apiClient.post('/api/chats', { |
75 | 103 | name: allMembers.find((m) => m.id === selectedMembers[0])?.name ?? 'New Chat',
|
76 | 104 | participantIds: [selectedMembers[0]]
|
77 | 105 | });
|
78 |
| - await loadMessages(); // 🛠️ Refresh to include the new direct message |
| 106 | + await loadMessages(); // Refresh to include the new direct message |
79 | 107 | } else {
|
| 108 | + // Create group chat |
80 | 109 | const groupMembers = allMembers.filter((m) => selectedMembers.includes(m.id));
|
81 | 110 | const groupName = groupMembers.map((m) => m.name ?? m.handle ?? m.ename).join(', ');
|
82 |
| - groups = [ |
83 |
| - ...groups, |
84 |
| - { |
85 |
| - id: Math.random().toString(36).slice(2), |
86 |
| - name: groupName, |
87 |
| - avatar: '/images/group.png' |
88 |
| - } |
89 |
| - ]; |
| 111 | +
|
| 112 | + // Create group chat via API |
| 113 | + const response = await apiClient.post('/api/chats', { |
| 114 | + name: groupName, |
| 115 | + participantIds: selectedMembers, |
| 116 | + isGroup: true |
| 117 | + }); |
| 118 | +
|
| 119 | + // Add to local groups state |
| 120 | + const newGroup: GroupInfo = { |
| 121 | + id: response.data.id, |
| 122 | + name: groupName, |
| 123 | + avatar: '/images/group.png' |
| 124 | + }; |
| 125 | + groups = [...groups, newGroup]; |
| 126 | +
|
| 127 | + // Also add to messages for consistency |
| 128 | + const newMessage: MessageType = { |
| 129 | + id: response.data.id, |
| 130 | + avatar: newGroup.avatar, |
| 131 | + username: groupName, |
| 132 | + text: 'Group chat created', |
| 133 | + unread: false, |
| 134 | + name: groupName, |
| 135 | + handle: groupName |
| 136 | + }; |
| 137 | + messages = [newMessage, ...messages]; |
90 | 138 | }
|
91 | 139 | } catch (err) {
|
92 | 140 | console.error('Failed to create chat:', err);
|
| 141 | + alert('Failed to create chat. Please try again.'); |
93 | 142 | } finally {
|
94 | 143 | openNewChatModal = false;
|
95 | 144 | selectedMembers = [];
|
|
128 | 177 | {/if}
|
129 | 178 |
|
130 | 179 | {#if groups.length > 0}
|
131 |
| - <h3 class="text-md mt-6 mb-2 font-semibold text-gray-700">Groups</h3> |
| 180 | + <h3 class="text-md mb-2 mt-6 font-semibold text-gray-700">Groups</h3> |
132 | 181 | {#each groups as group}
|
133 | 182 | <Group
|
134 | 183 | name={group.name || 'New Group'}
|
|
143 | 192 | </div>
|
144 | 193 | {/if}
|
145 | 194 |
|
146 |
| - <dialog |
147 |
| - open={openNewChatModal} |
148 |
| - use:clickOutside={() => (openNewChatModal = false)} |
149 |
| - onclose={() => (openNewChatModal = false)} |
150 |
| - class="absolute start-[50%] top-[50%] z-50 w-[90vw] translate-x-[-50%] translate-y-[-50%] rounded-3xl border border-gray-400 bg-white p-4 shadow-xl md:max-w-[40vw]" |
151 |
| - > |
152 |
| - <div class="relative w-full space-y-6 rounded-xl bg-white p-6"> |
153 |
| - <h2 class="text-xl font-semibold">Start a New Chat</h2> |
154 |
| - |
155 |
| - <Input |
156 |
| - type="text" |
157 |
| - bind:value={searchValue} |
158 |
| - placeholder="Search users..." |
159 |
| - oninput={(e: Event) => handleSearch((e.target as HTMLInputElement).value)} |
160 |
| - /> |
| 195 | + {#if openNewChatModal} |
| 196 | + <div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"> |
| 197 | + <div |
| 198 | + class="w-[90vw] max-w-md rounded-3xl border border-gray-200 bg-white p-6 shadow-xl" |
| 199 | + > |
| 200 | + <div class="mb-6 flex items-center justify-between"> |
| 201 | + <h2 class="text-xl font-semibold text-gray-900">Start a New Chat</h2> |
| 202 | + <button |
| 203 | + onclick={() => (openNewChatModal = false)} |
| 204 | + class="rounded-full p-2 hover:bg-gray-100" |
| 205 | + > |
| 206 | + <svg |
| 207 | + class="h-5 w-5 text-gray-500" |
| 208 | + fill="none" |
| 209 | + stroke="currentColor" |
| 210 | + viewBox="0 0 24 24" |
| 211 | + > |
| 212 | + <path |
| 213 | + stroke-linecap="round" |
| 214 | + stroke-linejoin="round" |
| 215 | + stroke-width="2" |
| 216 | + d="M6 18L18 6M6 6l12 12" |
| 217 | + ></path> |
| 218 | + </svg> |
| 219 | + </button> |
| 220 | + </div> |
161 | 221 |
|
162 |
| - {#if $isSearching} |
163 |
| - <div class="mt-2 text-gray-500">Searching...</div> |
164 |
| - {:else if $searchError} |
165 |
| - <div class="mt-2 text-red-500">{$searchError}</div> |
166 |
| - {/if} |
167 |
| - |
168 |
| - <div class="max-h-[250px] space-y-3 overflow-y-auto"> |
169 |
| - {#each $searchResults.filter((m) => m.id !== currentUserId) as member} |
170 |
| - <label class="flex cursor-pointer items-center space-x-3"> |
171 |
| - <input |
172 |
| - type="checkbox" |
173 |
| - checked={selectedMembers.includes(member.id)} |
174 |
| - onchange={() => toggleMemberSelection(member.id)} |
175 |
| - class="accent-brand focus:ring" |
176 |
| - /> |
177 |
| - <Avatar src={member.avatarUrl} size="sm" /> |
178 |
| - <span class="text-sm">{member.name ?? member.handle}</span> |
179 |
| - </label> |
180 |
| - {/each} |
181 |
| - </div> |
| 222 | + <div class="space-y-4"> |
| 223 | + <Input |
| 224 | + type="text" |
| 225 | + bind:value={searchValue} |
| 226 | + placeholder="Search users..." |
| 227 | + oninput={(e: Event) => handleSearch((e.target as HTMLInputElement).value)} |
| 228 | + /> |
| 229 | + |
| 230 | + {#if $isSearching} |
| 231 | + <div class="text-center text-gray-500">Searching...</div> |
| 232 | + {:else if $searchError} |
| 233 | + <div class="text-center text-red-500">{$searchError}</div> |
| 234 | + {:else if $searchResults.length === 0 && searchValue.trim()} |
| 235 | + <div class="text-center text-gray-500">No users found</div> |
| 236 | + {/if} |
| 237 | + |
| 238 | + {#if $searchResults.length > 0} |
| 239 | + <div class="max-h-[250px] space-y-3 overflow-y-auto"> |
| 240 | + {#each $searchResults.filter((m) => m.id !== currentUserId) as member} |
| 241 | + <label |
| 242 | + class="flex cursor-pointer items-center space-x-3 rounded-lg p-3 hover:bg-gray-50" |
| 243 | + > |
| 244 | + <input |
| 245 | + type="checkbox" |
| 246 | + checked={selectedMembers.includes(member.id)} |
| 247 | + onchange={(e: Event) => { |
| 248 | + toggleMemberSelection(member.id); |
| 249 | + }} |
| 250 | + class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 251 | + /> |
| 252 | + <Avatar src={member.avatarUrl} size="sm" /> |
| 253 | + <div class="flex flex-col"> |
| 254 | + <span class="text-sm font-medium text-gray-900" |
| 255 | + >{member.name ?? member.handle}</span |
| 256 | + > |
| 257 | + {#if member.description} |
| 258 | + <span class="text-xs text-gray-500" |
| 259 | + >{member.description}</span |
| 260 | + > |
| 261 | + {/if} |
| 262 | + </div> |
| 263 | + </label> |
| 264 | + {/each} |
| 265 | + </div> |
| 266 | + {/if} |
182 | 267 |
|
183 |
| - <div class="flex justify-end gap-2 pt-4"> |
184 |
| - <Button |
185 |
| - size="sm" |
186 |
| - variant="secondary" |
187 |
| - callback={() => { |
188 |
| - openNewChatModal = false; |
189 |
| - }}>Cancel</Button |
190 |
| - > |
191 |
| - <Button size="sm" variant="primary" callback={createChat}>Start Chat</Button> |
| 268 | + {#if selectedMembers.length > 0} |
| 269 | + <div class="rounded-lg bg-blue-50 p-3"> |
| 270 | + <p class="text-sm text-blue-800"> |
| 271 | + {selectedMembers.length === 1 |
| 272 | + ? 'Direct message will be created' |
| 273 | + : `Group chat with ${selectedMembers.length} members will be created`} |
| 274 | + </p> |
| 275 | + </div> |
| 276 | + {/if} |
| 277 | + |
| 278 | + <div class="flex justify-end gap-3 pt-4"> |
| 279 | + <Button |
| 280 | + size="sm" |
| 281 | + variant="secondary" |
| 282 | + callback={() => (openNewChatModal = false)} |
| 283 | + > |
| 284 | + Cancel |
| 285 | + </Button> |
| 286 | + <Button |
| 287 | + size="sm" |
| 288 | + variant="primary" |
| 289 | + callback={createChat} |
| 290 | + disabled={selectedMembers.length === 0} |
| 291 | + > |
| 292 | + {selectedMembers.length === 1 ? 'Start Chat' : 'Create Group'} |
| 293 | + </Button> |
| 294 | + </div> |
| 295 | + </div> |
192 | 296 | </div>
|
193 | 297 | </div>
|
194 |
| - </dialog> |
| 298 | + {/if} |
195 | 299 | </section>
|
0 commit comments