|
1 | 1 | <script lang="ts">
|
2 | 2 | import { goto } from '$app/navigation';
|
3 |
| - import { Message } from '$lib/fragments'; |
4 | 3 | import { onMount } from 'svelte';
|
5 |
| - import { apiClient } from '$lib/utils/axios'; |
6 |
| - import { heading } from '../../store'; |
| 4 | + import { Message } from '$lib/fragments'; |
7 | 5 | import Group from '$lib/fragments/Group/Group.svelte';
|
| 6 | + import { Button, Avatar, Input } from '$lib/ui'; |
| 7 | + import { clickOutside } from '$lib/utils'; |
| 8 | + import { heading } from '../../store'; |
| 9 | + import { apiClient } from '$lib/utils/axios'; |
| 10 | +
|
| 11 | + import { |
| 12 | + searchUsers, |
| 13 | + searchResults, |
| 14 | + isSearching, |
| 15 | + searchError |
| 16 | + } from '$lib/stores/users'; |
| 17 | + import type { GroupInfo } from '$lib/types'; |
8 | 18 |
|
9 | 19 | let messages = $state([]);
|
| 20 | + let groups: GroupInfo[] = $state([]); |
| 21 | + let allMembers = $state([]); |
| 22 | + let selectedMembers = $state<string[]>([]); |
| 23 | + let currentUserId = ''; |
| 24 | + let openNewChatModal = $state(false); |
| 25 | + let searchValue = $state(''); |
| 26 | + let debounceTimer: NodeJS.Timeout; |
10 | 27 |
|
11 |
| - onMount(async () => { |
| 28 | + async function loadMessages() { |
12 | 29 | const { data } = await apiClient.get('/api/chats');
|
13 | 30 | const { data: userData } = await apiClient.get('/api/users');
|
| 31 | + currentUserId = userData.id; |
| 32 | +
|
14 | 33 | messages = data.chats.map((c) => {
|
15 | 34 | const members = c.participants.filter((u) => u.id !== userData.id);
|
16 | 35 | const memberNames = members.map((m) => m.name ?? m.handle ?? m.ename);
|
|
23 | 42 | text: c.latestMessage?.text ?? 'No message yet'
|
24 | 43 | };
|
25 | 44 | });
|
| 45 | + } |
| 46 | +
|
| 47 | + onMount(async () => { |
| 48 | + await loadMessages(); |
| 49 | +
|
| 50 | + const memberRes = await apiClient.get('/api/members'); |
| 51 | + allMembers = memberRes.data; |
26 | 52 | });
|
| 53 | +
|
| 54 | + function toggleMemberSelection(id: string) { |
| 55 | + if (selectedMembers.includes(id)) { |
| 56 | + selectedMembers = selectedMembers.filter((m) => m !== id); |
| 57 | + } else { |
| 58 | + selectedMembers = [...selectedMembers, id]; |
| 59 | + } |
| 60 | + } |
| 61 | +
|
| 62 | + function handleSearch(value: string) { |
| 63 | + searchValue = value; |
| 64 | + clearTimeout(debounceTimer); |
| 65 | + debounceTimer = setTimeout(() => { |
| 66 | + searchUsers(value); |
| 67 | + }, 300); |
| 68 | + } |
| 69 | +
|
| 70 | + async function createChat() { |
| 71 | + if (selectedMembers.length === 0) return; |
| 72 | +
|
| 73 | + try { |
| 74 | + if (selectedMembers.length === 1) { |
| 75 | + await apiClient.post(`/api/chats/`, { |
| 76 | + name: |
| 77 | + allMembers.find((m) => m.id === selectedMembers[0])?.name ?? |
| 78 | + 'New Chat', |
| 79 | + participantIds: [selectedMembers[0]] |
| 80 | + }); |
| 81 | + await loadMessages(); // 🛠️ Refresh to include the new direct message |
| 82 | + } else { |
| 83 | + const groupMembers = allMembers.filter((m) => |
| 84 | + selectedMembers.includes(m.id) |
| 85 | + ); |
| 86 | + const groupName = groupMembers.map((m) => m.name ?? m.handle ?? m.ename).join(', '); |
| 87 | + groups = [ |
| 88 | + ...groups, |
| 89 | + { |
| 90 | + id: Math.random().toString(36).slice(2), |
| 91 | + name: groupName, |
| 92 | + avatar: '/images/group.png' |
| 93 | + } |
| 94 | + ]; |
| 95 | + } |
| 96 | + } catch (err) { |
| 97 | + console.error('Failed to create chat:', err); |
| 98 | + } finally { |
| 99 | + openNewChatModal = false; |
| 100 | + selectedMembers = []; |
| 101 | + searchValue = ''; |
| 102 | + } |
| 103 | + } |
27 | 104 | </script>
|
28 | 105 |
|
29 |
| -<section> |
30 |
| - {#if messages && messages.length > 0} |
| 106 | + |
| 107 | +<section class="px-4 py-4"> |
| 108 | + <div class="flex justify-end mb-4"> |
| 109 | + <Button variant="secondary" size="sm" callback={() => {(openNewChatModal = true)}}> |
| 110 | + New Chat |
| 111 | + </Button> |
| 112 | + </div> |
| 113 | + |
| 114 | + {#if messages.length > 0} |
| 115 | + <h3 class="text-md font-semibold text-gray-700 mb-2">Messages</h3> |
31 | 116 | {#each messages as message}
|
32 | 117 | <Message
|
33 |
| - class="mb-6" |
| 118 | + class="mb-2" |
34 | 119 | avatar={message.avatar}
|
35 | 120 | username={message.username}
|
36 | 121 | text={message.text}
|
|
41 | 126 | }}
|
42 | 127 | />
|
43 | 128 | {/each}
|
44 |
| - {:else} |
45 |
| - <div class="w-full px-5 py-5 text-center"> |
46 |
| - You don't have any messages yet, please start a Direct Message with Someone by searching |
47 |
| - their name |
| 129 | + {/if} |
| 130 | + |
| 131 | + {#if groups.length > 0} |
| 132 | + <h3 class="text-md font-semibold text-gray-700 mb-2 mt-6">Groups</h3> |
| 133 | + {#each groups as group} |
| 134 | + <Group |
| 135 | + name={group.name || "New Group"} |
| 136 | + avatar={group.avatar} |
| 137 | + unread={true} |
| 138 | + callback={() => goto(`/group/${group.id}`)} |
| 139 | + /> |
| 140 | + {/each} |
| 141 | + {:else if messages.length === 0} |
| 142 | + <div class="w-full px-5 py-5 text-center text-sm text-gray-500"> |
| 143 | + You don't have any messages yet. Start a Direct Message by searching a name. |
48 | 144 | </div>
|
49 |
| - <!-- group id needs to be added --> |
50 |
| - <Group name="Developers" avatar="https://picsum.photos/200/300" unread={true} callback={() => goto("/group/123")}/> |
51 | 145 | {/if}
|
| 146 | + |
| 147 | + <dialog |
| 148 | + open={openNewChatModal} |
| 149 | + use:clickOutside={() => (openNewChatModal = false)} |
| 150 | + onclose={() => (openNewChatModal = false)} |
| 151 | + class="w-[90vw] md:max-w-[40vw] z-50 absolute start-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] p-4 border border-gray-400 rounded-3xl bg-white shadow-xl" |
| 152 | + > |
| 153 | + <div class="bg-white rounded-xl w-full p-6 space-y-6 relative"> |
| 154 | + <h2 class="text-xl font-semibold">Start a New Chat</h2> |
| 155 | + |
| 156 | + <Input |
| 157 | + type="text" |
| 158 | + bind:value={searchValue} |
| 159 | + placeholder="Search users..." |
| 160 | + oninput={(e: Event) => handleSearch((e.target as HTMLInputElement).value)} |
| 161 | + /> |
| 162 | + |
| 163 | + {#if $isSearching} |
| 164 | + <div class="text-gray-500 mt-2">Searching...</div> |
| 165 | + {:else if $searchError} |
| 166 | + <div class="text-red-500 mt-2">{$searchError}</div> |
| 167 | + {/if} |
| 168 | + |
| 169 | + <div class="max-h-[250px] overflow-y-auto space-y-3"> |
| 170 | + {#each $searchResults.filter(m => m.id !== currentUserId) as member} |
| 171 | + <label class="flex items-center space-x-3 cursor-pointer"> |
| 172 | + <input |
| 173 | + type="checkbox" |
| 174 | + checked={selectedMembers.includes(member.id)} |
| 175 | + onchange={() => toggleMemberSelection(member.id)} |
| 176 | + class="accent-brand focus:ring" |
| 177 | + /> |
| 178 | + <Avatar src={member.avatarUrl} size="sm" /> |
| 179 | + <span class="text-sm">{member.name ?? member.handle ?? member.ename}</span> |
| 180 | + </label> |
| 181 | + {/each} |
| 182 | + </div> |
| 183 | + |
| 184 | + <div class="flex justify-end gap-2 pt-4"> |
| 185 | + <Button size="sm" variant="secondary" callback={() => {(openNewChatModal = false)}}>Cancel</Button> |
| 186 | + <Button size="sm" variant="primary" callback={createChat}>Start Chat</Button> |
| 187 | + </div> |
| 188 | + </div> |
| 189 | + </dialog> |
52 | 190 | </section>
|
0 commit comments