chahinebrini 35a71a9068 feat(presence): Online-Presence-Provider + Hooks
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:09:42 +02:00

94 lines
2.4 KiB
TypeScript

import { createContext, useContext, useEffect, useRef, useState } from 'react';
import type { RealtimeChannel } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
type OnlinePresenceContext = {
onlineUserIds: Set<string>;
isOnline: (userId: string) => boolean;
};
export const OnlinePresenceContext = createContext<OnlinePresenceContext>({
onlineUserIds: new Set(),
isOnline: () => false,
});
export function useOnlineUsers(): OnlinePresenceContext {
return useContext(OnlinePresenceContext);
}
let sharedChannel: RealtimeChannel | null = null;
let subscriberCount = 0;
let onlineUserIds: Set<string> = new Set();
const listeners = new Set<(ids: Set<string>) => void>();
function notify() {
const snapshot = new Set(onlineUserIds);
listeners.forEach((fn) => fn(snapshot));
}
function ensureChannel(currentUserId: string) {
if (sharedChannel) return;
const ch = supabase.channel('presence:online', {
config: { presence: { key: currentUserId } },
});
sharedChannel = ch;
ch
.on('presence', { event: 'sync' }, () => {
const state = ch.presenceState();
const keys = Object.keys(state);
onlineUserIds = new Set(keys);
notify();
})
.subscribe(async (status: string) => {
if (status === 'SUBSCRIBED') {
await ch.track({ userId: currentUserId, online_at: new Date().toISOString() });
}
});
}
function teardownChannel() {
if (!sharedChannel) return;
sharedChannel.untrack().catch(() => {});
supabase.removeChannel(sharedChannel);
sharedChannel = null;
onlineUserIds = new Set();
notify();
}
export function untrackSelf() {
sharedChannel?.untrack().catch(() => {});
}
export function retrackSelf(currentUserId: string) {
sharedChannel
?.track({ userId: currentUserId, online_at: new Date().toISOString() })
.catch(() => {});
}
export function useOnlinePresenceNode(currentUserId: string | null | undefined) {
const [ids, setIds] = useState<Set<string>>(new Set(onlineUserIds));
useEffect(() => {
if (!currentUserId) return;
subscriberCount++;
ensureChannel(currentUserId);
const listener = (next: Set<string>) => setIds(next);
listeners.add(listener);
return () => {
listeners.delete(listener);
subscriberCount--;
if (subscriberCount <= 0) {
subscriberCount = 0;
teardownChannel();
}
};
}, [currentUserId]);
return ids;
}