feat: pinned users event

This commit is contained in:
codytseng 2025-12-01 00:05:09 +08:00
parent ad016aba35
commit 7ec4835c61
10 changed files with 303 additions and 136 deletions

View file

@ -1397,6 +1397,10 @@ class ClientService extends EventTarget {
return this.fetchReplaceableEvent(pubkey, kinds.UserEmojiList)
}
async fetchPinnedUsersList(pubkey: string) {
return this.fetchReplaceableEvent(pubkey, ExtendedKind.PINNED_USERS)
}
async updateBlossomServerListEventCache(evt: NEvent) {
await this.updateReplaceableEventCache(evt)
}

View file

@ -16,7 +16,6 @@ const StoreNames = {
MUTE_LIST_EVENTS: 'muteListEvents',
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents',
EMOJI_SET_EVENTS: 'emojiSetEvents',
PIN_LIST_EVENTS: 'pinListEvents',
@ -24,6 +23,9 @@ const StoreNames = {
RELAY_SETS: 'relaySets',
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays',
RELAY_INFOS: 'relayInfos',
DECRYPTED_CONTENTS: 'decryptedContents',
PINNED_USERS_EVENTS: 'pinnedUsersEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags', // deprecated
RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated
}
@ -43,7 +45,7 @@ class IndexedDbService {
init(): Promise<void> {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 9)
const request = window.indexedDB.open('jumble', 10)
request.onerror = (event) => {
reject(event)
@ -71,8 +73,8 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
db.createObjectStore(StoreNames.MUTE_DECRYPTED_TAGS, { keyPath: 'key' })
if (!db.objectStoreNames.contains(StoreNames.DECRYPTED_CONTENTS)) {
db.createObjectStore(StoreNames.DECRYPTED_CONTENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
@ -98,9 +100,16 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.PIN_LIST_EVENTS)) {
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)) {
db.deleteObjectStore(StoreNames.RELAY_INFO_EVENTS)
}
if (db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
db.deleteObjectStore(StoreNames.MUTE_DECRYPTED_TAGS)
}
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
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readonly')
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
const request = store.get(id)
const transaction = this.db.transaction(StoreNames.DECRYPTED_CONTENTS, 'readonly')
const store = transaction.objectStore(StoreNames.DECRYPTED_CONTENTS)
const request = store.get(key)
request.onsuccess = () => {
transaction.commit()
resolve((request.result as TValue<string[][]>)?.value)
resolve((request.result as TValue<string>)?.value)
}
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
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readwrite')
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
const transaction = this.db.transaction(StoreNames.DECRYPTED_CONTENTS, 'readwrite')
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 = () => {
transaction.commit()
resolve()
@ -471,6 +480,8 @@ class IndexedDbService {
return StoreNames.EMOJI_SET_EVENTS
case kinds.Pinlist:
return StoreNames.PIN_LIST_EVENTS
case ExtendedKind.PINNED_USERS:
return StoreNames.PINNED_USERS_EVENTS
default:
return undefined
}

View file

@ -57,7 +57,6 @@ class LocalStorageService {
private enableSingleColumnLayout: boolean = true
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
private filterOutOnionRelays: boolean = !isTorBrowser()
private pinnedPubkeys: Set<string> = new Set()
constructor() {
if (!LocalStorageService.instance) {
@ -231,12 +230,8 @@ class LocalStorageService {
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
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
@ -564,18 +559,6 @@ class LocalStorageService {
this.filterOutOnionRelays = filterOut
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()

View file

@ -1,5 +1,4 @@
import { getEventKey } from '@/lib/event'
import storage from '@/services/local-storage.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
import { Event } from 'nostr-tools'
@ -14,7 +13,6 @@ export type TUserAggregation = {
class UserAggregationService {
static instance: UserAggregationService
private pinnedPubkeys: Set<string> = new Set()
private aggregationStore: Map<string, Map<string, Event[]>> = new Map()
private listenersMap: Map<string, Set<() => void>> = new Map()
private lastViewedMap: Map<string, number> = new Map()
@ -24,7 +22,6 @@ class UserAggregationService {
return UserAggregationService.instance
}
UserAggregationService.instance = this
this.pinnedPubkeys = storage.getPinnedPubkeys()
}
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
aggregateByUser(events: Event[]): TUserAggregation[] {
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[]) {
const map = new Map<string, Event[]>()
aggregations.forEach((agg) => map.set(agg.pubkey, agg.events))