Files
ChatApp/packages/shared/src/friends/index.ts
T
2026-04-18 23:11:35 +02:00

161 lines
4.5 KiB
TypeScript

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<string> {
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<ProfileBrief[]> {
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<Friendship[]> {
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<string, ProfileBrief>();
for (const p of profiles ?? []) {
const brief = mapProfileBrief(p);
profileMap.set(brief.userId, brief);
}
return rows.map<Friendship>((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<void> {
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<void> {
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<void> {
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;
}