@@ -12,6 +12,25 @@ export interface ChatRoom {
1212 created_by ?: string
1313 created_at : string
1414 updated_at : string
15+ last_message ?: Message
16+ members ?: RoomMember [ ]
17+ }
18+
19+ export interface RoomMember {
20+ room_id : string
21+ user_id : string
22+ user_email : string
23+ user_name ?: string
24+ joined_at : string
25+ last_read_at ?: string
26+ }
27+
28+ export interface UserPresence {
29+ user_id : string
30+ email : string
31+ name ?: string
32+ status : 'online' | 'offline' | 'away'
33+ last_seen : string
1534}
1635
1736export async function createChatRoom ( room : Omit < ChatRoom , 'id' | 'created_at' | 'updated_at' > ) {
@@ -26,6 +45,71 @@ export async function createChatRoom(room: Omit<ChatRoom, 'id' | 'created_at' |
2645 return data
2746}
2847
48+ // Create or get direct chat room between two users
49+ export async function getOrCreateDirectChat ( userId1 : string , userId2 : string ) {
50+ const supabase = createClient ( )
51+
52+ // Check if direct chat already exists between these users
53+ const { data : existingRooms , error : searchError } = await supabase
54+ . from ( 'chat_room_members' )
55+ . select ( 'room_id, chat_rooms!inner(id, type, name)' )
56+ . eq ( 'user_id' , userId1 )
57+
58+ if ( searchError ) throw searchError
59+
60+ // Find a direct chat that includes both users
61+ for ( const roomData of existingRooms || [ ] ) {
62+ const { data : members } = await supabase
63+ . from ( 'chat_room_members' )
64+ . select ( 'user_id' )
65+ . eq ( 'room_id' , roomData . room_id )
66+
67+ const memberIds = members ?. map ( m => m . user_id ) || [ ]
68+
69+ // Check if this is a direct chat with exactly these two users
70+ if ( memberIds . length === 2 &&
71+ memberIds . includes ( userId1 ) &&
72+ memberIds . includes ( userId2 ) &&
73+ ( roomData . chat_rooms as any ) . type === 'direct' ) {
74+ // Fetch full room data
75+ const { data : room } = await supabase
76+ . from ( 'chat_rooms' )
77+ . select ( '*' )
78+ . eq ( 'id' , roomData . room_id )
79+ . single ( )
80+
81+ return room
82+ }
83+ }
84+
85+ // No existing direct chat, create one
86+ const { data : user2Data } = await supabase
87+ . from ( 'users' )
88+ . select ( 'email, name' )
89+ . eq ( 'id' , userId2 )
90+ . single ( )
91+
92+ const chatName = user2Data ?. name || user2Data ?. email || 'Direct Chat'
93+
94+ const { data : newRoom , error : createError } = await supabase
95+ . from ( 'chat_rooms' )
96+ . insert ( {
97+ name : chatName ,
98+ type : 'direct' ,
99+ created_by : userId1
100+ } )
101+ . select ( )
102+ . single ( )
103+
104+ if ( createError ) throw createError
105+
106+ // Add both users as members
107+ await addRoomMember ( newRoom . id , userId1 )
108+ await addRoomMember ( newRoom . id , userId2 )
109+
110+ return newRoom
111+ }
112+
29113export async function getChatRooms ( userId : string ) {
30114 const supabase = createClient ( )
31115
@@ -43,12 +127,34 @@ export async function getChatRooms(userId: string) {
43127
44128 const { data : rooms , error : roomsError } = await supabase
45129 . from ( 'chat_rooms' )
46- . select ( '*' )
130+ . select ( `
131+ *,
132+ members:chat_room_members(user_id, user_email, user_name, joined_at, last_read_at)
133+ ` )
47134 . in ( 'id' , roomIds )
48135 . order ( 'updated_at' , { ascending : false } )
49136
50137 if ( roomsError ) throw roomsError
51- return rooms || [ ]
138+
139+ // Get last message for each room
140+ const roomsWithMessages = await Promise . all (
141+ ( rooms || [ ] ) . map ( async ( room ) => {
142+ const { data : lastMsg } = await supabase
143+ . from ( 'messages' )
144+ . select ( '*' )
145+ . eq ( 'room_id' , room . id )
146+ . order ( 'created_at' , { ascending : false } )
147+ . limit ( 1 )
148+ . single ( )
149+
150+ return {
151+ ...room ,
152+ last_message : lastMsg || undefined
153+ }
154+ } )
155+ )
156+
157+ return roomsWithMessages
52158}
53159
54160export async function addRoomMember ( roomId : string , userId : string ) {
@@ -225,6 +331,141 @@ export function subscribeToMessages(roomId: string, callback: (message: Message)
225331 }
226332}
227333
334+ // Subscribe to message updates (edits/deletes)
335+ export function subscribeToMessageUpdates ( roomId : string , onUpdate : ( message : Message ) => void , onDelete : ( messageId : string ) => void ) {
336+ const supabase = createClient ( )
337+
338+ const subscription = supabase
339+ . channel ( `room_updates:${ roomId } ` )
340+ . on (
341+ 'postgres_changes' ,
342+ {
343+ event : 'UPDATE' ,
344+ schema : 'public' ,
345+ table : 'messages' ,
346+ filter : `room_id=eq.${ roomId } `
347+ } ,
348+ ( payload ) => {
349+ onUpdate ( payload . new as Message )
350+ }
351+ )
352+ . on (
353+ 'postgres_changes' ,
354+ {
355+ event : 'DELETE' ,
356+ schema : 'public' ,
357+ table : 'messages' ,
358+ filter : `room_id=eq.${ roomId } `
359+ } ,
360+ ( payload ) => {
361+ onDelete ( payload . old . id )
362+ }
363+ )
364+ . subscribe ( )
365+
366+ return ( ) => {
367+ subscription . unsubscribe ( )
368+ }
369+ }
370+
371+ // ============================================
372+ // USER PRESENCE
373+ // ============================================
374+
375+ export async function updateUserPresence ( userId : string , email : string , name : string | undefined , status : 'online' | 'offline' | 'away' ) {
376+ const supabase = createClient ( )
377+
378+ const { data, error } = await supabase
379+ . from ( 'user_presence' )
380+ . upsert ( {
381+ user_id : userId ,
382+ email,
383+ name,
384+ status,
385+ last_seen : new Date ( ) . toISOString ( )
386+ } )
387+ . select ( )
388+ . single ( )
389+
390+ if ( error ) throw error
391+ return data
392+ }
393+
394+ export async function getUserPresence ( userId : string ) : Promise < UserPresence | null > {
395+ const supabase = createClient ( )
396+
397+ const { data, error } = await supabase
398+ . from ( 'user_presence' )
399+ . select ( '*' )
400+ . eq ( 'user_id' , userId )
401+ . single ( )
402+
403+ if ( error ) return null
404+ return data
405+ }
406+
407+ export async function getAllUsersPresence ( ) : Promise < UserPresence [ ] > {
408+ const supabase = createClient ( )
409+
410+ const { data, error } = await supabase
411+ . from ( 'user_presence' )
412+ . select ( '*' )
413+ . order ( 'email' , { ascending : true } )
414+
415+ if ( error ) return [ ]
416+ return data || [ ]
417+ }
418+
419+ export async function getCollaboratorsPresence ( userIds : string [ ] ) : Promise < UserPresence [ ] > {
420+ const supabase = createClient ( )
421+
422+ if ( userIds . length === 0 ) return [ ]
423+
424+ const { data, error } = await supabase
425+ . from ( 'user_presence' )
426+ . select ( '*' )
427+ . in ( 'user_id' , userIds )
428+
429+ if ( error ) return [ ]
430+ return data || [ ]
431+ }
432+
433+ // Subscribe to presence changes
434+ export function subscribeToPresence ( callback : ( presence : UserPresence ) => void ) {
435+ const supabase = createClient ( )
436+
437+ const subscription = supabase
438+ . channel ( 'user_presence_changes' )
439+ . on (
440+ 'postgres_changes' ,
441+ {
442+ event : '*' ,
443+ schema : 'public' ,
444+ table : 'user_presence'
445+ } ,
446+ ( payload ) => {
447+ if ( payload . eventType === 'INSERT' || payload . eventType === 'UPDATE' ) {
448+ callback ( payload . new as UserPresence )
449+ }
450+ }
451+ )
452+ . subscribe ( )
453+
454+ return ( ) => {
455+ subscription . unsubscribe ( )
456+ }
457+ }
458+
459+ // Set user as online when they join
460+ export async function setUserOnline ( userId : string , email : string , name ?: string ) {
461+ return updateUserPresence ( userId , email , name , 'online' )
462+ }
463+
464+ // Set user as offline when they leave
465+ export async function setUserOffline ( userId : string , email : string , name ?: string ) {
466+ return updateUserPresence ( userId , email , name , 'offline' )
467+ }
468+
228469// ============================================
229470// COMMENTS
230471// ============================================
0 commit comments