custom-emoji.service.ts raw

   1  import { getEmojisAndEmojiSetsFromEvent, getEmojisFromEvent } from '@/lib/event-metadata'
   2  import { parseEmojiPickerUnified } from '@/lib/utils'
   3  import client from '@/services/client.service'
   4  import { TEmoji } from '@/types'
   5  import { sha256 } from '@noble/hashes/sha2'
   6  import { SkinTones } from 'emoji-picker-react'
   7  import { getSuggested, setSuggested } from 'emoji-picker-react/src/dataUtils/suggested'
   8  import FlexSearch from 'flexsearch'
   9  import { Event } from 'nostr-tools'
  10  
  11  class CustomEmojiService {
  12    static instance: CustomEmojiService
  13  
  14    private emojiMap = new Map<string, TEmoji>()
  15    private emojiIndex = new FlexSearch.Index({
  16      tokenize: 'full'
  17    })
  18  
  19    constructor() {
  20      if (!CustomEmojiService.instance) {
  21        CustomEmojiService.instance = this
  22      }
  23      return CustomEmojiService.instance
  24    }
  25  
  26    async init(userEmojiListEvent: Event | null) {
  27      if (!userEmojiListEvent) return
  28  
  29      const { emojis, emojiSetPointers } = getEmojisAndEmojiSetsFromEvent(userEmojiListEvent)
  30      await this.addEmojisToIndex(emojis)
  31  
  32      const emojiSetEvents = await client.fetchEmojiSetEvents(emojiSetPointers, false)
  33      await Promise.allSettled(
  34        emojiSetEvents.map(async (event) => {
  35          if (!event || event instanceof Error) return
  36  
  37          await this.addEmojisToIndex(getEmojisFromEvent(event))
  38        })
  39      )
  40    }
  41  
  42    async searchEmojis(query: string = ''): Promise<string[]> {
  43      if (!query) {
  44        const idSet = new Set<string>()
  45        getSuggested()
  46          .sort((a, b) => b.count - a.count)
  47          .map((item) => parseEmojiPickerUnified(item.unified))
  48          .forEach((item) => {
  49            if (item && typeof item !== 'string') {
  50              const id = this.getEmojiId(item)
  51              if (!idSet.has(id)) {
  52                idSet.add(id)
  53              }
  54            }
  55          })
  56        for (const key of this.emojiMap.keys()) {
  57          idSet.add(key)
  58        }
  59        return Array.from(idSet)
  60      }
  61      const results = await this.emojiIndex.searchAsync(query)
  62      return results.filter((id) => typeof id === 'string') as string[]
  63    }
  64  
  65    getEmojiById(id?: string): TEmoji | undefined {
  66      if (!id) return undefined
  67  
  68      return this.emojiMap.get(id)
  69    }
  70  
  71    getAllCustomEmojisForPicker() {
  72      return Array.from(this.emojiMap.values()).map((emoji) => ({
  73        id: `:${emoji.shortcode}:${emoji.url}`,
  74        imgUrl: emoji.url,
  75        names: [emoji.shortcode]
  76      }))
  77    }
  78  
  79    isCustomEmojiId(shortcode: string) {
  80      return this.emojiMap.has(shortcode)
  81    }
  82  
  83    private async addEmojisToIndex(emojis: TEmoji[]) {
  84      await Promise.allSettled(
  85        emojis.map(async (emoji) => {
  86          const id = this.getEmojiId(emoji)
  87          this.emojiMap.set(id, emoji)
  88          await this.emojiIndex.addAsync(id, emoji.shortcode)
  89        })
  90      )
  91    }
  92  
  93    getEmojiId(emoji: TEmoji) {
  94      const encoder = new TextEncoder()
  95      const data = encoder.encode(`${emoji.shortcode}:${emoji.url}`.toLowerCase())
  96      const hashBuffer = sha256(data)
  97      const hashArray = Array.from(new Uint8Array(hashBuffer))
  98      return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
  99    }
 100  
 101    updateSuggested(id: string) {
 102      const emoji = this.getEmojiById(id)
 103      if (!emoji) return
 104  
 105      setSuggested(
 106        {
 107          n: [emoji.shortcode.toLowerCase()],
 108          u: `:${emoji.shortcode}:${emoji.url}`.toLowerCase(),
 109          a: '0',
 110          imgUrl: emoji.url
 111        },
 112        SkinTones.NEUTRAL
 113      )
 114    }
 115  }
 116  
 117  const instance = new CustomEmojiService()
 118  export default instance
 119