import type { AppSupabaseClient } from '../supabase/client.js'; import type { FriendshipStatus } from '../supabase/types.js'; export interface ProfileBrief { userId: string; username: string; displayName: string; avatarUrl: string | null; } export type FriendDirection = 'outgoing' | 'incoming'; export interface Friendship { peer: ProfileBrief; status: FriendshipStatus; // Only meaningful when status === 'pending'. direction: FriendDirection; createdAt: string; acceptedAt: string | null; } const PROFILE_BRIEF_COLS = 'user_id, username, display_name, avatar_url'; function mapProfileBrief(row: { user_id: string; username: string; display_name: string; avatar_url: string | null; }): ProfileBrief { return { userId: row.user_id, username: row.username, displayName: row.display_name, avatarUrl: row.avatar_url, }; } async function currentUserId(client: AppSupabaseClient): Promise { const { data, error } = await client.auth.getUser(); if (error) throw error; if (!data.user) throw new Error('not authenticated'); return data.user.id; } function pairKey(a: string, b: string): { lo: string; hi: string } { return a < b ? { lo: a, hi: b } : { lo: b, hi: a }; } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- export async function searchProfiles( client: AppSupabaseClient, query: string, limit = 10, ): Promise { const trimmed = query.trim().toLowerCase(); if (trimmed.length < 2) return []; const myId = await currentUserId(client); const { data, error } = await client .from('profiles') .select(PROFILE_BRIEF_COLS) .ilike('username', trimmed + '%') .neq('user_id', myId) .limit(limit); if (error) throw error; return (data ?? []).map(mapProfileBrief); } // --------------------------------------------------------------------------- // Friendships list // --------------------------------------------------------------------------- export async function listFriendships(client: AppSupabaseClient): Promise { const myId = await currentUserId(client); const { data: rows, error } = await client .from('friendships') .select('user_lo, user_hi, status, requested_by, created_at, accepted_at') .order('created_at', { ascending: false }); if (error) throw error; if (!rows || rows.length === 0) return []; const peerIds = Array.from( new Set(rows.map((r) => (r.user_lo === myId ? r.user_hi : r.user_lo))), ); const { data: profiles, error: pErr } = await client .from('profiles') .select(PROFILE_BRIEF_COLS) .in('user_id', peerIds); if (pErr) throw pErr; const profileMap = new Map(); for (const p of profiles ?? []) { const brief = mapProfileBrief(p); profileMap.set(brief.userId, brief); } return rows.map((r) => { const peerId = r.user_lo === myId ? r.user_hi : r.user_lo; const peer = profileMap.get(peerId) ?? { userId: peerId, username: '?', displayName: '?', avatarUrl: null, }; return { peer, status: r.status, direction: r.requested_by === myId ? 'outgoing' : 'incoming', createdAt: r.created_at, acceptedAt: r.accepted_at, }; }); } // --------------------------------------------------------------------------- // Mutations // --------------------------------------------------------------------------- export async function sendFriendRequest( client: AppSupabaseClient, targetUserId: string, ): Promise { const { error } = await client.rpc('send_friend_request', { target_user_id: targetUserId }); if (error) throw error; } export async function acceptFriendRequest( client: AppSupabaseClient, peerUserId: string, ): Promise { const myId = await currentUserId(client); const { lo, hi } = pairKey(myId, peerUserId); const { error } = await client .from('friendships') .update({ status: 'accepted' }) .eq('user_lo', lo) .eq('user_hi', hi); if (error) throw error; } // Used for both "decline incoming" and "cancel outgoing" — semantically a // bilateral break. export async function removeFriendship( client: AppSupabaseClient, peerUserId: string, ): Promise { const myId = await currentUserId(client); const { lo, hi } = pairKey(myId, peerUserId); const { error } = await client .from('friendships') .delete() .eq('user_lo', lo) .eq('user_hi', hi); if (error) throw error; }