ChatProvider.tsx raw
1 import chatService, {
2 TAccessMode,
3 TChannel,
4 TChannelMessage,
5 TMemberEntry
6 } from '@/services/chat.service'
7 import client from '@/services/client.service'
8 import { useNostr } from '@/providers/NostrProvider'
9 import {
10 createContext,
11 useCallback,
12 useContext,
13 useEffect,
14 useMemo,
15 useRef,
16 useState
17 } from 'react'
18
19 // --- localStorage helpers ---
20
21 function loadJsonMap(key: string): Record<string, number> {
22 try {
23 return JSON.parse(localStorage.getItem(key) || '{}')
24 } catch {
25 return {}
26 }
27 }
28
29 function saveJsonMap(key: string, map: Record<string, number>) {
30 localStorage.setItem(key, JSON.stringify(map))
31 }
32
33 function loadStringSet(key: string): Set<string> {
34 try {
35 return new Set(JSON.parse(localStorage.getItem(key) || '[]'))
36 } catch {
37 return new Set()
38 }
39 }
40
41 function saveStringSet(key: string, set: Set<string>) {
42 localStorage.setItem(key, JSON.stringify([...set]))
43 }
44
45 // --- Types ---
46
47 type TChatContext = {
48 // Channel state
49 channels: TChannel[]
50 currentChannel: TChannel | null
51 messages: TChannelMessage[]
52 isLoadingChannels: boolean
53 isLoadingMessages: boolean
54 relayUrl: string
55 setRelayUrl: (url: string) => void
56 selectChannel: (channel: TChannel | null) => void
57 selectChannelById: (channelId: string | null) => void
58 sendMessage: (content: string) => Promise<void>
59 createChannel: (name: string, about: string, accessMode?: TAccessMode) => Promise<void>
60 refreshChannels: () => Promise<void>
61 loadMoreMessages: () => Promise<void>
62 // Notifications
63 unreadCounts: Record<string, number>
64 hasUnreadChannels: boolean
65 mutedChannels: Set<string>
66 markChannelAsSeen: (channelId: string) => void
67 toggleMuteChannel: (channelId: string) => void
68 // Moderation
69 channelMods: string[]
70 channelMembers: TMemberEntry[]
71 channelBlocked: TMemberEntry[]
72 channelInvited: TMemberEntry[]
73 channelRequested: string[]
74 channelRejected: string[]
75 channelAccessMode: TAccessMode
76 hiddenMessages: Set<string>
77 isOwnerOrMod: boolean
78 isMember: boolean
79 addMod: (pubkey: string) => Promise<void>
80 removeMod: (pubkey: string) => Promise<void>
81 approveMember: (pubkey: string) => Promise<void>
82 removeMember: (pubkey: string) => Promise<void>
83 hideMessage: (messageId: string) => Promise<void>
84 blockUser: (pubkey: string) => Promise<void>
85 unblockUser: (pubkey: string) => Promise<void>
86 updateAccessMode: (mode: TAccessMode) => Promise<void>
87 updateMessageExpiry: (expirySecs: number) => Promise<void>
88 sendInvite: (pubkey: string) => Promise<void>
89 revokeInvite: (pubkey: string) => Promise<void>
90 acceptRequest: (pubkey: string) => Promise<void>
91 rejectRequest: (pubkey: string) => Promise<void>
92 revokeRejection: (pubkey: string) => Promise<void>
93 // Participants (for @ mentions and member list)
94 channelParticipants: string[]
95 }
96
97 const ChatContext = createContext<TChatContext | undefined>(undefined)
98
99 export function useChat() {
100 const ctx = useContext(ChatContext)
101 if (!ctx) throw new Error('useChat must be used within ChatProvider')
102 return ctx
103 }
104
105 const DEFAULT_RELAY = 'wss://relay.orly.dev/'
106
107 export function ChatProvider({ children }: { children: React.ReactNode }) {
108 const { pubkey, signEvent } = useNostr()
109 const [relayUrl, setRelayUrl] = useState(DEFAULT_RELAY)
110 const [channels, setChannels] = useState<TChannel[]>([])
111 const [currentChannel, setCurrentChannel] = useState<TChannel | null>(null)
112 const [messages, setMessages] = useState<TChannelMessage[]>([])
113 const [isLoadingChannels, setIsLoadingChannels] = useState(false)
114 const [isLoadingMessages, setIsLoadingMessages] = useState(false)
115 const subCloserRef = useRef<{ close: () => void } | null>(null)
116 const seenIdsRef = useRef(new Set<string>())
117
118 // Notification state
119 const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({})
120 const [mutedChannels, setMutedChannels] = useState<Set<string>>(new Set())
121 const [, setLastSeenTimestamps] = useState<Record<string, number>>({})
122 const currentChannelRef = useRef<TChannel | null>(null)
123
124 // Moderation state (for current channel)
125 const [channelMods, setChannelMods] = useState<string[]>([])
126 const [channelMembers, setChannelMembers] = useState<TMemberEntry[]>([])
127 const [channelBlocked, setChannelBlocked] = useState<TMemberEntry[]>([])
128 const [channelInvited, setChannelInvited] = useState<TMemberEntry[]>([])
129 const [channelRequested, setChannelRequested] = useState<string[]>([])
130 const [channelRejected, setChannelRejected] = useState<string[]>([])
131 const [channelAccessMode, setChannelAccessMode] = useState<TAccessMode>('whitelist')
132 const [hiddenMessages, setHiddenMessages] = useState<Set<string>>(new Set())
133
134 // Keep ref in sync
135 useEffect(() => {
136 currentChannelRef.current = currentChannel
137 }, [currentChannel])
138
139 // Load notification prefs from localStorage on login
140 useEffect(() => {
141 if (!pubkey) return
142 loadJsonMap(`nirc:lastSeen:${pubkey}`)
143 setMutedChannels(loadStringSet(`nirc:muted:${pubkey}`))
144 }, [pubkey])
145
146 const isOwnerOrMod = useMemo(() => {
147 if (!pubkey || !currentChannel) return false
148 if (currentChannel.creator === pubkey) return true
149 return channelMods.includes(pubkey)
150 }, [pubkey, currentChannel, channelMods])
151
152 const isMember = useMemo(() => {
153 if (!pubkey || !currentChannel) return false
154 if (channelAccessMode === 'open') return true
155 if (currentChannel.creator === pubkey) return true
156 if (channelMods.includes(pubkey)) return true
157 if (channelAccessMode === 'whitelist') {
158 return (
159 channelMembers.some((m) => m.pubkey === pubkey) ||
160 channelInvited.some((m) => m.pubkey === pubkey)
161 )
162 }
163 if (channelAccessMode === 'blacklist') {
164 return !channelBlocked.some((m) => m.pubkey === pubkey)
165 }
166 return false
167 }, [pubkey, currentChannel, channelAccessMode, channelMods, channelMembers, channelInvited, channelBlocked])
168
169 // Collect unique participants from messages + member list for @ mentions
170 const channelParticipants = useMemo(() => {
171 const pks = new Set<string>()
172 for (const msg of messages) pks.add(msg.pubkey)
173 for (const m of channelMembers) pks.add(m.pubkey)
174 for (const m of channelInvited) pks.add(m.pubkey)
175 for (const pk of channelMods) pks.add(pk)
176 if (currentChannel) pks.add(currentChannel.creator)
177 return [...pks]
178 }, [messages, channelMembers, channelInvited, channelMods, currentChannel])
179
180 const hasUnreadChannels = useMemo(() => {
181 return Object.entries(unreadCounts).some(
182 ([chId, count]) => count > 0 && !mutedChannels.has(chId)
183 )
184 }, [unreadCounts, mutedChannels])
185
186 const markChannelAsSeen = useCallback(
187 (channelId: string) => {
188 setUnreadCounts((prev) => {
189 if (!prev[channelId]) return prev
190 const next = { ...prev }
191 delete next[channelId]
192 return next
193 })
194 const now = Math.floor(Date.now() / 1000)
195 setLastSeenTimestamps((prev) => {
196 const next = { ...prev, [channelId]: now }
197 if (pubkey) saveJsonMap(`nirc:lastSeen:${pubkey}`, next)
198 return next
199 })
200 },
201 [pubkey]
202 )
203
204 const toggleMuteChannel = useCallback(
205 (channelId: string) => {
206 setMutedChannels((prev) => {
207 const next = new Set(prev)
208 if (next.has(channelId)) {
209 next.delete(channelId)
210 } else {
211 next.add(channelId)
212 }
213 if (pubkey) saveStringSet(`nirc:muted:${pubkey}`, next)
214 return next
215 })
216 },
217 [pubkey]
218 )
219
220 const refreshChannels = useCallback(async () => {
221 setIsLoadingChannels(true)
222 try {
223 const chs = await chatService.fetchChannels(relayUrl)
224 setChannels(chs)
225 } finally {
226 setIsLoadingChannels(false)
227 }
228 }, [relayUrl])
229
230 // Load channels on mount and relay change
231 useEffect(() => {
232 refreshChannels()
233 }, [refreshChannels])
234
235 // Fetch moderation state for a channel
236 const loadModState = useCallback(
237 async (channel: TChannel) => {
238 const meta = await chatService.fetchChannelMeta(relayUrl, channel.id)
239 const ownerPk = channel.creator
240 let mods: string[] = []
241 let members: TMemberEntry[] = []
242 let blocked: TMemberEntry[] = []
243 let invited: TMemberEntry[] = []
244 let requested: string[] = []
245 let rejected: string[] = []
246 let accessMode: TAccessMode = channel.accessMode
247 if (meta) {
248 mods = meta.mods
249 members = meta.members
250 blocked = meta.blocked
251 invited = meta.invited
252 requested = meta.requested
253 rejected = meta.rejected
254 accessMode = meta.accessMode
255 // Update channel's accessMode and messageExpiry from latest metadata
256 channel.accessMode = accessMode
257 if (meta.messageExpiry !== undefined) {
258 channel.messageExpiry = meta.messageExpiry
259 }
260 }
261 // Owner is always a mod
262 if (!mods.includes(ownerPk)) mods = [ownerPk, ...mods]
263 setChannelMods(mods)
264 setChannelMembers(members)
265 setChannelBlocked(blocked)
266 setChannelInvited(invited)
267 setChannelRequested(requested)
268 setChannelRejected(rejected)
269 setChannelAccessMode(accessMode)
270
271 // Fetch hidden messages and blocked users from mod actions
272 const allMods = mods
273 const hidden = await chatService.fetchHiddenMessageIds(relayUrl, channel.id, allMods)
274 setHiddenMessages(hidden)
275
276 const blockedFromActions = await chatService.fetchBlockedUsers(relayUrl, channel.id, allMods)
277 if (blockedFromActions.size > 0) {
278 setChannelBlocked((prev) => {
279 const existingPks = new Set(prev.map((e) => e.pubkey))
280 const newEntries = [...blockedFromActions]
281 .filter((pk) => !existingPks.has(pk))
282 .map((pk) => ({ pubkey: pk, addedBy: '' }))
283 return [...prev, ...newEntries]
284 })
285 }
286 },
287 [relayUrl]
288 )
289
290 const selectChannel = useCallback(
291 async (channel: TChannel | null) => {
292 subCloserRef.current?.close()
293 subCloserRef.current = null
294 seenIdsRef.current.clear()
295
296 setCurrentChannel(channel)
297 setMessages([])
298 setChannelMods([])
299 setChannelMembers([])
300 setChannelBlocked([])
301 setChannelInvited([])
302 setChannelRequested([])
303 setChannelRejected([])
304 setChannelAccessMode('whitelist')
305 setHiddenMessages(new Set())
306
307 if (!channel) return
308
309 markChannelAsSeen(channel.id)
310
311 setIsLoadingMessages(true)
312 try {
313 const [msgs] = await Promise.all([
314 chatService.fetchMessages(relayUrl, channel.id),
315 loadModState(channel)
316 ])
317 setMessages(msgs)
318 msgs.forEach((m) => seenIdsRef.current.add(m.id))
319 } finally {
320 setIsLoadingMessages(false)
321 }
322
323 subCloserRef.current = chatService.subscribeMessages(
324 relayUrl,
325 channel.id,
326 (msg) => {
327 if (seenIdsRef.current.has(msg.id)) return
328 seenIdsRef.current.add(msg.id)
329 setMessages((prev) => [...prev, msg])
330 }
331 )
332 },
333 [relayUrl, markChannelAsSeen, loadModState]
334 )
335
336 const pendingChannelIdRef = useRef<string | null>(null)
337
338 const selectChannelById = useCallback(
339 (channelId: string | null) => {
340 if (!channelId) {
341 pendingChannelIdRef.current = null
342 selectChannel(null)
343 return
344 }
345 const ch = channels.find((c) => c.id === channelId)
346 if (ch) {
347 pendingChannelIdRef.current = null
348 selectChannel(ch)
349 } else {
350 pendingChannelIdRef.current = channelId
351 }
352 },
353 [channels, selectChannel]
354 )
355
356 useEffect(() => {
357 if (pendingChannelIdRef.current && channels.length > 0) {
358 const ch = channels.find((c) => c.id === pendingChannelIdRef.current)
359 if (ch) {
360 pendingChannelIdRef.current = null
361 selectChannel(ch)
362 }
363 }
364 }, [channels, selectChannel])
365
366 // Cleanup subscription on unmount
367 useEffect(() => {
368 return () => {
369 subCloserRef.current?.close()
370 }
371 }, [])
372
373 // Global subscription for unread tracking across all channels
374 useEffect(() => {
375 if (!pubkey || channels.length === 0) return
376
377 const channelIds = channels.map((ch) => ch.id)
378
379 const globalSub = client.subscribe(
380 [relayUrl],
381 {
382 kinds: [42],
383 '#e': channelIds,
384 since: Math.floor(Date.now() / 1000)
385 },
386 {
387 onevent: (event: any) => {
388 if (event.pubkey === pubkey) return
389 const eTag = event.tags?.find(
390 (t: string[]) => t[0] === 'e' && (t[3] === 'root' || t.length === 2)
391 )
392 if (!eTag) return
393 const chId = eTag[1]
394 if (currentChannelRef.current?.id === chId) return
395 if (mutedChannels.has(chId)) return
396
397 setUnreadCounts((prev) => ({
398 ...prev,
399 [chId]: (prev[chId] || 0) + 1
400 }))
401 }
402 }
403 )
404
405 return () => {
406 globalSub.close()
407 }
408 }, [pubkey, channels, relayUrl, mutedChannels])
409
410 const sendMessage = useCallback(
411 async (content: string) => {
412 if (!currentChannel || !pubkey) return
413 const draft = chatService.createMessageDraft(
414 currentChannel.id, relayUrl, content, currentChannel.messageExpiry
415 )
416 const signed = await signEvent(draft)
417 await client.publishEvent([relayUrl], signed)
418 },
419 [currentChannel, relayUrl, pubkey, signEvent]
420 )
421
422 const createChannel = useCallback(
423 async (name: string, about: string, accessMode: TAccessMode = 'whitelist') => {
424 if (!pubkey) return
425 const draft = chatService.createChannelDraft(name, about, accessMode)
426 const signed = await signEvent(draft)
427 await client.publishEvent([relayUrl], signed)
428 await refreshChannels()
429 },
430 [relayUrl, pubkey, signEvent, refreshChannels]
431 )
432
433 const loadMoreMessages = useCallback(async () => {
434 if (!currentChannel || messages.length === 0) return
435 const oldest = messages[0]
436 const older = await chatService.fetchMessages(
437 relayUrl,
438 currentChannel.id,
439 50,
440 oldest.createdAt - 1
441 )
442 older.forEach((m) => seenIdsRef.current.add(m.id))
443 setMessages((prev) => [...older, ...prev])
444 }, [currentChannel, messages, relayUrl])
445
446 // --- Moderation actions ---
447
448 const publishMetadataUpdate = useCallback(
449 async (
450 mods: string[],
451 members: TMemberEntry[],
452 blocked: TMemberEntry[],
453 invited: TMemberEntry[],
454 requested: string[],
455 rejected: string[],
456 accessMode?: TAccessMode,
457 messageExpiry?: number
458 ) => {
459 if (!currentChannel || !pubkey) return
460 const meta: Record<string, unknown> = {
461 name: currentChannel.name,
462 about: currentChannel.about,
463 access_mode: accessMode ?? channelAccessMode
464 }
465 const expiry = messageExpiry ?? currentChannel.messageExpiry
466 if (expiry !== undefined) {
467 meta.message_expiry = expiry
468 }
469
470 const draft = chatService.createMetadataUpdateDraft(
471 currentChannel.id,
472 relayUrl,
473 meta as any,
474 mods.filter((pk) => pk !== currentChannel.creator),
475 members,
476 blocked,
477 invited,
478 requested,
479 rejected
480 )
481 const signed = await signEvent(draft)
482 await client.publishEvent([relayUrl], signed)
483 },
484 [currentChannel, relayUrl, pubkey, signEvent, channelAccessMode]
485 )
486
487 const addMod = useCallback(
488 async (pk: string) => {
489 const newMods = [...channelMods, pk]
490 setChannelMods(newMods)
491 await publishMetadataUpdate(newMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected)
492 },
493 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
494 )
495
496 const removeMod = useCallback(
497 async (pk: string) => {
498 // Cascade: remove all members/blocked/invited that this mod added
499 const newMods = channelMods.filter((m) => m !== pk)
500 const newMembers = channelMembers.filter((m) => m.addedBy !== pk)
501 const newBlocked = channelBlocked.filter((m) => m.addedBy !== pk)
502 const newInvited = channelInvited.filter((m) => m.addedBy !== pk)
503 setChannelMods(newMods)
504 setChannelMembers(newMembers)
505 setChannelBlocked(newBlocked)
506 setChannelInvited(newInvited)
507 await publishMetadataUpdate(newMods, newMembers, newBlocked, newInvited, channelRequested, channelRejected)
508 },
509 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
510 )
511
512 const approveMember = useCallback(
513 async (pk: string) => {
514 if (!pubkey) return
515 const entry: TMemberEntry = { pubkey: pk, addedBy: pubkey }
516 const newMembers = [...channelMembers, entry]
517 // Remove from requested if present
518 const newRequested = channelRequested.filter((r) => r !== pk)
519 setChannelMembers(newMembers)
520 setChannelRequested(newRequested)
521 await publishMetadataUpdate(channelMods, newMembers, channelBlocked, channelInvited, newRequested, channelRejected)
522 },
523 [pubkey, channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
524 )
525
526 const removeMember = useCallback(
527 async (pk: string) => {
528 const newMembers = channelMembers.filter((m) => m.pubkey !== pk)
529 setChannelMembers(newMembers)
530 await publishMetadataUpdate(channelMods, newMembers, channelBlocked, channelInvited, channelRequested, channelRejected)
531 },
532 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
533 )
534
535 const hideMessage = useCallback(
536 async (messageId: string) => {
537 if (!pubkey) return
538 const draft = chatService.createHideMessageDraft(messageId, relayUrl)
539 const signed = await signEvent(draft)
540 await client.publishEvent([relayUrl], signed)
541 setHiddenMessages((prev) => new Set([...prev, messageId]))
542 },
543 [relayUrl, pubkey, signEvent]
544 )
545
546 const blockUser = useCallback(
547 async (targetPubkey: string) => {
548 if (!currentChannel || !pubkey) return
549 const draft = chatService.createBlockUserDraft(
550 currentChannel.id,
551 targetPubkey,
552 relayUrl
553 )
554 const signed = await signEvent(draft)
555 await client.publishEvent([relayUrl], signed)
556 const entry: TMemberEntry = { pubkey: targetPubkey, addedBy: pubkey }
557 setChannelBlocked((prev) => [...prev, entry])
558 },
559 [currentChannel, relayUrl, pubkey, signEvent]
560 )
561
562 const unblockUser = useCallback(
563 async (targetPubkey: string) => {
564 const newBlocked = channelBlocked.filter((e) => e.pubkey !== targetPubkey)
565 setChannelBlocked(newBlocked)
566 await publishMetadataUpdate(channelMods, channelMembers, newBlocked, channelInvited, channelRequested, channelRejected)
567 },
568 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
569 )
570
571 const updateAccessMode = useCallback(
572 async (mode: TAccessMode) => {
573 setChannelAccessMode(mode)
574 setCurrentChannel((prev) => (prev ? { ...prev, accessMode: mode } : null))
575 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, mode)
576 },
577 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
578 )
579
580 const updateMessageExpiry = useCallback(
581 async (expirySecs: number) => {
582 setCurrentChannel((prev) => (prev ? { ...prev, messageExpiry: expirySecs } : null))
583 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, undefined, expirySecs)
584 },
585 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
586 )
587
588 const sendInvite = useCallback(
589 async (targetPubkey: string) => {
590 if (!currentChannel || !pubkey) return
591 const entry: TMemberEntry = { pubkey: targetPubkey, addedBy: pubkey }
592 const newInvited = [...channelInvited, entry]
593 setChannelInvited(newInvited)
594 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, newInvited, channelRequested, channelRejected)
595
596 // Send DM with channel link
597 const link = `https://smesh.mleku.dev/#/chat/${currentChannel.id}`
598 const dmContent = `You've been invited to #${currentChannel.name} on NIRC:\n${link}`
599 const dmDraft = {
600 kind: 4,
601 created_at: Math.floor(Date.now() / 1000),
602 tags: [['p', targetPubkey]],
603 content: dmContent
604 }
605 try {
606 const signed = await signEvent(dmDraft)
607 await client.publishEvent([relayUrl], signed)
608 } catch {
609 // DM send failure is non-fatal — invite is already in metadata
610 }
611 },
612 [currentChannel, pubkey, channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate, signEvent, relayUrl]
613 )
614
615 const revokeInvite = useCallback(
616 async (targetPubkey: string) => {
617 const newInvited = channelInvited.filter((e) => e.pubkey !== targetPubkey)
618 setChannelInvited(newInvited)
619 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, newInvited, channelRequested, channelRejected)
620 },
621 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
622 )
623
624 const acceptRequest = useCallback(
625 async (pk: string) => {
626 // Move from requested to member
627 await approveMember(pk)
628 },
629 [approveMember]
630 )
631
632 const rejectRequest = useCallback(
633 async (pk: string) => {
634 const newRequested = channelRequested.filter((r) => r !== pk)
635 const newRejected = [...channelRejected, pk]
636 setChannelRequested(newRequested)
637 setChannelRejected(newRejected)
638 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, channelInvited, newRequested, newRejected)
639 },
640 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
641 )
642
643 const revokeRejection = useCallback(
644 async (pk: string) => {
645 const newRejected = channelRejected.filter((r) => r !== pk)
646 setChannelRejected(newRejected)
647 await publishMetadataUpdate(channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, newRejected)
648 },
649 [channelMods, channelMembers, channelBlocked, channelInvited, channelRequested, channelRejected, publishMetadataUpdate]
650 )
651
652 // Filter messages: hide hidden messages and blocked users
653 const filteredMessages = useMemo(() => {
654 const blockedSet = new Set(channelBlocked.map((e) => e.pubkey))
655 return messages.filter(
656 (msg) => !hiddenMessages.has(msg.id) && !blockedSet.has(msg.pubkey)
657 )
658 }, [messages, hiddenMessages, channelBlocked])
659
660 return (
661 <ChatContext.Provider
662 value={{
663 channels,
664 currentChannel,
665 messages: filteredMessages,
666 isLoadingChannels,
667 isLoadingMessages,
668 relayUrl,
669 setRelayUrl,
670 selectChannel,
671 selectChannelById,
672 sendMessage,
673 createChannel,
674 refreshChannels,
675 loadMoreMessages,
676 // Notifications
677 unreadCounts,
678 hasUnreadChannels,
679 mutedChannels,
680 markChannelAsSeen,
681 toggleMuteChannel,
682 // Moderation
683 channelMods,
684 channelMembers,
685 channelBlocked,
686 channelInvited,
687 channelRequested,
688 channelRejected,
689 channelAccessMode,
690 hiddenMessages,
691 isOwnerOrMod,
692 isMember,
693 addMod,
694 removeMod,
695 approveMember,
696 removeMember,
697 hideMessage,
698 blockUser,
699 unblockUser,
700 updateAccessMode,
701 updateMessageExpiry,
702 sendInvite,
703 revokeInvite,
704 acceptRequest,
705 rejectRequest,
706 revokeRejection,
707 channelParticipants
708 }}
709 >
710 {children}
711 </ChatContext.Provider>
712 )
713 }
714