94 lines
2.4 KiB
TypeScript
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;
|
|
}
|