From 1e2385da3bfc9d68a6ad3b8acb778fc9477f85ba Mon Sep 17 00:00:00 2001 From: codytseng Date: Fri, 7 Nov 2025 22:36:07 +0800 Subject: [PATCH] feat: emoji packs --- AGENTS.md | 4 + src/App.tsx | 27 +++-- src/components/BookmarkList/index.tsx | 2 +- .../ContentPreview/EmojiPackPreview.tsx | 23 ++++ src/components/ContentPreview/index.tsx | 5 + src/components/EmojiPackList/index.tsx | 86 +++++++++++++++ src/components/Note/EmojiPack.tsx | 103 ++++++++++++++++++ src/components/Note/index.tsx | 3 + src/components/NoteCard/index.tsx | 5 +- src/components/NoteList/index.tsx | 2 +- src/components/PinnedNoteCard/index.tsx | 2 +- src/components/Settings/index.tsx | 11 ++ src/components/ui/button.tsx | 7 +- src/constants.ts | 3 +- src/i18n/locales/ar.ts | 14 ++- src/i18n/locales/de.ts | 13 ++- src/i18n/locales/en.ts | 13 ++- src/i18n/locales/es.ts | 13 ++- src/i18n/locales/fa.ts | 13 ++- src/i18n/locales/fr.ts | 13 ++- src/i18n/locales/hi.ts | 13 ++- src/i18n/locales/hu.ts | 13 ++- src/i18n/locales/it.ts | 13 ++- src/i18n/locales/ja.ts | 13 ++- src/i18n/locales/ko.ts | 13 ++- src/i18n/locales/pl.ts | 13 ++- src/i18n/locales/pt-BR.ts | 13 ++- src/i18n/locales/pt-PT.ts | 13 ++- src/i18n/locales/ru.ts | 13 ++- src/i18n/locales/th.ts | 13 ++- src/i18n/locales/zh.ts | 13 ++- src/lib/draft-event.ts | 9 ++ src/lib/event-metadata.ts | 14 ++- src/lib/link.ts | 1 + .../secondary/EmojiPackSettingsPage/index.tsx | 43 ++++++++ .../secondary/GeneralSettingsPage/index.tsx | 17 --- src/providers/EmojiPackProvider.tsx | 90 +++++++++++++++ src/providers/NostrProvider/index.tsx | 9 ++ src/routes/secondary.tsx | 2 + src/services/client.service.ts | 9 ++ src/services/indexed-db.service.ts | 6 + 41 files changed, 646 insertions(+), 59 deletions(-) create mode 100644 src/components/ContentPreview/EmojiPackPreview.tsx create mode 100644 src/components/EmojiPackList/index.tsx create mode 100644 src/components/Note/EmojiPack.tsx create mode 100644 src/pages/secondary/EmojiPackSettingsPage/index.tsx create mode 100644 src/providers/EmojiPackProvider.tsx diff --git a/AGENTS.md b/AGENTS.md index 1654755..c9bb26f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,6 +147,8 @@ And some Providers are placed in `PageManager.tsx` because they need to use the ### Internationalization (i18n) +Jumble is a multi-language application. When you add new text content, please ensure to add translations for all supported languages as much as possible. Append new translations to the end of each translation file without modifying or removing existing keys. + - Translation files located in `src/i18n/locales/` - Using `react-i18next` for internationalization - Supported languages: ar, de, en, es, fa, fr, hi, hu, it, ja, ko, pl, pt-BR, pt-PT, ru, th, zh @@ -188,6 +190,8 @@ I mean kinds that are supported to be displayed in the feed. More details you can find in `src/components/Note/`. If you want to add support for new kinds, you need to create new components under `src/components/Note/` and update `src/components/Note/index.tsx`. +And also you need to update `src/components/ContentPreview/` to support preview rendering for the new kinds. `ContentPreview` is used in various places like parent notes, notifications, highlight sources, etc. It only has one line of text space, so you need to figure out a suitable preview display method for different types of content. Use text only as much as possible. + Please avoid modifying the framework, such as avatars, usernames, timestamps, and action buttons in the `Note` component. Only add content rendering logic for new types. ## Common Modification Scenarios diff --git a/src/App.tsx b/src/App.tsx index b5000fc..f9be778 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { Toaster } from '@/components/ui/sonner' import { BookmarksProvider } from '@/providers/BookmarksProvider' import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider' import { DeletedEventProvider } from '@/providers/DeletedEventProvider' +import { EmojiPackProvider } from '@/providers/EmojiPackProvider' import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider' import { FeedProvider } from '@/providers/FeedProvider' import { FollowListProvider } from '@/providers/FollowListProvider' @@ -37,18 +38,20 @@ export default function App(): JSX.Element { - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/components/BookmarkList/index.tsx b/src/components/BookmarkList/index.tsx index 3aa2ee7..b6080e3 100644 --- a/src/components/BookmarkList/index.tsx +++ b/src/components/BookmarkList/index.tsx @@ -91,7 +91,7 @@ function BookmarkedNote({ eventId }: { eventId: string }) { const { event, isFetching } = useFetchEvent(eventId) if (isFetching) { - return + return } if (!event) { diff --git a/src/components/ContentPreview/EmojiPackPreview.tsx b/src/components/ContentPreview/EmojiPackPreview.tsx new file mode 100644 index 0000000..207a406 --- /dev/null +++ b/src/components/ContentPreview/EmojiPackPreview.tsx @@ -0,0 +1,23 @@ +import { getEmojiPackInfoFromEvent } from '@/lib/event-metadata' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function EmojiPackPreview({ + event, + className +}: { + event: Event + className?: string +}) { + const { t } = useTranslation() + const { title, emojis } = useMemo(() => getEmojiPackInfoFromEvent(event), [event]) + + return ( +
+ [{t('Emoji Pack')}] {title} + {emojis.length > 0 && ({emojis.length})} +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index e3e597d..18aeaa6 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -7,6 +7,7 @@ import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import CommunityDefinitionPreview from './CommunityDefinitionPreview' +import EmojiPackPreview from './EmojiPackPreview' import GroupMetadataPreview from './GroupMetadataPreview' import HighlightPreview from './HighlightPreview' import LiveEventPreview from './LiveEventPreview' @@ -100,5 +101,9 @@ export default function ContentPreview({ return } + if (event.kind === kinds.Emojisets) { + return + } + return
[{t('Cannot handle event of kind k', { k: event.kind })}]
} diff --git a/src/components/EmojiPackList/index.tsx b/src/components/EmojiPackList/index.tsx new file mode 100644 index 0000000..0169e9c --- /dev/null +++ b/src/components/EmojiPackList/index.tsx @@ -0,0 +1,86 @@ +import { useFetchEvent } from '@/hooks' +import { generateBech32IdFromATag } from '@/lib/tag' +import { useNostr } from '@/providers/NostrProvider' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' + +const SHOW_COUNT = 10 + +export default function EmojiPackList() { + const { t } = useTranslation() + const { userEmojiListEvent } = useNostr() + const eventIds = useMemo(() => { + if (!userEmojiListEvent) return [] + + return ( + userEmojiListEvent.tags + .map((tag) => (tag[0] === 'a' ? generateBech32IdFromATag(tag) : null)) + .filter(Boolean) as `naddr1${string}`[] + ).reverse() + }, [userEmojiListEvent]) + const [showCount, setShowCount] = useState(SHOW_COUNT) + const bottomRef = useRef(null) + + useEffect(() => { + const options = { + root: null, + rootMargin: '10px', + threshold: 0.1 + } + + const loadMore = () => { + if (showCount < eventIds.length) { + setShowCount((prev) => prev + SHOW_COUNT) + } + } + + const observerInstance = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + loadMore() + } + }, options) + + const currentBottomRef = bottomRef.current + + if (currentBottomRef) { + observerInstance.observe(currentBottomRef) + } + + return () => { + if (observerInstance && currentBottomRef) { + observerInstance.unobserve(currentBottomRef) + } + } + }, [showCount, eventIds]) + + if (eventIds.length === 0) { + return ( +
+ {t('no emoji packs found')} +
+ ) + } + + return ( +
+ {eventIds.slice(0, showCount).map((eventId) => ( + + ))} +
+ ) +} + +function EmojiPackNote({ eventId }: { eventId: string }) { + const { event, isFetching } = useFetchEvent(eventId) + + if (isFetching) { + return + } + + if (!event) { + return null + } + + return +} diff --git a/src/components/Note/EmojiPack.tsx b/src/components/Note/EmojiPack.tsx new file mode 100644 index 0000000..404b805 --- /dev/null +++ b/src/components/Note/EmojiPack.tsx @@ -0,0 +1,103 @@ +import { Button } from '@/components/ui/button' +import { getReplaceableCoordinateFromEvent } from '@/lib/event' +import { getEmojiPackInfoFromEvent } from '@/lib/event-metadata' +import { useEmojiPack } from '@/providers/EmojiPackProvider' +import { useNostr } from '@/providers/NostrProvider' +import { CheckIcon, Loader, PlusIcon } from 'lucide-react' +import { Event } from 'nostr-tools' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import Image from '../Image' + +export default function EmojiPack({ event, className }: { event: Event; className?: string }) { + const { t } = useTranslation() + const { pubkey: accountPubkey, checkLogin } = useNostr() + const { emojiPackCoordinateSet, addEmojiPack, removeEmojiPack } = useEmojiPack() + const [updating, setUpdating] = useState(false) + const { title, emojis } = useMemo(() => getEmojiPackInfoFromEvent(event), [event]) + const coordinate = useMemo(() => getReplaceableCoordinateFromEvent(event), [event]) + const isCollected = useMemo(() => { + return emojiPackCoordinateSet.has(coordinate) + }, [emojiPackCoordinateSet, coordinate]) + + const handleCollect = async (e: React.MouseEvent) => { + e.stopPropagation() + checkLogin(async () => { + if (isCollected) return + + setUpdating(true) + try { + await addEmojiPack(event) + toast.success(t('Emoji pack added')) + } catch (error) { + toast.error(t('Add emoji pack failed') + ': ' + (error as Error).message) + } finally { + setUpdating(false) + } + }) + } + + const handleRemoveCollect = async (e: React.MouseEvent) => { + e.stopPropagation() + checkLogin(async () => { + if (!isCollected) return + + setUpdating(true) + try { + await removeEmojiPack(event) + toast.success(t('Emoji pack removed')) + } catch (error) { + toast.error(t('Remove emoji pack failed') + ': ' + (error as Error).message) + } finally { + setUpdating(false) + } + }) + } + + return ( +
+
+

{title}

+ {accountPubkey && ( + + )} +
+
+ {emojis.map((emoji, index) => ( + + ))} +
+
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 5608625..2d01251 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -19,6 +19,7 @@ import TranslateButton from '../TranslateButton' import UserAvatar from '../UserAvatar' import Username from '../Username' import CommunityDefinition from './CommunityDefinition' +import EmojiPack from './EmojiPack' import GroupMetadata from './GroupMetadata' import Highlight from './Highlight' import IValue from './IValue' @@ -102,6 +103,8 @@ export default function Note({ content = } else if (event.kind === ExtendedKind.RELAY_REVIEW) { content = + } else if (event.kind === kinds.Emojisets) { + content = } else { content = } diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index b095315..05d4f0a 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -1,5 +1,6 @@ import { Skeleton } from '@/components/ui/skeleton' import { isMentioningMutedUsers } from '@/lib/event' +import { cn } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' @@ -46,9 +47,9 @@ export default function NoteCard({ return } -export function NoteCardLoadingSkeleton() { +export function NoteCardLoadingSkeleton({ className }: { className?: string }) { return ( -
+
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 6c016f8..856e9cd 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -394,7 +394,7 @@ const NoteList = forwardRef( ) : (
)} diff --git a/src/components/PinnedNoteCard/index.tsx b/src/components/PinnedNoteCard/index.tsx index b0cc387..07910db 100644 --- a/src/components/PinnedNoteCard/index.tsx +++ b/src/components/PinnedNoteCard/index.tsx @@ -11,7 +11,7 @@ export default function PinnedNoteCard({ const { event, isFetching } = useFetchEvent(eventId) if (isFetching) { - return + return } if (!event) { diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index dfbfabd..cc13475 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -2,6 +2,7 @@ import AboutInfoDialog from '@/components/AboutInfoDialog' import Donation from '@/components/Donation' import { toAppearanceSettings, + toEmojiPackSettings, toGeneralSettings, toPostSettings, toRelaySettings, @@ -22,6 +23,7 @@ import { PencilLine, Server, Settings2, + Smile, Wallet } from 'lucide-react' import { forwardRef, HTMLProps, useState } from 'react' @@ -84,6 +86,15 @@ export default function Settings() { )} + {!!pubkey && ( + push(toEmojiPackSettings())}> +
+ +
{t('Emoji Packs')}
+
+ +
+ )} {!!nsec && ( { - if (tagName === 'emoji' && tagValues.length >= 2) { + if (tagName === 'title' && tagValues[0]) { + title = tagValues[0] + } else if (tagName === 'emoji' && tagValues.length >= 2) { emojis.push({ shortcode: tagValues[0], url: tagValues[1] @@ -367,7 +370,12 @@ export function getEmojisFromEvent(event: Event): TEmoji[] { } }) - return emojis + return { title, emojis } +} + +export function getEmojisFromEvent(event: Event): TEmoji[] { + const info = getEmojiPackInfoFromEvent(event) + return info.emojis } export function getStarsFromRelayReviewEvent(event: Event): number { diff --git a/src/lib/link.ts b/src/lib/link.ts index 1cf8040..473fb47 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -71,6 +71,7 @@ export const toPostSettings = () => '/settings/posts' export const toGeneralSettings = () => '/settings/general' export const toAppearanceSettings = () => '/settings/appearance' export const toTranslation = () => '/settings/translation' +export const toEmojiPackSettings = () => '/settings/emoji-packs' export const toProfileEditor = () => '/profile-editor' export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}` export const toRelayReviews = (url: string) => `/relays/${encodeURIComponent(url)}/reviews` diff --git a/src/pages/secondary/EmojiPackSettingsPage/index.tsx b/src/pages/secondary/EmojiPackSettingsPage/index.tsx new file mode 100644 index 0000000..2f3a251 --- /dev/null +++ b/src/pages/secondary/EmojiPackSettingsPage/index.tsx @@ -0,0 +1,43 @@ +import EmojiPackList from '@/components/EmojiPackList' +import NoteList from '@/components/NoteList' +import Tabs from '@/components/Tabs' +import { BIG_RELAY_URLS } from '@/constants' +import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' +import { useUserTrust } from '@/providers/UserTrustProvider' +import { kinds } from 'nostr-tools' +import { forwardRef, useState } from 'react' +import { useTranslation } from 'react-i18next' + +type TTab = 'my-packs' | 'explore' + +const EmojiPackSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { + const { t } = useTranslation() + const { hideUntrustedNotes } = useUserTrust() + const [tab, setTab] = useState('my-packs') + + return ( + + { + setTab(tab as TTab) + }} + /> + {tab === 'my-packs' ? ( + + ) : ( + + )} + + ) +}) +EmojiPackSettingsPage.displayName = 'EmojiPackSettingsPage' +export default EmojiPackSettingsPage diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx index 7d3e7e4..b303c31 100644 --- a/src/pages/secondary/GeneralSettingsPage/index.tsx +++ b/src/pages/secondary/GeneralSettingsPage/index.tsx @@ -9,7 +9,6 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useUserTrust } from '@/providers/UserTrustProvider' import { TMediaAutoLoadPolicy } from '@/types' import { SelectValue } from '@radix-ui/react-select' -import { ExternalLink } from 'lucide-react' import { forwardRef, HTMLProps, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -109,22 +108,6 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { - -
- - {t('Custom emoji management')} - - -
- {t('After changing emojis, you may need to refresh the page')} -
-
-
) diff --git a/src/providers/EmojiPackProvider.tsx b/src/providers/EmojiPackProvider.tsx new file mode 100644 index 0000000..9134b5c --- /dev/null +++ b/src/providers/EmojiPackProvider.tsx @@ -0,0 +1,90 @@ +import { buildATag, createUserEmojiListDraftEvent } from '@/lib/draft-event' +import { getReplaceableCoordinateFromEvent } from '@/lib/event' +import client from '@/services/client.service' +import { Event, kinds } from 'nostr-tools' +import { createContext, useContext, useMemo } from 'react' +import { useNostr } from './NostrProvider' + +type TEmojiPackContext = { + emojiPackCoordinateSet: Set + addEmojiPack: (event: Event) => Promise + removeEmojiPack: (event: Event) => Promise +} + +const EmojiPackContext = createContext(undefined) + +export const useEmojiPack = () => { + const context = useContext(EmojiPackContext) + if (!context) { + throw new Error('useEmojiPack must be used within a EmojiPackProvider') + } + return context +} + +export function EmojiPackProvider({ children }: { children: React.ReactNode }) { + const { + pubkey: accountPubkey, + userEmojiListEvent, + publish, + updateUserEmojiListEvent + } = useNostr() + const emojiPackCoordinateSet = useMemo(() => { + const set = new Set() + userEmojiListEvent?.tags.forEach((tag) => { + if (tag[0] === 'a') { + set.add(tag[1]) + } + }) + return set + }, [userEmojiListEvent]) + + const addEmojiPack = async (event: Event) => { + if (!accountPubkey || event.kind !== kinds.Emojisets) return + + const userEmojiListEvent = await client.fetchUserEmojiListEvent(accountPubkey) + const currentTags = userEmojiListEvent?.tags || [] + const coordinate = getReplaceableCoordinateFromEvent(event) + + // Check if already exists + if (currentTags.some((tag) => tag[0] === 'a' && tag[1] === coordinate)) { + return + } + + const newUserEmojiListDraftEvent = createUserEmojiListDraftEvent( + [...currentTags, buildATag(event)], + userEmojiListEvent?.content + ) + const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) + await updateUserEmojiListEvent(newUserEmojiListEvent) + } + + const removeEmojiPack = async (event: Event) => { + if (!accountPubkey) return + + const userEmojiListEvent = await client.fetchUserEmojiListEvent(accountPubkey) + if (!userEmojiListEvent) return + + const coordinate = getReplaceableCoordinateFromEvent(event) + const newTags = userEmojiListEvent.tags.filter((tag) => tag[0] !== 'a' || tag[1] !== coordinate) + if (newTags.length === userEmojiListEvent.tags.length) return + + const newUserEmojiListDraftEvent = createUserEmojiListDraftEvent( + newTags, + userEmojiListEvent.content + ) + const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) + await updateUserEmojiListEvent(newUserEmojiListEvent) + } + + return ( + + {children} + + ) +} diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 570f91a..448ea82 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -86,6 +86,7 @@ type TNostrContext = { updateMuteListEvent: (muteListEvent: Event, privateTags: string[][]) => Promise updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise + updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise updatePinListEvent: (pinListEvent: Event) => Promise updateNotificationsSeenAt: (skipPublish?: boolean) => Promise } @@ -743,6 +744,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setFavoriteRelaysEvent(newFavoriteRelaysEvent) } + const updateUserEmojiListEvent = async (userEmojiListEvent: Event) => { + const newUserEmojiListEvent = await indexedDb.putReplaceableEvent(userEmojiListEvent) + if (newUserEmojiListEvent.id !== userEmojiListEvent.id) return + + setUserEmojiListEvent(newUserEmojiListEvent) + } + const updatePinListEvent = async (pinListEvent: Event) => { const newPinListEvent = await indexedDb.putReplaceableEvent(pinListEvent) if (newPinListEvent.id !== pinListEvent.id) return @@ -813,6 +821,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { updateMuteListEvent, updateBookmarkListEvent, updateFavoriteRelaysEvent, + updateUserEmojiListEvent, updatePinListEvent, updateNotificationsSeenAt }} diff --git a/src/routes/secondary.tsx b/src/routes/secondary.tsx index 950c606..eb3283e 100644 --- a/src/routes/secondary.tsx +++ b/src/routes/secondary.tsx @@ -1,5 +1,6 @@ import AppearanceSettingsPage from '@/pages/secondary/AppearanceSettingsPage' import BookmarkPage from '@/pages/secondary/BookmarkPage' +import EmojiPackSettingsPage from '@/pages/secondary/EmojiPackSettingsPage' import FollowingListPage from '@/pages/secondary/FollowingListPage' import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage' import MuteListPage from '@/pages/secondary/MuteListPage' @@ -39,6 +40,7 @@ const SECONDARY_ROUTE_CONFIGS = [ { path: '/settings/general', element: }, { path: '/settings/appearance', element: }, { path: '/settings/translation', element: }, + { path: '/settings/emoji-packs', element: }, { path: '/profile-editor', element: }, { path: '/mutes', element: }, { path: '/rizful', element: }, diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 549dee4..72ad36d 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -759,6 +759,11 @@ class ClientService extends EventTarget { if (cache) { return cache } + const indexedDbCache = await indexedDb.getReplaceableEventByCoordinate(coordinate) + if (indexedDbCache) { + this.replaceableEventCacheMap.set(coordinate, indexedDbCache) + return indexedDbCache + } } else if (eventId) { const cache = this.eventCacheMap.get(eventId) if (cache) { @@ -1356,6 +1361,10 @@ class ClientService extends EventTarget { return this.fetchReplaceableEvent(pubkey, kinds.Pinlist) } + async fetchUserEmojiListEvent(pubkey: string) { + return this.fetchReplaceableEvent(pubkey, kinds.UserEmojiList) + } + async updateBlossomServerListEventCache(evt: NEvent) { await this.updateReplaceableEventCache(evt) } diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 9662ab5..18150a0 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -189,6 +189,12 @@ class IndexedDbService { }) } + async getReplaceableEventByCoordinate(coordinate: string): Promise { + const [kind, pubkey, ...rest] = coordinate.split(':') + const d = rest.length > 0 ? rest.join(':') : undefined + return this.getReplaceableEvent(pubkey, parseInt(kind), d) + } + async getReplaceableEvent( pubkey: string, kind: number,