feat: pinned users event
This commit is contained in:
parent
ad016aba35
commit
7ec4835c61
10 changed files with 303 additions and 136 deletions
23
src/App.tsx
23
src/App.tsx
|
|
@ -14,6 +14,7 @@ import { MediaUploadServiceProvider } from '@/providers/MediaUploadServiceProvid
|
||||||
import { MuteListProvider } from '@/providers/MuteListProvider'
|
import { MuteListProvider } from '@/providers/MuteListProvider'
|
||||||
import { NostrProvider } from '@/providers/NostrProvider'
|
import { NostrProvider } from '@/providers/NostrProvider'
|
||||||
import { PinListProvider } from '@/providers/PinListProvider'
|
import { PinListProvider } from '@/providers/PinListProvider'
|
||||||
|
import { PinnedUsersProvider } from '@/providers/PinnedUsersProvider'
|
||||||
import { ReplyProvider } from '@/providers/ReplyProvider'
|
import { ReplyProvider } from '@/providers/ReplyProvider'
|
||||||
import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
|
import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
|
||||||
import { ThemeProvider } from '@/providers/ThemeProvider'
|
import { ThemeProvider } from '@/providers/ThemeProvider'
|
||||||
|
|
@ -40,16 +41,18 @@ export default function App(): JSX.Element {
|
||||||
<BookmarksProvider>
|
<BookmarksProvider>
|
||||||
<EmojiPackProvider>
|
<EmojiPackProvider>
|
||||||
<PinListProvider>
|
<PinListProvider>
|
||||||
<FeedProvider>
|
<PinnedUsersProvider>
|
||||||
<ReplyProvider>
|
<FeedProvider>
|
||||||
<MediaUploadServiceProvider>
|
<ReplyProvider>
|
||||||
<KindFilterProvider>
|
<MediaUploadServiceProvider>
|
||||||
<PageManager />
|
<KindFilterProvider>
|
||||||
<Toaster />
|
<PageManager />
|
||||||
</KindFilterProvider>
|
<Toaster />
|
||||||
</MediaUploadServiceProvider>
|
</KindFilterProvider>
|
||||||
</ReplyProvider>
|
</MediaUploadServiceProvider>
|
||||||
</FeedProvider>
|
</ReplyProvider>
|
||||||
|
</FeedProvider>
|
||||||
|
</PinnedUsersProvider>
|
||||||
</PinListProvider>
|
</PinListProvider>
|
||||||
</EmojiPackProvider>
|
</EmojiPackProvider>
|
||||||
</BookmarksProvider>
|
</BookmarksProvider>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
|
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
|
||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { usePinnedUsers } from '@/providers/PinnedUsersProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import userAggregationService, { TUserAggregation } from '@/services/user-aggregation.service'
|
import userAggregationService, { TUserAggregation } from '@/services/user-aggregation.service'
|
||||||
|
|
@ -53,6 +54,7 @@ const UserAggregationList = forwardRef<
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
|
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
|
||||||
const { mutePubkeySet } = useMuteList()
|
const { mutePubkeySet } = useMuteList()
|
||||||
|
const { pinnedPubkeySet } = usePinnedUsers()
|
||||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||||
const { isEventDeleted } = useDeletedEvent()
|
const { isEventDeleted } = useDeletedEvent()
|
||||||
const [since, setSince] = useState(() => dayjs().subtract(1, 'day').unix())
|
const [since, setSince] = useState(() => dayjs().subtract(1, 'day').unix())
|
||||||
|
|
@ -64,9 +66,6 @@ const UserAggregationList = forwardRef<
|
||||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const supportTouch = useMemo(() => isTouchDevice(), [])
|
const supportTouch = useMemo(() => isTouchDevice(), [])
|
||||||
const [pinnedPubkeys, setPinnedPubkeys] = useState<Set<string>>(
|
|
||||||
new Set(userAggregationService.getPinnedPubkeys())
|
|
||||||
)
|
|
||||||
const feedId = useMemo(() => {
|
const feedId = useMemo(() => {
|
||||||
return userAggregationService.getFeedId(subRequests, showKinds)
|
return userAggregationService.getFeedId(subRequests, showKinds)
|
||||||
}, [JSON.stringify(subRequests), JSON.stringify(showKinds)])
|
}, [JSON.stringify(subRequests), JSON.stringify(showKinds)])
|
||||||
|
|
@ -97,7 +96,6 @@ const UserAggregationList = forwardRef<
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!subRequests.length) return
|
if (!subRequests.length) return
|
||||||
|
|
||||||
setPinnedPubkeys(new Set(userAggregationService.getPinnedPubkeys()))
|
|
||||||
setSince(dayjs().subtract(1, 'day').unix())
|
setSince(dayjs().subtract(1, 'day').unix())
|
||||||
setHasMore(true)
|
setHasMore(true)
|
||||||
|
|
||||||
|
|
@ -223,28 +221,24 @@ const UserAggregationList = forwardRef<
|
||||||
const aggregations = useMemo(() => {
|
const aggregations = useMemo(() => {
|
||||||
const aggs = userAggregationService.aggregateByUser(filteredEvents)
|
const aggs = userAggregationService.aggregateByUser(filteredEvents)
|
||||||
userAggregationService.saveAggregations(feedId, aggs)
|
userAggregationService.saveAggregations(feedId, aggs)
|
||||||
|
return aggs
|
||||||
|
}, [feedId, filteredEvents])
|
||||||
|
|
||||||
const pinned: TUserAggregation[] = []
|
const pinnedAggregations = useMemo(() => {
|
||||||
const unpinned: TUserAggregation[] = []
|
return aggregations.filter((agg) => pinnedPubkeySet.has(agg.pubkey))
|
||||||
|
}, [aggregations, pinnedPubkeySet])
|
||||||
|
|
||||||
aggs.forEach((agg) => {
|
const normalAggregations = useMemo(() => {
|
||||||
if (pinnedPubkeys.has(agg.pubkey)) {
|
return aggregations.filter((agg) => !pinnedPubkeySet.has(agg.pubkey))
|
||||||
pinned.push(agg)
|
}, [aggregations, pinnedPubkeySet])
|
||||||
} else {
|
|
||||||
unpinned.push(agg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...pinned, ...unpinned]
|
const displayedNormalAggregations = useMemo(() => {
|
||||||
}, [feedId, filteredEvents, pinnedPubkeys])
|
return normalAggregations.slice(0, showCount)
|
||||||
|
}, [normalAggregations, showCount])
|
||||||
const displayedAggregations = useMemo(() => {
|
|
||||||
return aggregations.slice(0, showCount)
|
|
||||||
}, [aggregations, showCount])
|
|
||||||
|
|
||||||
const hasMoreToDisplay = useMemo(() => {
|
const hasMoreToDisplay = useMemo(() => {
|
||||||
return aggregations.length > displayedAggregations.length
|
return normalAggregations.length > displayedNormalAggregations.length
|
||||||
}, [aggregations, displayedAggregations])
|
}, [normalAggregations, displayedNormalAggregations])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = {
|
const options = {
|
||||||
|
|
@ -294,7 +288,7 @@ const UserAggregationList = forwardRef<
|
||||||
|
|
||||||
const list = (
|
const list = (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
{displayedAggregations.map((agg) => (
|
{pinnedAggregations.map((agg) => (
|
||||||
<UserAggregationItem
|
<UserAggregationItem
|
||||||
key={agg.pubkey}
|
key={agg.pubkey}
|
||||||
feedId={feedId}
|
feedId={feedId}
|
||||||
|
|
@ -302,11 +296,21 @@ const UserAggregationList = forwardRef<
|
||||||
onClick={() => handleViewUser(agg)}
|
onClick={() => handleViewUser(agg)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{normalAggregations.map((agg) => (
|
||||||
|
<UserAggregationItem
|
||||||
|
key={agg.pubkey}
|
||||||
|
feedId={feedId}
|
||||||
|
aggregation={agg}
|
||||||
|
onClick={() => handleViewUser(agg)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{loading || hasMoreToDisplay ? (
|
{loading || hasMoreToDisplay ? (
|
||||||
<div ref={bottomRef}>
|
<div ref={bottomRef}>
|
||||||
<UserAggregationItemSkeleton />
|
<UserAggregationItemSkeleton />
|
||||||
</div>
|
</div>
|
||||||
) : displayedAggregations.length === 0 ? (
|
) : aggregations.length === 0 ? (
|
||||||
<div className="flex justify-center w-full mt-2">
|
<div className="flex justify-center w-full mt-2">
|
||||||
<Button size="lg" onClick={() => setRefreshCount((count) => count + 1)}>
|
<Button size="lg" onClick={() => setRefreshCount((count) => count + 1)}>
|
||||||
{t('Reload')}
|
{t('Reload')}
|
||||||
|
|
@ -373,7 +377,7 @@ function UserAggregationItem({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const supportTouch = useMemo(() => isTouchDevice(), [])
|
const supportTouch = useMemo(() => isTouchDevice(), [])
|
||||||
const [hasNewEvents, setHasNewEvents] = useState(true)
|
const [hasNewEvents, setHasNewEvents] = useState(true)
|
||||||
const [isPinned, setIsPinned] = useState(userAggregationService.isPinned(aggregation.pubkey))
|
const { isPinned, togglePin } = usePinnedUsers()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const update = () => {
|
const update = () => {
|
||||||
|
|
@ -396,13 +400,7 @@ function UserAggregationItem({
|
||||||
|
|
||||||
const onTogglePin = (e: React.MouseEvent) => {
|
const onTogglePin = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isPinned) {
|
togglePin(aggregation.pubkey)
|
||||||
userAggregationService.unpinUser(aggregation.pubkey)
|
|
||||||
setIsPinned(false)
|
|
||||||
} else {
|
|
||||||
userAggregationService.pinUser(aggregation.pubkey)
|
|
||||||
setIsPinned(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onToggleViewed = (e: React.MouseEvent) => {
|
const onToggleViewed = (e: React.MouseEvent) => {
|
||||||
|
|
@ -459,13 +457,17 @@ function UserAggregationItem({
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onTogglePin}
|
onClick={onTogglePin}
|
||||||
className={`flex-shrink-0 ${
|
className={`flex-shrink-0 ${
|
||||||
isPinned
|
isPinned(aggregation.pubkey)
|
||||||
? 'text-primary hover:text-primary/80'
|
? 'text-primary hover:text-primary/80'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
title={isPinned ? t('Unpin') : t('Pin')}
|
title={isPinned(aggregation.pubkey) ? t('Unpin') : t('Pin')}
|
||||||
>
|
>
|
||||||
{isPinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
|
{isPinned(aggregation.pubkey) ? (
|
||||||
|
<PinOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Pin className="w-4 h-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export const StorageKey = {
|
||||||
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
||||||
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
||||||
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
||||||
PINNED_PUBKEYS: 'pinnedPubkeys',
|
PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated
|
||||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
||||||
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
||||||
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
||||||
|
|
@ -80,6 +80,7 @@ export const ExtendedKind = {
|
||||||
COMMENT: 1111,
|
COMMENT: 1111,
|
||||||
VOICE: 1222,
|
VOICE: 1222,
|
||||||
VOICE_COMMENT: 1244,
|
VOICE_COMMENT: 1244,
|
||||||
|
PINNED_USERS: 10010,
|
||||||
FAVORITE_RELAYS: 10012,
|
FAVORITE_RELAYS: 10012,
|
||||||
BLOSSOM_SERVER_LIST: 10063,
|
BLOSSOM_SERVER_LIST: 10063,
|
||||||
FOLLOW_PACK: 39089,
|
FOLLOW_PACK: 39089,
|
||||||
|
|
|
||||||
|
|
@ -54,25 +54,30 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||||
}, [publicMutePubkeySet, privateMutePubkeySet])
|
}, [publicMutePubkeySet, privateMutePubkeySet])
|
||||||
const [changing, setChanging] = useState(false)
|
const [changing, setChanging] = useState(false)
|
||||||
|
|
||||||
const getPrivateTags = async (muteListEvent: Event) => {
|
const getPrivateTags = useCallback(
|
||||||
if (!muteListEvent.content) return []
|
async (muteListEvent: Event) => {
|
||||||
|
if (!muteListEvent.content) return []
|
||||||
|
|
||||||
const storedDecryptedTags = await indexedDb.getMuteDecryptedTags(muteListEvent.id)
|
|
||||||
|
|
||||||
if (storedDecryptedTags) {
|
|
||||||
return storedDecryptedTags
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
const plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
const storedPlainText = await indexedDb.getDecryptedContent(muteListEvent.id)
|
||||||
|
|
||||||
|
let plainText: string
|
||||||
|
if (storedPlainText) {
|
||||||
|
plainText = storedPlainText
|
||||||
|
} else {
|
||||||
|
plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
||||||
|
await indexedDb.putDecryptedContent(muteListEvent.id, plainText)
|
||||||
|
}
|
||||||
|
|
||||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
|
|
||||||
return privateTags
|
return privateTags
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to decrypt mute list content', error)
|
console.error('Failed to decrypt mute list content', error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
[nip04Decrypt]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateMuteTags = async () => {
|
const updateMuteTags = async () => {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ type TNostrContext = {
|
||||||
favoriteRelaysEvent: Event | null
|
favoriteRelaysEvent: Event | null
|
||||||
userEmojiListEvent: Event | null
|
userEmojiListEvent: Event | null
|
||||||
pinListEvent: Event | null
|
pinListEvent: Event | null
|
||||||
|
pinnedUsersEvent: Event | null
|
||||||
notificationsSeenAt: number
|
notificationsSeenAt: number
|
||||||
account: TAccountPointer | null
|
account: TAccountPointer | null
|
||||||
accounts: TAccountPointer[]
|
accounts: TAccountPointer[]
|
||||||
|
|
@ -88,6 +89,7 @@ type TNostrContext = {
|
||||||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||||
updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise<void>
|
updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise<void>
|
||||||
updatePinListEvent: (pinListEvent: Event) => Promise<void>
|
updatePinListEvent: (pinListEvent: Event) => Promise<void>
|
||||||
|
updatePinnedUsersEvent: (pinnedUsersEvent: Event, privateTags?: string[][]) => Promise<void>
|
||||||
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
|
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +121,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
||||||
const [followListEvent, setFollowListEvent] = useState<Event | null>(null)
|
const [followListEvent, setFollowListEvent] = useState<Event | null>(null)
|
||||||
const [muteListEvent, setMuteListEvent] = useState<Event | null>(null)
|
const [muteListEvent, setMuteListEvent] = useState<Event | null>(null)
|
||||||
|
const [pinnedUsersEvent, setPinnedUsersEvent] = useState<Event | null>(null)
|
||||||
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
|
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
|
||||||
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
|
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
|
||||||
const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
|
const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
|
||||||
|
|
@ -195,7 +198,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
storedBookmarkListEvent,
|
storedBookmarkListEvent,
|
||||||
storedFavoriteRelaysEvent,
|
storedFavoriteRelaysEvent,
|
||||||
storedUserEmojiListEvent,
|
storedUserEmojiListEvent,
|
||||||
storedPinListEvent
|
storedPinListEvent,
|
||||||
|
storedPinnedUsersEvent
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||||
|
|
@ -204,7 +208,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
|
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist)
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist),
|
||||||
|
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.PINNED_USERS)
|
||||||
])
|
])
|
||||||
if (storedRelayListEvent) {
|
if (storedRelayListEvent) {
|
||||||
setRelayList(getRelayListFromEvent(storedRelayListEvent, storage.getFilterOutOnionRelays()))
|
setRelayList(getRelayListFromEvent(storedRelayListEvent, storage.getFilterOutOnionRelays()))
|
||||||
|
|
@ -231,6 +236,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
if (storedPinListEvent) {
|
if (storedPinListEvent) {
|
||||||
setPinListEvent(storedPinListEvent)
|
setPinListEvent(storedPinListEvent)
|
||||||
}
|
}
|
||||||
|
if (storedPinnedUsersEvent !== undefined) {
|
||||||
|
setPinnedUsersEvent(storedPinnedUsersEvent)
|
||||||
|
}
|
||||||
|
|
||||||
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
||||||
kinds: [kinds.RelayList],
|
kinds: [kinds.RelayList],
|
||||||
|
|
@ -254,7 +262,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
ExtendedKind.FAVORITE_RELAYS,
|
ExtendedKind.FAVORITE_RELAYS,
|
||||||
ExtendedKind.BLOSSOM_SERVER_LIST,
|
ExtendedKind.BLOSSOM_SERVER_LIST,
|
||||||
kinds.UserEmojiList,
|
kinds.UserEmojiList,
|
||||||
kinds.Pinlist
|
kinds.Pinlist,
|
||||||
|
ExtendedKind.PINNED_USERS
|
||||||
],
|
],
|
||||||
authors: [account.pubkey]
|
authors: [account.pubkey]
|
||||||
},
|
},
|
||||||
|
|
@ -280,6 +289,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
|
getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
|
||||||
)
|
)
|
||||||
const pinnedNotesEvent = sortedEvents.find((e) => e.kind === kinds.Pinlist)
|
const pinnedNotesEvent = sortedEvents.find((e) => e.kind === kinds.Pinlist)
|
||||||
|
const pinnedUsersEvent = sortedEvents.find((e) => e.kind === ExtendedKind.PINNED_USERS)
|
||||||
|
|
||||||
if (profileEvent) {
|
if (profileEvent) {
|
||||||
const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
|
const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
|
||||||
if (updatedProfileEvent.id === profileEvent.id) {
|
if (updatedProfileEvent.id === profileEvent.id) {
|
||||||
|
|
@ -332,6 +343,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
setPinListEvent(updatedPinnedNotesEvent)
|
setPinListEvent(updatedPinnedNotesEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pinnedUsersEvent) {
|
||||||
|
const updatedPinnedUsersEvent = await indexedDb.putReplaceableEvent(pinnedUsersEvent)
|
||||||
|
if (updatedPinnedUsersEvent.id === pinnedUsersEvent.id) {
|
||||||
|
setPinnedUsersEvent(updatedPinnedUsersEvent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await indexedDb.putNullReplaceableEvent(account.pubkey, ExtendedKind.PINNED_USERS)
|
||||||
|
setPinnedUsersEvent(null)
|
||||||
|
}
|
||||||
|
|
||||||
const notificationsSeenAt = Math.max(
|
const notificationsSeenAt = Math.max(
|
||||||
notificationsSeenAtEvent?.created_at ?? 0,
|
notificationsSeenAtEvent?.created_at ?? 0,
|
||||||
|
|
@ -726,7 +746,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
const newMuteListEvent = await indexedDb.putReplaceableEvent(muteListEvent)
|
const newMuteListEvent = await indexedDb.putReplaceableEvent(muteListEvent)
|
||||||
if (newMuteListEvent.id !== muteListEvent.id) return
|
if (newMuteListEvent.id !== muteListEvent.id) return
|
||||||
|
|
||||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
|
await indexedDb.putDecryptedContent(muteListEvent.id, JSON.stringify(privateTags))
|
||||||
setMuteListEvent(muteListEvent)
|
setMuteListEvent(muteListEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -758,6 +778,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
setPinListEvent(newPinListEvent)
|
setPinListEvent(newPinListEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePinnedUsersEvent = async (pinnedUsersEvent: Event, privateTags?: string[][]) => {
|
||||||
|
const newPinnedUsersEvent = await indexedDb.putReplaceableEvent(pinnedUsersEvent)
|
||||||
|
if (newPinnedUsersEvent.id !== pinnedUsersEvent.id) return
|
||||||
|
|
||||||
|
if (privateTags) {
|
||||||
|
await indexedDb.putDecryptedContent(pinnedUsersEvent.id, JSON.stringify(privateTags))
|
||||||
|
}
|
||||||
|
setPinnedUsersEvent(newPinnedUsersEvent)
|
||||||
|
}
|
||||||
|
|
||||||
const updateNotificationsSeenAt = async (skipPublish = false) => {
|
const updateNotificationsSeenAt = async (skipPublish = false) => {
|
||||||
if (!account) return
|
if (!account) return
|
||||||
|
|
||||||
|
|
@ -794,6 +824,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
favoriteRelaysEvent,
|
favoriteRelaysEvent,
|
||||||
userEmojiListEvent,
|
userEmojiListEvent,
|
||||||
pinListEvent,
|
pinListEvent,
|
||||||
|
pinnedUsersEvent,
|
||||||
notificationsSeenAt,
|
notificationsSeenAt,
|
||||||
account,
|
account,
|
||||||
accounts,
|
accounts,
|
||||||
|
|
@ -823,6 +854,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
updateFavoriteRelaysEvent,
|
updateFavoriteRelaysEvent,
|
||||||
updateUserEmojiListEvent,
|
updateUserEmojiListEvent,
|
||||||
updatePinListEvent,
|
updatePinListEvent,
|
||||||
|
updatePinnedUsersEvent,
|
||||||
updateNotificationsSeenAt
|
updateNotificationsSeenAt
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
171
src/providers/PinnedUsersProvider.tsx
Normal file
171
src/providers/PinnedUsersProvider.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { ExtendedKind } from '@/constants'
|
||||||
|
import { getPubkeysFromPTags } from '@/lib/tag'
|
||||||
|
import indexedDb from '@/services/indexed-db.service'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { useNostr } from './NostrProvider'
|
||||||
|
|
||||||
|
type TPinnedUsersContext = {
|
||||||
|
pinnedPubkeySet: Set<string>
|
||||||
|
isPinned: (pubkey: string) => boolean
|
||||||
|
pinUser: (pubkey: string) => Promise<void>
|
||||||
|
unpinUser: (pubkey: string) => Promise<void>
|
||||||
|
togglePin: (pubkey: string) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PinnedUsersContext = createContext<TPinnedUsersContext | undefined>(undefined)
|
||||||
|
|
||||||
|
export const usePinnedUsers = () => {
|
||||||
|
const context = useContext(PinnedUsersContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePinnedUsers must be used within a PinnedUsersProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPinnedUsersListDraftEvent(tags: string[][], content = '') {
|
||||||
|
return {
|
||||||
|
kind: ExtendedKind.PINNED_USERS,
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PinnedUsersProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const {
|
||||||
|
pubkey: accountPubkey,
|
||||||
|
pinnedUsersEvent,
|
||||||
|
updatePinnedUsersEvent,
|
||||||
|
publish,
|
||||||
|
nip04Decrypt,
|
||||||
|
nip04Encrypt
|
||||||
|
} = useNostr()
|
||||||
|
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||||
|
const pinnedPubkeySet = useMemo(() => {
|
||||||
|
if (!pinnedUsersEvent) return new Set<string>()
|
||||||
|
return new Set(getPubkeysFromPTags(pinnedUsersEvent.tags.concat(privateTags)))
|
||||||
|
}, [pinnedUsersEvent, privateTags])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updatePrivateTags = async () => {
|
||||||
|
if (!pinnedUsersEvent) {
|
||||||
|
setPrivateTags([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateTags = await getPrivateTags(pinnedUsersEvent).catch(() => {
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
setPrivateTags(privateTags)
|
||||||
|
}
|
||||||
|
updatePrivateTags()
|
||||||
|
}, [pinnedUsersEvent])
|
||||||
|
|
||||||
|
const getPrivateTags = useCallback(
|
||||||
|
async (event: Event) => {
|
||||||
|
if (!event.content) return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedPlainText = await indexedDb.getDecryptedContent(event.id)
|
||||||
|
|
||||||
|
let plainText: string
|
||||||
|
if (storedPlainText) {
|
||||||
|
plainText = storedPlainText
|
||||||
|
} else {
|
||||||
|
plainText = await nip04Decrypt(event.pubkey, event.content)
|
||||||
|
await indexedDb.putDecryptedContent(event.id, plainText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||||
|
return privateTags
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt pinned users content', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[nip04Decrypt]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isPinned = useCallback(
|
||||||
|
(pubkey: string) => {
|
||||||
|
return pinnedPubkeySet.has(pubkey)
|
||||||
|
},
|
||||||
|
[pinnedPubkeySet]
|
||||||
|
)
|
||||||
|
|
||||||
|
const pinUser = useCallback(
|
||||||
|
async (pubkey: string) => {
|
||||||
|
if (!accountPubkey || isPinned(pubkey)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTags = [...(pinnedUsersEvent?.tags ?? []), ['p', pubkey]]
|
||||||
|
const draftEvent = createPinnedUsersListDraftEvent(newTags, pinnedUsersEvent?.content ?? '')
|
||||||
|
const newEvent = await publish(draftEvent)
|
||||||
|
await updatePinnedUsersEvent(newEvent, privateTags)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to pin user:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[accountPubkey, isPinned, pinnedUsersEvent, publish, updatePinnedUsersEvent, privateTags]
|
||||||
|
)
|
||||||
|
|
||||||
|
const unpinUser = useCallback(
|
||||||
|
async (pubkey: string) => {
|
||||||
|
if (!accountPubkey || !pinnedUsersEvent || !isPinned(pubkey)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTags = pinnedUsersEvent.tags.filter(
|
||||||
|
([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey
|
||||||
|
)
|
||||||
|
const newPrivateTags = privateTags.filter(
|
||||||
|
([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey
|
||||||
|
)
|
||||||
|
let newContent = pinnedUsersEvent.content
|
||||||
|
if (newPrivateTags.length !== privateTags.length) {
|
||||||
|
newContent = await nip04Encrypt(pinnedUsersEvent.pubkey, JSON.stringify(newPrivateTags))
|
||||||
|
}
|
||||||
|
const draftEvent = createPinnedUsersListDraftEvent(newTags, newContent)
|
||||||
|
const newEvent = await publish(draftEvent)
|
||||||
|
await updatePinnedUsersEvent(newEvent, newPrivateTags)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to unpin user:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
accountPubkey,
|
||||||
|
isPinned,
|
||||||
|
pinnedUsersEvent,
|
||||||
|
publish,
|
||||||
|
updatePinnedUsersEvent,
|
||||||
|
privateTags,
|
||||||
|
nip04Encrypt
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const togglePin = useCallback(
|
||||||
|
async (pubkey: string) => {
|
||||||
|
if (isPinned(pubkey)) {
|
||||||
|
await unpinUser(pubkey)
|
||||||
|
} else {
|
||||||
|
await pinUser(pubkey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isPinned, pinUser, unpinUser]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PinnedUsersContext.Provider
|
||||||
|
value={{
|
||||||
|
pinnedPubkeySet,
|
||||||
|
isPinned,
|
||||||
|
pinUser,
|
||||||
|
unpinUser,
|
||||||
|
togglePin
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PinnedUsersContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1397,6 +1397,10 @@ class ClientService extends EventTarget {
|
||||||
return this.fetchReplaceableEvent(pubkey, kinds.UserEmojiList)
|
return this.fetchReplaceableEvent(pubkey, kinds.UserEmojiList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchPinnedUsersList(pubkey: string) {
|
||||||
|
return this.fetchReplaceableEvent(pubkey, ExtendedKind.PINNED_USERS)
|
||||||
|
}
|
||||||
|
|
||||||
async updateBlossomServerListEventCache(evt: NEvent) {
|
async updateBlossomServerListEventCache(evt: NEvent) {
|
||||||
await this.updateReplaceableEventCache(evt)
|
await this.updateReplaceableEventCache(evt)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ const StoreNames = {
|
||||||
MUTE_LIST_EVENTS: 'muteListEvents',
|
MUTE_LIST_EVENTS: 'muteListEvents',
|
||||||
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
|
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
|
||||||
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
|
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
|
||||||
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
|
|
||||||
USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents',
|
USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents',
|
||||||
EMOJI_SET_EVENTS: 'emojiSetEvents',
|
EMOJI_SET_EVENTS: 'emojiSetEvents',
|
||||||
PIN_LIST_EVENTS: 'pinListEvents',
|
PIN_LIST_EVENTS: 'pinListEvents',
|
||||||
|
|
@ -24,6 +23,9 @@ const StoreNames = {
|
||||||
RELAY_SETS: 'relaySets',
|
RELAY_SETS: 'relaySets',
|
||||||
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays',
|
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays',
|
||||||
RELAY_INFOS: 'relayInfos',
|
RELAY_INFOS: 'relayInfos',
|
||||||
|
DECRYPTED_CONTENTS: 'decryptedContents',
|
||||||
|
PINNED_USERS_EVENTS: 'pinnedUsersEvents',
|
||||||
|
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags', // deprecated
|
||||||
RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated
|
RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@ class IndexedDbService {
|
||||||
init(): Promise<void> {
|
init(): Promise<void> {
|
||||||
if (!this.initPromise) {
|
if (!this.initPromise) {
|
||||||
this.initPromise = new Promise((resolve, reject) => {
|
this.initPromise = new Promise((resolve, reject) => {
|
||||||
const request = window.indexedDB.open('jumble', 9)
|
const request = window.indexedDB.open('jumble', 10)
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
reject(event)
|
reject(event)
|
||||||
|
|
@ -71,8 +73,8 @@ class IndexedDbService {
|
||||||
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
|
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
|
||||||
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
|
if (!db.objectStoreNames.contains(StoreNames.DECRYPTED_CONTENTS)) {
|
||||||
db.createObjectStore(StoreNames.MUTE_DECRYPTED_TAGS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.DECRYPTED_CONTENTS, { keyPath: 'key' })
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
|
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
|
||||||
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
|
||||||
|
|
@ -98,9 +100,16 @@ class IndexedDbService {
|
||||||
if (!db.objectStoreNames.contains(StoreNames.PIN_LIST_EVENTS)) {
|
if (!db.objectStoreNames.contains(StoreNames.PIN_LIST_EVENTS)) {
|
||||||
db.createObjectStore(StoreNames.PIN_LIST_EVENTS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.PIN_LIST_EVENTS, { keyPath: 'key' })
|
||||||
}
|
}
|
||||||
|
if (!db.objectStoreNames.contains(StoreNames.PINNED_USERS_EVENTS)) {
|
||||||
|
db.createObjectStore(StoreNames.PINNED_USERS_EVENTS, { keyPath: 'key' })
|
||||||
|
}
|
||||||
|
|
||||||
if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
|
if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
|
||||||
db.deleteObjectStore(StoreNames.RELAY_INFO_EVENTS)
|
db.deleteObjectStore(StoreNames.RELAY_INFO_EVENTS)
|
||||||
}
|
}
|
||||||
|
if (db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
|
||||||
|
db.deleteObjectStore(StoreNames.MUTE_DECRYPTED_TAGS)
|
||||||
|
}
|
||||||
this.db = db
|
this.db = db
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -268,19 +277,19 @@ class IndexedDbService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMuteDecryptedTags(id: string): Promise<string[][] | null> {
|
async getDecryptedContent(key: string): Promise<string | null> {
|
||||||
await this.initPromise
|
await this.initPromise
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
return reject('database not initialized')
|
return reject('database not initialized')
|
||||||
}
|
}
|
||||||
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readonly')
|
const transaction = this.db.transaction(StoreNames.DECRYPTED_CONTENTS, 'readonly')
|
||||||
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
|
const store = transaction.objectStore(StoreNames.DECRYPTED_CONTENTS)
|
||||||
const request = store.get(id)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
resolve((request.result as TValue<string[][]>)?.value)
|
resolve((request.result as TValue<string>)?.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
|
|
@ -290,16 +299,16 @@ class IndexedDbService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async putMuteDecryptedTags(id: string, tags: string[][]): Promise<void> {
|
async putDecryptedContent(key: string, content: string): Promise<void> {
|
||||||
await this.initPromise
|
await this.initPromise
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
return reject('database not initialized')
|
return reject('database not initialized')
|
||||||
}
|
}
|
||||||
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readwrite')
|
const transaction = this.db.transaction(StoreNames.DECRYPTED_CONTENTS, 'readwrite')
|
||||||
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
|
const store = transaction.objectStore(StoreNames.DECRYPTED_CONTENTS)
|
||||||
|
|
||||||
const putRequest = store.put(this.formatValue(id, tags))
|
const putRequest = store.put(this.formatValue(key, content))
|
||||||
putRequest.onsuccess = () => {
|
putRequest.onsuccess = () => {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
resolve()
|
resolve()
|
||||||
|
|
@ -471,6 +480,8 @@ class IndexedDbService {
|
||||||
return StoreNames.EMOJI_SET_EVENTS
|
return StoreNames.EMOJI_SET_EVENTS
|
||||||
case kinds.Pinlist:
|
case kinds.Pinlist:
|
||||||
return StoreNames.PIN_LIST_EVENTS
|
return StoreNames.PIN_LIST_EVENTS
|
||||||
|
case ExtendedKind.PINNED_USERS:
|
||||||
|
return StoreNames.PINNED_USERS_EVENTS
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ class LocalStorageService {
|
||||||
private enableSingleColumnLayout: boolean = true
|
private enableSingleColumnLayout: boolean = true
|
||||||
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
||||||
private filterOutOnionRelays: boolean = !isTorBrowser()
|
private filterOutOnionRelays: boolean = !isTorBrowser()
|
||||||
private pinnedPubkeys: Set<string> = new Set()
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LocalStorageService.instance) {
|
if (!LocalStorageService.instance) {
|
||||||
|
|
@ -231,12 +230,8 @@ class LocalStorageService {
|
||||||
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedPubkeysStr = window.localStorage.getItem(StorageKey.PINNED_PUBKEYS)
|
|
||||||
if (pinnedPubkeysStr) {
|
|
||||||
this.pinnedPubkeys = new Set(JSON.parse(pinnedPubkeysStr))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up deprecated data
|
// Clean up deprecated data
|
||||||
|
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
|
||||||
|
|
@ -564,18 +559,6 @@ class LocalStorageService {
|
||||||
this.filterOutOnionRelays = filterOut
|
this.filterOutOnionRelays = filterOut
|
||||||
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
getPinnedPubkeys(): Set<string> {
|
|
||||||
return this.pinnedPubkeys
|
|
||||||
}
|
|
||||||
|
|
||||||
setPinnedPubkeys(pinnedPubkeys: Set<string>) {
|
|
||||||
this.pinnedPubkeys = pinnedPubkeys
|
|
||||||
window.localStorage.setItem(
|
|
||||||
StorageKey.PINNED_PUBKEYS,
|
|
||||||
JSON.stringify(Array.from(this.pinnedPubkeys))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LocalStorageService()
|
const instance = new LocalStorageService()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { getEventKey } from '@/lib/event'
|
import { getEventKey } from '@/lib/event'
|
||||||
import storage from '@/services/local-storage.service'
|
|
||||||
import { TFeedSubRequest } from '@/types'
|
import { TFeedSubRequest } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
|
|
@ -14,7 +13,6 @@ export type TUserAggregation = {
|
||||||
class UserAggregationService {
|
class UserAggregationService {
|
||||||
static instance: UserAggregationService
|
static instance: UserAggregationService
|
||||||
|
|
||||||
private pinnedPubkeys: Set<string> = new Set()
|
|
||||||
private aggregationStore: Map<string, Map<string, Event[]>> = new Map()
|
private aggregationStore: Map<string, Map<string, Event[]>> = new Map()
|
||||||
private listenersMap: Map<string, Set<() => void>> = new Map()
|
private listenersMap: Map<string, Set<() => void>> = new Map()
|
||||||
private lastViewedMap: Map<string, number> = new Map()
|
private lastViewedMap: Map<string, number> = new Map()
|
||||||
|
|
@ -24,7 +22,6 @@ class UserAggregationService {
|
||||||
return UserAggregationService.instance
|
return UserAggregationService.instance
|
||||||
}
|
}
|
||||||
UserAggregationService.instance = this
|
UserAggregationService.instance = this
|
||||||
this.pinnedPubkeys = storage.getPinnedPubkeys()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeAggregationChange(feedId: string, pubkey: string, listener: () => void) {
|
subscribeAggregationChange(feedId: string, pubkey: string, listener: () => void) {
|
||||||
|
|
@ -64,33 +61,6 @@ class UserAggregationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pinned users management
|
|
||||||
getPinnedPubkeys(): string[] {
|
|
||||||
return [...this.pinnedPubkeys]
|
|
||||||
}
|
|
||||||
|
|
||||||
isPinned(pubkey: string): boolean {
|
|
||||||
return this.pinnedPubkeys.has(pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
pinUser(pubkey: string) {
|
|
||||||
this.pinnedPubkeys.add(pubkey)
|
|
||||||
storage.setPinnedPubkeys(this.pinnedPubkeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
unpinUser(pubkey: string) {
|
|
||||||
this.pinnedPubkeys.delete(pubkey)
|
|
||||||
storage.setPinnedPubkeys(this.pinnedPubkeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePin(pubkey: string) {
|
|
||||||
if (this.isPinned(pubkey)) {
|
|
||||||
this.unpinUser(pubkey)
|
|
||||||
} else {
|
|
||||||
this.pinUser(pubkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate events by user
|
// Aggregate events by user
|
||||||
aggregateByUser(events: Event[]): TUserAggregation[] {
|
aggregateByUser(events: Event[]): TUserAggregation[] {
|
||||||
const userEventsMap = new Map<string, Event[]>()
|
const userEventsMap = new Map<string, Event[]>()
|
||||||
|
|
@ -125,21 +95,6 @@ class UserAggregationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sortWithPinned(aggregations: TUserAggregation[]): TUserAggregation[] {
|
|
||||||
const pinned: TUserAggregation[] = []
|
|
||||||
const unpinned: TUserAggregation[] = []
|
|
||||||
|
|
||||||
aggregations.forEach((agg) => {
|
|
||||||
if (this.isPinned(agg.pubkey)) {
|
|
||||||
pinned.push(agg)
|
|
||||||
} else {
|
|
||||||
unpinned.push(agg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...pinned, ...unpinned]
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAggregations(feedId: string, aggregations: TUserAggregation[]) {
|
saveAggregations(feedId: string, aggregations: TUserAggregation[]) {
|
||||||
const map = new Map<string, Event[]>()
|
const map = new Map<string, Event[]>()
|
||||||
aggregations.forEach((agg) => map.set(agg.pubkey, agg.events))
|
aggregations.forEach((agg) => map.set(agg.pubkey, agg.events))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue