1/**
2 * Client-side helpers for authenticated Convex calls (AIM features).
3 *
4 * Every authQuery/authMutation needs `tokenHash`; this module reads it from
5 * chatState so components only pass their own args.
6 */
7import { useConvexClient, useQuery } from 'convex-svelte';
8import { api } from '../../convex/_generated/api';
9import type { FunctionReference, FunctionArgs } from 'convex/server';
10import type { Id } from '../../convex/_generated/dataModel';
11import { chatState } from '$lib/states/chat.svelte';
12import { browser } from '$app/environment';
13import { env } from '$env/dynamic/public';
14
15export const convexAvailable = browser && Boolean(env.PUBLIC_CONVEX_URL);
16
17/** Returns the current tokenHash or null when signed out. Reactive when read inside $derived. */
18export function tokenHash() {
19 return chatState.getSessionTokenHash();
20}
21
22/**
23 * Subscribe to an authenticated query. Must be called at component init
24 * (top level of <script>), like useQuery. The subscription is skipped while
25 * signed out and resumes automatically after login (data is undefined meanwhile).
26 *
27 * const buddies = authQuery(api.aim.getMyBuddies, () => ({}));
28 * buddies?.data
29 */
30export function authQuery<Q extends FunctionReference<'query'>>(
31 fn: Q,
32 args: () => Omit<FunctionArgs<Q>, 'tokenHash'> | 'skip' = () =>
33 ({}) as Omit<FunctionArgs<Q>, 'tokenHash'>
34) {
35 if (!convexAvailable) return null;
36 return useQuery(fn, () => {
37 const hash = tokenHash();
38 const a = args();
39 if (!hash || a === 'skip') return 'skip';
40 return { tokenHash: hash, ...a } as FunctionArgs<Q>;
41 });
42}
43
44export function useAimClient() {
45 const client = convexAvailable ? useConvexClient() : null;
46
47 function requireAuth() {
48 const hash = tokenHash();
49 if (!client || !hash) throw new Error('Non connecté');
50 return { client, hash };
51 }
52
53 return {
54 client,
55 async addBuddy(buddyId: Id<'users'>, group?: string) {
56 const { client, hash } = requireAuth();
57 return client.mutation(api.aim.addBuddy, { tokenHash: hash, buddyId, group });
58 },
59 async removeBuddy(buddyId: Id<'users'>) {
60 const { client, hash } = requireAuth();
61 return client.mutation(api.aim.removeBuddy, { tokenHash: hash, buddyId });
62 },
63 async getOrCreateDirectRoom(otherUserId: Id<'users'>) {
64 const { client, hash } = requireAuth();
65 return client.mutation(api.aim.getOrCreateDirectRoom, { tokenHash: hash, otherUserId });
66 },
67 async createGroupRoom(name: string, topic?: string) {
68 const { client, hash } = requireAuth();
69 return client.mutation(api.aim.createGroupRoom, { tokenHash: hash, name, topic });
70 },
71 async setAwayMessage(awayMessage?: string) {
72 const { client, hash } = requireAuth();
73 return client.mutation(api.aim.setAwayMessage, { tokenHash: hash, awayMessage });
74 },
75 async updateProfile(patch: { profile?: string }) {
76 const { client, hash } = requireAuth();
77 return client.mutation(api.aim.updateProfile, { tokenHash: hash, ...patch });
78 },
79 async markRoomRead(roomId: Id<'chatRooms'>) {
80 const { client, hash } = requireAuth();
81 return client.mutation(api.aim.markRoomRead, { tokenHash: hash, roomId });
82 },
83 async setTyping(roomId: Id<'chatRooms'>, typing: boolean) {
84 const { client, hash } = requireAuth();
85 return client.mutation(api.aim.setTyping, { tokenHash: hash, roomId, typing });
86 }
87 };
88}