b61f929cf7
packages/shared/src/index.ts and all sub-modules used .js extensions on relative imports (e.g. './admin/index.js') pointing at .ts source files. TypeScript with moduleResolution: "Bundler" doesn't need them, and Metro's eager exporter (used for preview / production builds) reads them literally and fails — only the dev-server Metro fell back to .ts. Workspace typecheck remains 8/8 green; Vite and TS Bundler resolution already accept both styles, so desktop is unaffected.
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import type { AppSupabaseClient } from '../supabase/client';
|
|
import type { MemberRole } from '../supabase/types';
|
|
|
|
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;
|
|
}
|
|
|
|
export interface CreateGroupParams {
|
|
client: AppSupabaseClient;
|
|
name: string;
|
|
memberUserIds: string[];
|
|
}
|
|
|
|
// Client-side 3-step create: conversation → self as admin → peers as members.
|
|
// On any peer-insert failure the conversation still survives (partial group is
|
|
// still usable, admin can retry). On self-insert failure we delete the empty
|
|
// conversation to avoid orphans.
|
|
export async function createGroup(params: CreateGroupParams): Promise<string> {
|
|
const myId = await currentUserId(params.client);
|
|
const trimmed = params.name.trim();
|
|
if (trimmed.length === 0) throw new Error('group name required');
|
|
|
|
// Generate the uuid client-side so we don't need a post-insert SELECT on
|
|
// conversations — the SELECT policy requires membership, which only exists
|
|
// AFTER we insert the creator's member row in step 2.
|
|
const conversationId = crypto.randomUUID();
|
|
|
|
const { error: cErr } = await params.client
|
|
.from('conversations')
|
|
.insert({
|
|
id: conversationId,
|
|
type: 'group',
|
|
name: trimmed,
|
|
created_by: myId,
|
|
} as never);
|
|
if (cErr) throw cErr;
|
|
|
|
const { error: sErr } = await params.client.from('conversation_members').insert({
|
|
conversation_id: conversationId,
|
|
user_id: myId,
|
|
role: 'admin',
|
|
accepted: true,
|
|
} as never);
|
|
if (sErr) {
|
|
await params.client.from('conversations').delete().eq('id', conversationId);
|
|
throw sErr;
|
|
}
|
|
|
|
const peerIds = Array.from(new Set(params.memberUserIds)).filter((id) => id !== myId);
|
|
if (peerIds.length > 0) {
|
|
const rows = peerIds.map((user_id) => ({
|
|
conversation_id: conversationId,
|
|
user_id,
|
|
role: 'member' as MemberRole,
|
|
accepted: true,
|
|
}));
|
|
const { error: mErr } = await params.client
|
|
.from('conversation_members')
|
|
.insert(rows as never);
|
|
if (mErr) throw mErr;
|
|
}
|
|
|
|
return conversationId;
|
|
}
|
|
|
|
// Add an accepted friend to a group. RLS allows the insert only when the
|
|
// caller is admin/mod of the conversation AND is friends with `userId`.
|
|
export async function addGroupMember(
|
|
client: AppSupabaseClient,
|
|
conversationId: string,
|
|
userId: string,
|
|
): Promise<void> {
|
|
const { error } = await client.from('conversation_members').insert({
|
|
conversation_id: conversationId,
|
|
user_id: userId,
|
|
role: 'member',
|
|
accepted: true,
|
|
} as never);
|
|
if (error) throw error;
|
|
}
|
|
|
|
export async function leaveGroup(
|
|
client: AppSupabaseClient,
|
|
conversationId: string,
|
|
): Promise<void> {
|
|
const myId = await currentUserId(client);
|
|
const { error } = await client
|
|
.from('conversation_members')
|
|
.delete()
|
|
.eq('conversation_id', conversationId)
|
|
.eq('user_id', myId);
|
|
if (error) throw error;
|
|
}
|