/** * useChatChannel — Find or create a default public chat channel. * * Encapsulates default-channel discovery so consuming components never need * to know about the channels collection. On first mount, a member or admin * creates the "general" public channel when none exists. Channel membership * remains optional and is not used as a read-access gate. * * Usage: * const { channelId, status } = useChatChannel() * if (!channelId) return * return */ import { useEffect, useRef } from 'react' import { useUser, isWriterRole } from 'deepspace' import { useChannels } from 'deepspace' import type { Channel } from 'deepspace' import type { RecordData } from 'deepspace' const DEFAULT_CHANNEL_NAME = 'general' export function useChatChannel(channelName: string = DEFAULT_CHANNEL_NAME) { const { user } = useUser() const { channels, status, create } = useChannels() // Keyed by channel name, not a boolean: the same mounted hook can be // repointed at another channel, which needs its own one-shot create. const initializedFor = useRef(null) const canCreateChannel = isWriterRole(user?.role) // Nothing enforces name uniqueness server-side, so two first visitors can // race the one-shot init below and BOTH create the default channel. With a // bare .find(), each client then resolves whichever duplicate its local // snapshot orders first — split-brain: A and B post into different rooms // and never see each other. Pick deterministically (oldest createdAt, // recordId tie-break) so every client converges on the same channel once // the duplicates sync. const defaultChannel = channels .filter( (c: RecordData) => c.data.name === channelName && c.data.type === 'public' && !c.data.archived, ) .sort( (a: RecordData, b: RecordData) => a.createdAt.localeCompare(b.createdAt) || a.recordId.localeCompare(b.recordId), )[0] // One-shot initialization: create the default channel if it doesn't exist. useEffect(() => { if (!canCreateChannel || status !== 'ready' || initializedFor.current === channelName) return initializedFor.current = channelName if (!defaultChannel) { void create({ name: channelName, description: 'Default chat channel', }).catch(() => { // A not-ready write already reaches RecordProvider.onWriteError. Allow // a later ready render to retry instead of leaving initialization stuck. initializedFor.current = null }) } }, [canCreateChannel, status, defaultChannel, create, channelName]) return { channelId: defaultChannel?.recordId, status, } }