FavoriteRelays.ts raw

   1  import { Event, kinds } from 'nostr-tools'
   2  import { Pubkey, RelayUrl, Timestamp } from '../shared'
   3  import { RelaySet } from './RelaySet'
   4  
   5  /**
   6   * Result of a favorite relays modification
   7   */
   8  export type FavoriteRelaysChange =
   9    | { type: 'relay_added'; relay: RelayUrl }
  10    | { type: 'relay_removed'; relay: RelayUrl }
  11    | { type: 'set_added'; set: RelaySet }
  12    | { type: 'set_removed'; setId: string }
  13    | { type: 'no_change' }
  14  
  15  /**
  16   * FavoriteRelays Aggregate
  17   *
  18   * Represents a user's favorite relays collection (kind 10012 in Nostr).
  19   * Combines individual relay URLs and references to relay sets.
  20   *
  21   * This is the user's curated list of relays they want quick access to,
  22   * separate from their mailbox relays (kind 10002).
  23   */
  24  export class FavoriteRelays {
  25    private readonly _relays: Map<string, RelayUrl>
  26    private readonly _sets: Map<string, RelaySet>
  27    private readonly _setOrder: string[]
  28  
  29    private constructor(
  30      private readonly _owner: Pubkey,
  31      relays: RelayUrl[],
  32      sets: RelaySet[]
  33    ) {
  34      this._relays = new Map()
  35      this._sets = new Map()
  36      this._setOrder = []
  37  
  38      for (const relay of relays) {
  39        this._relays.set(relay.value, relay)
  40      }
  41      for (const set of sets) {
  42        this._sets.set(set.id, set)
  43        this._setOrder.push(set.id)
  44      }
  45    }
  46  
  47    /**
  48     * Create an empty FavoriteRelays for a user
  49     */
  50    static empty(owner: Pubkey): FavoriteRelays {
  51      return new FavoriteRelays(owner, [], [])
  52    }
  53  
  54    /**
  55     * Create FavoriteRelays from URLs only
  56     */
  57    static fromUrls(owner: Pubkey, urls: string[]): FavoriteRelays {
  58      const relays: RelayUrl[] = []
  59      for (const url of urls) {
  60        const relay = RelayUrl.tryCreate(url)
  61        if (relay && !relays.some((r) => r.value === relay.value)) {
  62          relays.push(relay)
  63        }
  64      }
  65      return new FavoriteRelays(owner, relays, [])
  66    }
  67  
  68    /**
  69     * Reconstruct FavoriteRelays from a Nostr kind 10012 event
  70     *
  71     * @param event The favorite relays event
  72     * @param relaySets The relay set events referenced by 'a' tags
  73     */
  74    static fromEvent(event: Event, relaySets: RelaySet[] = []): FavoriteRelays {
  75      const owner = Pubkey.fromHex(event.pubkey)
  76      const relays: RelayUrl[] = []
  77      const setIds: string[] = []
  78  
  79      for (const tag of event.tags) {
  80        if (tag[0] === 'relay' && tag[1]) {
  81          const relay = RelayUrl.tryCreate(tag[1])
  82          if (relay && !relays.some((r) => r.value === relay.value)) {
  83            relays.push(relay)
  84          }
  85        } else if (tag[0] === 'a' && tag[1]) {
  86          const [kind, , setId] = tag[1].split(':')
  87          if (kind === kinds.Relaysets.toString() && setId && !setIds.includes(setId)) {
  88            setIds.push(setId)
  89          }
  90        }
  91      }
  92  
  93      // Match relay sets to their IDs in order
  94      const orderedSets: RelaySet[] = []
  95      for (const id of setIds) {
  96        const set = relaySets.find((s) => s.id === id)
  97        if (set) {
  98          orderedSets.push(set)
  99        }
 100      }
 101  
 102      return new FavoriteRelays(owner, relays, orderedSets)
 103    }
 104  
 105    /**
 106     * The owner of this favorite relays list
 107     */
 108    get owner(): Pubkey {
 109      return this._owner
 110    }
 111  
 112    /**
 113     * Number of individual favorite relays
 114     */
 115    get relayCount(): number {
 116      return this._relays.size
 117    }
 118  
 119    /**
 120     * Number of relay sets
 121     */
 122    get setCount(): number {
 123      return this._sets.size
 124    }
 125  
 126    /**
 127     * Get all individual favorite relays
 128     */
 129    getRelays(): RelayUrl[] {
 130      return Array.from(this._relays.values())
 131    }
 132  
 133    /**
 134     * Get all relay URLs as strings
 135     */
 136    getRelayUrls(): string[] {
 137      return Array.from(this._relays.keys())
 138    }
 139  
 140    /**
 141     * Get all relay sets in order
 142     */
 143    getSets(): RelaySet[] {
 144      return this._setOrder.map((id) => this._sets.get(id)!).filter(Boolean)
 145    }
 146  
 147    /**
 148     * Get a relay set by ID
 149     */
 150    getSet(id: string): RelaySet | undefined {
 151      return this._sets.get(id)
 152    }
 153  
 154    /**
 155     * Get all unique relays (from both individual relays and sets)
 156     */
 157    getAllUniqueRelays(): RelayUrl[] {
 158      const all = new Map<string, RelayUrl>()
 159  
 160      for (const relay of this._relays.values()) {
 161        all.set(relay.value, relay)
 162      }
 163  
 164      for (const set of this._sets.values()) {
 165        for (const relay of set.getRelays()) {
 166          all.set(relay.value, relay)
 167        }
 168      }
 169  
 170      return Array.from(all.values())
 171    }
 172  
 173    /**
 174     * Check if a relay is in the favorites
 175     */
 176    hasRelay(relay: RelayUrl): boolean {
 177      return this._relays.has(relay.value)
 178    }
 179  
 180    /**
 181     * Check if a relay set is in the favorites
 182     */
 183    hasSet(id: string): boolean {
 184      return this._sets.has(id)
 185    }
 186  
 187    /**
 188     * Add a relay to favorites
 189     *
 190     * @returns FavoriteRelaysChange indicating what changed
 191     */
 192    addRelay(relay: RelayUrl): FavoriteRelaysChange {
 193      if (this._relays.has(relay.value)) {
 194        return { type: 'no_change' }
 195      }
 196  
 197      this._relays.set(relay.value, relay)
 198      return { type: 'relay_added', relay }
 199    }
 200  
 201    /**
 202     * Add multiple relays to favorites
 203     */
 204    addRelays(relays: RelayUrl[]): FavoriteRelaysChange[] {
 205      return relays.map((r) => this.addRelay(r))
 206    }
 207  
 208    /**
 209     * Add a relay by URL string
 210     */
 211    addRelayUrl(url: string): FavoriteRelaysChange | null {
 212      const relay = RelayUrl.tryCreate(url)
 213      if (!relay) return null
 214      return this.addRelay(relay)
 215    }
 216  
 217    /**
 218     * Remove a relay from favorites
 219     *
 220     * @returns FavoriteRelaysChange indicating what changed
 221     */
 222    removeRelay(relay: RelayUrl): FavoriteRelaysChange {
 223      if (!this._relays.has(relay.value)) {
 224        return { type: 'no_change' }
 225      }
 226  
 227      this._relays.delete(relay.value)
 228      return { type: 'relay_removed', relay }
 229    }
 230  
 231    /**
 232     * Remove multiple relays from favorites
 233     */
 234    removeRelays(relays: RelayUrl[]): FavoriteRelaysChange[] {
 235      return relays.map((r) => this.removeRelay(r))
 236    }
 237  
 238    /**
 239     * Add a relay set to favorites
 240     *
 241     * @returns FavoriteRelaysChange indicating what changed
 242     */
 243    addSet(set: RelaySet): FavoriteRelaysChange {
 244      if (this._sets.has(set.id)) {
 245        return { type: 'no_change' }
 246      }
 247  
 248      this._sets.set(set.id, set)
 249      this._setOrder.push(set.id)
 250      return { type: 'set_added', set }
 251    }
 252  
 253    /**
 254     * Remove a relay set from favorites
 255     *
 256     * @returns FavoriteRelaysChange indicating what changed
 257     */
 258    removeSet(id: string): FavoriteRelaysChange {
 259      if (!this._sets.has(id)) {
 260        return { type: 'no_change' }
 261      }
 262  
 263      this._sets.delete(id)
 264      const index = this._setOrder.indexOf(id)
 265      if (index !== -1) {
 266        this._setOrder.splice(index, 1)
 267      }
 268      return { type: 'set_removed', setId: id }
 269    }
 270  
 271    /**
 272     * Update a relay set
 273     */
 274    updateSet(set: RelaySet): boolean {
 275      if (!this._sets.has(set.id)) {
 276        return false
 277      }
 278      this._sets.set(set.id, set)
 279      return true
 280    }
 281  
 282    /**
 283     * Reorder the favorite relays
 284     */
 285    reorderRelays(newOrder: RelayUrl[]): void {
 286      this._relays.clear()
 287      for (const relay of newOrder) {
 288        this._relays.set(relay.value, relay)
 289      }
 290    }
 291  
 292    /**
 293     * Reorder the relay sets
 294     */
 295    reorderSets(newOrder: RelaySet[]): void {
 296      this._setOrder.length = 0
 297      for (const set of newOrder) {
 298        if (this._sets.has(set.id)) {
 299          this._setOrder.push(set.id)
 300        }
 301      }
 302    }
 303  
 304    /**
 305     * Convert to Nostr event tags format
 306     */
 307    toTags(pubkey: string): string[][] {
 308      const tags: string[][] = []
 309  
 310      for (const relay of this._relays.values()) {
 311        tags.push(['relay', relay.value])
 312      }
 313  
 314      for (const id of this._setOrder) {
 315        const set = this._sets.get(id)
 316        if (set) {
 317          tags.push(['a', `${kinds.Relaysets}:${pubkey}:${id}`])
 318        }
 319      }
 320  
 321      return tags
 322    }
 323  
 324    /**
 325     * Convert to a draft event for publishing
 326     */
 327    toDraftEvent(pubkey: string): {
 328      kind: number
 329      content: string
 330      created_at: number
 331      tags: string[][]
 332    } {
 333      return {
 334        kind: 10012, // ExtendedKind.FAVORITE_RELAYS
 335        content: '',
 336        created_at: Timestamp.now().unix,
 337        tags: this.toTags(pubkey)
 338      }
 339    }
 340  }
 341