PinListProvider.tsx raw
1 import {
2 PinList,
3 tryToPinList,
4 Pubkey,
5 CannotPinOthersContentError,
6 CanOnlyPinNotesError,
7 eventDispatcher,
8 NotePinned,
9 NoteUnpinned,
10 PinsLimitExceeded,
11 PinListPublished
12 } from '@/domain'
13 import client from '@/services/client.service'
14 import { Event } from 'nostr-tools'
15 import { createContext, useContext, useMemo } from 'react'
16 import { useTranslation } from 'react-i18next'
17 import { toast } from 'sonner'
18 import { useNostr } from './NostrProvider'
19
20 type TPinListContext = {
21 pinnedEventHexIdSet: Set<string>
22 pin: (event: Event) => Promise<void>
23 unpin: (event: Event) => Promise<void>
24 }
25
26 const PinListContext = createContext<TPinListContext | undefined>(undefined)
27
28 export const usePinList = () => {
29 const context = useContext(PinListContext)
30 if (!context) {
31 throw new Error('usePinList must be used within a PinListProvider')
32 }
33 return context
34 }
35
36 export function PinListProvider({ children }: { children: React.ReactNode }) {
37 const { t } = useTranslation()
38 const { pubkey: accountPubkey, pinListEvent, publish, updatePinListEvent } = useNostr()
39
40 // Use domain aggregate for pinned event IDs
41 const pinnedEventHexIdSet = useMemo(() => {
42 const pinList = tryToPinList(pinListEvent)
43 return pinList?.getEventIdSet() ?? new Set<string>()
44 }, [pinListEvent])
45
46 const pin = async (event: Event) => {
47 if (!accountPubkey) return
48
49 const _pin = async () => {
50 const pinListEvent = await client.fetchPinListEvent(accountPubkey)
51 const ownerPubkey = Pubkey.fromHex(accountPubkey)
52
53 // Use domain aggregate
54 const pinList = tryToPinList(pinListEvent) ?? PinList.empty(ownerPubkey)
55
56 // Pin using domain method - throws if invalid
57 const change = pinList.pin(event)
58 if (change.type === 'no_change') return
59
60 // Publish the updated pin list
61 const draftEvent = pinList.toDraftEvent()
62 const newPinListEvent = await publish(draftEvent)
63 await updatePinListEvent(newPinListEvent)
64
65 // Dispatch domain events
66 if (change.type === 'pinned') {
67 await eventDispatcher.dispatch(
68 new NotePinned(ownerPubkey, change.entry.eventId)
69 )
70 } else if (change.type === 'limit_exceeded') {
71 const removedIds = change.removed.map((e) => e.eventId.hex)
72 await eventDispatcher.dispatch(
73 new PinsLimitExceeded(ownerPubkey, removedIds)
74 )
75 // Also dispatch the pinned event for the new pin
76 const newPinEntry = pinList.getEntries()[pinList.count - 1]
77 if (newPinEntry) {
78 await eventDispatcher.dispatch(
79 new NotePinned(ownerPubkey, newPinEntry.eventId)
80 )
81 }
82 }
83 await eventDispatcher.dispatch(
84 new PinListPublished(ownerPubkey, pinList.count)
85 )
86 }
87
88 const { unwrap } = toast.promise(_pin, {
89 loading: t('Pinning...'),
90 success: t('Pinned!'),
91 error: (err) => {
92 if (err instanceof CannotPinOthersContentError) {
93 return t('Can only pin your own notes')
94 }
95 if (err instanceof CanOnlyPinNotesError) {
96 return t('Can only pin short text notes')
97 }
98 return t('Failed to pin: {{error}}', { error: err.message })
99 }
100 })
101 await unwrap()
102 }
103
104 const unpin = async (event: Event) => {
105 if (!accountPubkey) return
106
107 const _unpin = async () => {
108 const pinListEvent = await client.fetchPinListEvent(accountPubkey)
109 if (!pinListEvent) return
110
111 const pinList = tryToPinList(pinListEvent)
112 if (!pinList) return
113
114 const ownerPubkey = pinList.owner
115
116 // Unpin using domain method
117 const change = pinList.unpinEvent(event)
118 if (change.type === 'no_change') return
119
120 // Publish the updated pin list
121 const draftEvent = pinList.toDraftEvent()
122 const newPinListEvent = await publish(draftEvent)
123 await updatePinListEvent(newPinListEvent)
124
125 // Dispatch domain events
126 if (change.type === 'unpinned') {
127 await eventDispatcher.dispatch(
128 new NoteUnpinned(ownerPubkey, change.eventId)
129 )
130 await eventDispatcher.dispatch(
131 new PinListPublished(ownerPubkey, pinList.count)
132 )
133 }
134 }
135
136 const { unwrap } = toast.promise(_unpin, {
137 loading: t('Unpinning...'),
138 success: t('Unpinned!'),
139 error: (err) => t('Failed to unpin: {{error}}', { error: err.message })
140 })
141 await unwrap()
142 }
143
144 return (
145 <PinListContext.Provider
146 value={{
147 pinnedEventHexIdSet,
148 pin,
149 unpin
150 }}
151 >
152 {children}
153 </PinListContext.Provider>
154 )
155 }
156