ChannelList.tsx raw
1 import { useChat } from '@/providers/ChatProvider'
2 import { useSecondaryPage } from '@/PageManager'
3 import { toChatChannel } from '@/lib/link'
4 import { cn } from '@/lib/utils'
5 import { Hash, Plus, Loader2, RefreshCw, Lock, BellOff } from 'lucide-react'
6 import { useState } from 'react'
7 import { Button } from '../ui/button'
8 import CreateChannelDialog from './CreateChannelDialog'
9
10 export default function ChannelList() {
11 const {
12 channels,
13 currentChannel,
14 isLoadingChannels,
15 refreshChannels,
16 unreadCounts,
17 mutedChannels
18 } = useChat()
19 const { push, pop } = useSecondaryPage()
20 const [showCreate, setShowCreate] = useState(false)
21
22 return (
23 <div className="flex flex-col h-full">
24 <div className="flex items-center justify-between px-3 py-2 border-b">
25 <span className="text-sm font-semibold">Channels</span>
26 <div className="flex gap-1">
27 <Button
28 variant="ghost"
29 size="icon"
30 className="size-7"
31 onClick={() => refreshChannels()}
32 title="Refresh"
33 >
34 <RefreshCw className="size-3.5" />
35 </Button>
36 <Button
37 variant="ghost"
38 size="icon"
39 className="size-7"
40 onClick={() => setShowCreate(true)}
41 title="Create channel"
42 >
43 <Plus className="size-3.5" />
44 </Button>
45 </div>
46 </div>
47
48 <div className="flex-1 overflow-y-auto">
49 {isLoadingChannels ? (
50 <div className="flex justify-center py-8">
51 <Loader2 className="size-5 animate-spin text-muted-foreground" />
52 </div>
53 ) : channels.length === 0 ? (
54 <div className="px-3 py-8 text-center text-sm text-muted-foreground">
55 No channels yet
56 </div>
57 ) : (
58 channels.map((ch) => {
59 const unread = unreadCounts[ch.id] || 0
60 const isMuted = mutedChannels.has(ch.id)
61 return (
62 <button
63 key={ch.id}
64 onClick={() => {
65 if (currentChannel && currentChannel.id !== ch.id) {
66 pop()
67 }
68 push(toChatChannel(ch.id))
69 }}
70 className={cn(
71 'flex items-center gap-2 w-full px-3 py-2 text-left text-sm transition-colors hover:bg-accent',
72 currentChannel?.id === ch.id && 'bg-accent text-accent-foreground'
73 )}
74 >
75 <Hash className="size-4 flex-shrink-0 text-muted-foreground" />
76 <div className="min-w-0 flex-1">
77 <div className="flex items-center gap-1">
78 <span className={cn('truncate font-medium', unread > 0 && !isMuted && 'font-bold')}>
79 {ch.name}
80 </span>
81 {ch.accessMode !== 'open' && <Lock className="size-3 text-muted-foreground flex-shrink-0" />}
82 {isMuted && <BellOff className="size-3 text-muted-foreground flex-shrink-0" />}
83 </div>
84 {ch.about && (
85 <div className="truncate text-xs text-muted-foreground">{ch.about}</div>
86 )}
87 </div>
88 {unread > 0 && !isMuted && (
89 <span className="inline-flex items-center justify-center size-5 text-xs rounded-full bg-primary text-primary-foreground flex-shrink-0">
90 {unread > 99 ? '99+' : unread}
91 </span>
92 )}
93 </button>
94 )
95 })
96 )}
97 </div>
98
99 <CreateChannelDialog open={showCreate} onOpenChange={setShowCreate} />
100 </div>
101 )
102 }
103