initial
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user