'use client' import { useState, useEffect, useRef } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { createClient } from '@/lib/supabase/client' import { Hash, Users2, Send, ChevronLeft, Plus } from 'lucide-react' import { createChannel } from '@/lib/actions' import type { Tables } from '@/types/database' import { PageTitle } from '@/components/ui/page-title' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' type Channel = Tables<'comms_channels'> type Message = Tables<'comms_messages'> interface Props { member: Tables<'space_members'> space: Tables<'spaces'> channels: Channel[] } export default function CommsClient({ member, space, channels }: Props) { const router = useRouter() const [selectedChannel, setSelectedChannel] = useState(channels[0] || null) const [messages, setMessages] = useState([]) const [newMessage, setNewMessage] = useState('') const [showNewChannel, setShowNewChannel] = useState(false) const [newChannelName, setNewChannelName] = useState('') const [newChannelDescription, setNewChannelDescription] = useState('') const [newChannelType, setNewChannelType] = useState<'general' | 'area' | 'project'>('general') const [creatingChannel, setCreatingChannel] = useState(false) async function handleCreateChannel(e: React.FormEvent) { e.preventDefault() if (!newChannelName.trim()) return setCreatingChannel(true) const result = await createChannel({ name: newChannelName.trim().toLowerCase(), description: newChannelDescription.trim() || undefined, channel_type: newChannelType, }) setCreatingChannel(false) if ('error' in result && result.error) { toast.error(result.error); return } toast.success('Channel created') setNewChannelName('') setNewChannelDescription('') setShowNewChannel(false) router.refresh() } const [sending, setSending] = useState(false) const [showChannelList, setShowChannelList] = useState(true) const messagesEndRef = useRef(null) useEffect(() => { if (!selectedChannel) return loadMessages() const supabase = createClient() const subscription = supabase .channel(`channel:${selectedChannel.id}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'comms_messages', filter: `channel_id=eq.${selectedChannel.id}`, }, payload => { setMessages(prev => { // Dedup: don't add if we already have a message with this id // (e.g. from the optimistic update that was then confirmed) if (prev.some(m => m.id === payload.new.id)) return prev // Also replace any temp message from same user sent at ~same time const hasTempForThisUser = prev.some( m => typeof m.id === 'string' && m.id.startsWith('temp-') && m.user_id === payload.new.user_id && m.content === payload.new.content, ) if (hasTempForThisUser) { return prev.map(m => typeof m.id === 'string' && m.id.startsWith('temp-') && m.user_id === payload.new.user_id && m.content === payload.new.content ? payload.new : m, ) } return [...prev, payload.new] }) }, ) .subscribe() return () => { subscription.unsubscribe() } }, [selectedChannel]) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) async function loadMessages() { const supabase = createClient() const { data } = await supabase .from('comms_messages') .select('*') .eq('channel_id', selectedChannel.id) .order('created_at') .limit(100) setMessages(data ?? []) } async function sendMessage(e: React.FormEvent) { e.preventDefault() const content = newMessage.trim() if (!content || sending) return setSending(true) // Optimistic insert โ€” message appears immediately for the sender const tempId = `temp-${Date.now()}` const optimisticMsg = { id: tempId, channel_id: selectedChannel.id, space_id: space?.id ?? member.space_id, user_id: member.user_id, display_name: member.display_name, handle: member.handle, content, created_at: new Date().toISOString(), } setMessages(prev => [...prev, optimisticMsg]) setNewMessage('') const supabase = createClient() const { data, error } = await supabase .from('comms_messages') .insert({ channel_id: selectedChannel.id, space_id: space?.id ?? member.space_id, user_id: member.user_id, display_name: member.display_name, handle: member.handle, content, }) .select() .single() if (error) { // Roll back optimistic message on failure setMessages(prev => prev.filter(m => m.id !== tempId)) setNewMessage(content) } else if (data) { // Replace temp with confirmed message (realtime may also fire โ€” dedup handles it) setMessages(prev => prev.map(m => (m.id === tempId ? data : m))) } setSending(false) } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() sendMessage(e as unknown as React.FormEvent) } } const generalChannels = channels.filter(c => c.channel_type === 'general') const areaChannels = channels.filter(c => c.channel_type === 'area') const projectChannels = channels.filter(c => c.channel_type === 'project') return (
{/* Mobile back to channels */} {!showChannelList && selectedChannel && ( )} {!showChannelList && selectedChannel ? `#${selectedChannel.name}` : 'Comms'}
{/* Channels sidebar โ€” hidden on mobile when a channel is selected */} {/* Main chat area */} {selectedChannel ? (
{/* Channel header */}

{selectedChannel.name}

{selectedChannel.description && (

{selectedChannel.description}

)}
{selectedChannel.member_count && ( {selectedChannel.member_count} members )}
{/* Messages */}
{messages.length === 0 && (
No messages yet Be the first to post in #{selectedChannel.name}.
)} {messages.map(msg => { const isMe = msg.user_id === member.user_id const isTemp = typeof msg.id === 'string' && msg.id.startsWith('temp-') return (
{(msg.display_name || msg.handle || '?').slice(0, 2).toUpperCase()}
{!isMe && ( {msg.display_name || msg.handle} )} {new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {isTemp && ' ยท sending...'}
{msg.content}
) })}
{/* Message input */}
setNewMessage(e.target.value)} onKeyDown={handleKeyDown} placeholder={`Message #${selectedChannel.name}`} className="flex-1 bg-background border border-border rounded-full px-4 py-2 font-sans text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition" disabled={sending} />

Press Enter to send

) : (
{channels.length === 0 ? 'No channels yet' : 'No channel selected'} {channels.length === 0 ? 'Create a channel from the sidebar to start messaging.' : 'Pick a channel from the sidebar to start messaging.'}
)}
) }