Skip to content

Commit af39654

Browse files
committed
fix: pictique group chat
1 parent 1362ede commit af39654

File tree

2 files changed

+230
-106
lines changed

2 files changed

+230
-106
lines changed

platforms/pictique/src/routes/(auth)/auth/+page.svelte

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { apiClient, setAuthId, setAuthToken } from '$lib/utils';
77
import { onMount } from 'svelte';
88
import { qrcode } from 'svelte-qrcode-action';
9+
import { isMobileDevice, getDeepLinkUrl } from '$lib/utils/mobile-detection';
910
1011
let qrData: string;
1112
@@ -46,34 +47,53 @@
4647
<div
4748
class="h-max-[600px] w-max-[400px] mb-5 flex flex-col items-center gap-5 rounded-xl bg-[#F476481A] p-5"
4849
>
49-
<h2>Scan the QR code using your <b><u>eID App</u></b> to login</h2>
50+
<h2>
51+
{#if isMobileDevice()}
52+
Login with your <b><u>eID Wallet</u></b>
53+
{:else}
54+
Scan the QR code using your <b><u>eID App</u></b> to login
55+
{/if}
56+
</h2>
5057
{#if qrData}
51-
<article
52-
class="overflow-hidden rounded-2xl"
53-
use:qrcode={{
54-
data: qrData,
55-
width: 250,
56-
height: 250,
57-
margin: 12,
58-
type: 'canvas',
59-
dotsOptions: {
60-
type: 'rounded',
61-
color: '#fff'
62-
},
63-
backgroundOptions: {
64-
gradient: {
65-
type: 'linear',
66-
rotation: 50,
67-
colorStops: [
68-
{ offset: 0, color: '#4D44EF' },
69-
{ offset: 0.65, color: '#F35B5B' },
70-
{ offset: 1, color: '#F7A428' }
71-
]
58+
{#if isMobileDevice()}
59+
<div class="flex flex-col items-center gap-4">
60+
<a
61+
href={getDeepLinkUrl(qrData)}
62+
class="rounded-xl bg-gradient-to-r from-[#4D44EF] via-[#F35B5B] to-[#F7A428] px-8 py-4 text-lg font-semibold text-white transition-opacity hover:opacity-90"
63+
>
64+
Login with eID Wallet
65+
</a>
66+
<div class="max-w-xs text-center text-sm text-gray-600">
67+
Click the button to open your eID wallet app
68+
</div>
69+
</div>
70+
{:else}
71+
<article
72+
class="overflow-hidden rounded-2xl"
73+
use:qrcode={{
74+
data: qrData,
75+
width: 250,
76+
height: 250,
77+
margin: 12,
78+
type: 'canvas',
79+
dotsOptions: {
80+
type: 'rounded',
81+
color: '#fff'
82+
},
83+
backgroundOptions: {
84+
gradient: {
85+
type: 'linear',
86+
rotation: 50,
87+
colorStops: [
88+
{ offset: 0, color: '#4D44EF' },
89+
{ offset: 0.65, color: '#F35B5B' },
90+
{ offset: 1, color: '#F7A428' }
91+
]
92+
}
7293
}
73-
}
74-
}}
75-
></article>
76-
<a href={qrData}>{qrData}</a>
94+
}}
95+
></article>
96+
{/if}
7797
{/if}
7898
<p>
7999
<span class="mb-1 block font-bold text-gray-600">The code is valid for 60 seconds</span>

platforms/pictique/src/routes/(protected)/messages/+page.svelte

Lines changed: 184 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,61 @@
2020
let debounceTimer: NodeJS.Timeout;
2121
2222
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+
}
4467
}
4568
4669
onMount(async () => {
47-
await loadMessages();
70+
try {
71+
await loadMessages();
4872
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+
}
5178
});
5279
5380
function toggleMemberSelection(id: string) {
@@ -71,25 +98,47 @@
7198
7299
try {
73100
if (selectedMembers.length === 1) {
74-
await apiClient.post('/api/chats/', {
101+
// Create direct message
102+
await apiClient.post('/api/chats', {
75103
name: allMembers.find((m) => m.id === selectedMembers[0])?.name ?? 'New Chat',
76104
participantIds: [selectedMembers[0]]
77105
});
78-
await loadMessages(); // 🛠️ Refresh to include the new direct message
106+
await loadMessages(); // Refresh to include the new direct message
79107
} else {
108+
// Create group chat
80109
const groupMembers = allMembers.filter((m) => selectedMembers.includes(m.id));
81110
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];
90138
}
91139
} catch (err) {
92140
console.error('Failed to create chat:', err);
141+
alert('Failed to create chat. Please try again.');
93142
} finally {
94143
openNewChatModal = false;
95144
selectedMembers = [];
@@ -128,7 +177,7 @@
128177
{/if}
129178

130179
{#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>
132181
{#each groups as group}
133182
<Group
134183
name={group.name || 'New Group'}
@@ -143,53 +192,108 @@
143192
</div>
144193
{/if}
145194

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>
161221

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}
182267

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>
192296
</div>
193297
</div>
194-
</dialog>
298+
{/if}
195299
</section>

0 commit comments

Comments
 (0)