feat: emoji packs
This commit is contained in:
@@ -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
|
||||
|
||||
27
src/App.tsx
27
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 {
|
||||
<MuteListProvider>
|
||||
<UserTrustProvider>
|
||||
<BookmarksProvider>
|
||||
<PinListProvider>
|
||||
<FeedProvider>
|
||||
<ReplyProvider>
|
||||
<MediaUploadServiceProvider>
|
||||
<KindFilterProvider>
|
||||
<PageManager />
|
||||
<Toaster />
|
||||
</KindFilterProvider>
|
||||
</MediaUploadServiceProvider>
|
||||
</ReplyProvider>
|
||||
</FeedProvider>
|
||||
</PinListProvider>
|
||||
<EmojiPackProvider>
|
||||
<PinListProvider>
|
||||
<FeedProvider>
|
||||
<ReplyProvider>
|
||||
<MediaUploadServiceProvider>
|
||||
<KindFilterProvider>
|
||||
<PageManager />
|
||||
<Toaster />
|
||||
</KindFilterProvider>
|
||||
</MediaUploadServiceProvider>
|
||||
</ReplyProvider>
|
||||
</FeedProvider>
|
||||
</PinListProvider>
|
||||
</EmojiPackProvider>
|
||||
</BookmarksProvider>
|
||||
</UserTrustProvider>
|
||||
</MuteListProvider>
|
||||
|
||||
@@ -91,7 +91,7 @@ function BookmarkedNote({ eventId }: { eventId: string }) {
|
||||
const { event, isFetching } = useFetchEvent(eventId)
|
||||
|
||||
if (isFetching) {
|
||||
return <NoteCardLoadingSkeleton />
|
||||
return <NoteCardLoadingSkeleton className="border-b" />
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
|
||||
23
src/components/ContentPreview/EmojiPackPreview.tsx
Normal file
23
src/components/ContentPreview/EmojiPackPreview.tsx
Normal file
@@ -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 (
|
||||
<div className={cn('pointer-events-none', className)}>
|
||||
[{t('Emoji Pack')}] <span className="italic pr-0.5">{title}</span>
|
||||
{emojis.length > 0 && <span>({emojis.length})</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 <LiveEventPreview event={event} className={className} />
|
||||
}
|
||||
|
||||
if (event.kind === kinds.Emojisets) {
|
||||
return <EmojiPackPreview event={event} className={className} />
|
||||
}
|
||||
|
||||
return <div className={className}>[{t('Cannot handle event of kind k', { k: event.kind })}]</div>
|
||||
}
|
||||
|
||||
86
src/components/EmojiPackList/index.tsx
Normal file
86
src/components/EmojiPackList/index.tsx
Normal file
@@ -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<HTMLDivElement | null>(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 (
|
||||
<div className="mt-2 text-sm text-center text-muted-foreground">
|
||||
{t('no emoji packs found')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{eventIds.slice(0, showCount).map((eventId) => (
|
||||
<EmojiPackNote key={eventId} eventId={eventId} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EmojiPackNote({ eventId }: { eventId: string }) {
|
||||
const { event, isFetching } = useFetchEvent(eventId)
|
||||
|
||||
if (isFetching) {
|
||||
return <NoteCardLoadingSkeleton className="border-b" />
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <NoteCard event={event} className="w-full" />
|
||||
}
|
||||
103
src/components/Note/EmojiPack.tsx
Normal file
103
src/components/Note/EmojiPack.tsx
Normal file
@@ -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 (
|
||||
<div className={className}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-2xl font-semibold">{title}</h3>
|
||||
{accountPubkey && (
|
||||
<Button
|
||||
variant={isCollected ? 'secondary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={isCollected ? handleRemoveCollect : handleCollect}
|
||||
disabled={updating}
|
||||
className="shrink-0"
|
||||
>
|
||||
{updating ? (
|
||||
<Loader className="animate-spin mr-1" />
|
||||
) : isCollected ? (
|
||||
<CheckIcon />
|
||||
) : (
|
||||
<PlusIcon />
|
||||
)}
|
||||
{updating
|
||||
? isCollected
|
||||
? t('Removing...')
|
||||
: t('Adding...')
|
||||
: isCollected
|
||||
? t('Added')
|
||||
: t('Add')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{emojis.map((emoji, index) => (
|
||||
<Image
|
||||
key={`emoji-${index}`}
|
||||
image={{ url: emoji.url, pubkey: event.pubkey }}
|
||||
className="size-14 object-contain"
|
||||
classNames={{
|
||||
wrapper: 'size-14 flex items-center justify-center p-1',
|
||||
errorPlaceholder: 'size-14'
|
||||
}}
|
||||
hideIfError
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 = <VideoNote className="mt-2" event={event} />
|
||||
} else if (event.kind === ExtendedKind.RELAY_REVIEW) {
|
||||
content = <RelayReview className="mt-2" event={event} />
|
||||
} else if (event.kind === kinds.Emojisets) {
|
||||
content = <EmojiPack className="mt-2" event={event} />
|
||||
} else {
|
||||
content = <Content className="mt-2" event={event} />
|
||||
}
|
||||
|
||||
@@ -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 <MainNoteCard event={event} className={className} pinned={pinned} reposters={reposters} />
|
||||
}
|
||||
|
||||
export function NoteCardLoadingSkeleton() {
|
||||
export function NoteCardLoadingSkeleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className="px-4 py-3">
|
||||
<div className={cn('px-4 py-3', className)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Skeleton className="w-10 h-10 rounded-full" />
|
||||
<div className={`flex-1 w-0`}>
|
||||
|
||||
@@ -394,7 +394,7 @@ const NoteList = forwardRef(
|
||||
) : (
|
||||
<div className="flex justify-center w-full mt-2">
|
||||
<Button size="lg" onClick={() => setRefreshCount((count) => count + 1)}>
|
||||
{t('reload notes')}
|
||||
{t('Reload')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function PinnedNoteCard({
|
||||
const { event, isFetching } = useFetchEvent(eventId)
|
||||
|
||||
if (isFetching) {
|
||||
return <NoteCardLoadingSkeleton />
|
||||
return <NoteCardLoadingSkeleton className="border-b" />
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
|
||||
@@ -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() {
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toEmojiPackSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Smile />
|
||||
<div>{t('Emoji Packs')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!nsec && (
|
||||
<SettingItem
|
||||
className="clickable"
|
||||
|
||||
@@ -10,10 +10,9 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
'secondary-2': 'bg-secondary text-secondary-foreground hover:bg-primary',
|
||||
ghost: 'clickable hover:text-accent-foreground',
|
||||
'ghost-destructive': 'cursor-pointer hover:bg-destructive/20 text-destructive',
|
||||
|
||||
@@ -92,7 +92,8 @@ export const SUPPORTED_KINDS = [
|
||||
ExtendedKind.VOICE_COMMENT,
|
||||
kinds.Highlights,
|
||||
kinds.LongFormArticle,
|
||||
ExtendedKind.RELAY_REVIEW
|
||||
ExtendedKind.RELAY_REVIEW,
|
||||
kinds.Emojisets
|
||||
]
|
||||
|
||||
export const URL_REGEX =
|
||||
|
||||
@@ -491,6 +491,18 @@ export default {
|
||||
'Explore Relays': 'استكشف المرحلات',
|
||||
'Choose a feed': 'اختر خلاصة',
|
||||
'and {{x}} others': 'و {{x}} آخرون',
|
||||
selfZapWarning: 'Jumble غير مسؤولة عما يحدث إذا أرسلت zap لنفسك. تابع على مسؤوليتك الخاصة. 😉⚡'
|
||||
selfZapWarning:
|
||||
'Jumble غير مسؤولة عما يحدث إذا أرسلت zap لنفسك. تابع على مسؤوليتك الخاصة. 😉⚡',
|
||||
'Emoji Pack': 'حزمة الرموز التعبيرية',
|
||||
'Emoji pack added': 'تمت إضافة حزمة الرموز التعبيرية',
|
||||
'Add emoji pack failed': 'فشل إضافة حزمة الرموز التعبيرية',
|
||||
'Emoji pack removed': 'تمت إزالة حزمة الرموز التعبيرية',
|
||||
'Remove emoji pack failed': 'فشل إزالة حزمة الرموز التعبيرية',
|
||||
Added: 'تمت الإضافة',
|
||||
'Emoji Packs': 'حزم الرموز التعبيرية',
|
||||
'My Packs': 'حزمي',
|
||||
'Adding...': 'جاري الإضافة...',
|
||||
'Removing...': 'جاري الإزالة...',
|
||||
Reload: 'إعادة التحميل'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,17 @@ export default {
|
||||
'Choose a feed': 'Wähle einen Feed',
|
||||
'and {{x}} others': 'und {{x}} andere',
|
||||
selfZapWarning:
|
||||
'Jumble ist nicht verantwortlich für das, was passiert, wenn Sie sich selbst zappen. Fahren Sie auf eigene Gefahr fort. 😉⚡'
|
||||
'Jumble ist nicht verantwortlich für das, was passiert, wenn Sie sich selbst zappen. Fahren Sie auf eigene Gefahr fort. 😉⚡',
|
||||
'Emoji Pack': 'Emoji-Paket',
|
||||
'Emoji pack added': 'Emoji-Paket hinzugefügt',
|
||||
'Add emoji pack failed': 'Hinzufügen des Emoji-Pakets fehlgeschlagen',
|
||||
'Emoji pack removed': 'Emoji-Paket entfernt',
|
||||
'Remove emoji pack failed': 'Entfernen des Emoji-Pakets fehlgeschlagen',
|
||||
Added: 'Hinzugefügt',
|
||||
'Emoji Packs': 'Emoji-Pakete',
|
||||
'My Packs': 'Meine Pakete',
|
||||
'Adding...': 'Wird hinzugefügt...',
|
||||
'Removing...': 'Wird entfernt...',
|
||||
Reload: 'Neu laden'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +491,17 @@ export default {
|
||||
'Choose a feed': 'Choose a feed',
|
||||
'and {{x}} others': 'and {{x}} others',
|
||||
selfZapWarning:
|
||||
'Jumble is not responsible for what happens if you zap yourself. Proceed at your own risk. 😉⚡'
|
||||
'Jumble is not responsible for what happens if you zap yourself. Proceed at your own risk. 😉⚡',
|
||||
'Emoji Pack': 'Emoji Pack',
|
||||
'Emoji pack added': 'Emoji pack added',
|
||||
'Add emoji pack failed': 'Add emoji pack failed',
|
||||
'Emoji pack removed': 'Emoji pack removed',
|
||||
'Remove emoji pack failed': 'Remove emoji pack failed',
|
||||
Added: 'Added',
|
||||
'Emoji Packs': 'Emoji Packs',
|
||||
'My Packs': 'My Packs',
|
||||
'Adding...': 'Adding...',
|
||||
'Removing...': 'Removing...',
|
||||
Reload: 'Reload'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +500,17 @@ export default {
|
||||
'Choose a feed': 'Elige un feed',
|
||||
'and {{x}} others': 'y {{x}} otros',
|
||||
selfZapWarning:
|
||||
'Jumble no se hace responsable de lo que suceda si te zapeas a ti mismo. Procede bajo tu propio riesgo. 😉⚡'
|
||||
'Jumble no se hace responsable de lo que suceda si te zapeas a ti mismo. Procede bajo tu propio riesgo. 😉⚡',
|
||||
'Emoji Pack': 'Paquete de Emojis',
|
||||
'Emoji pack added': 'Paquete de emojis añadido',
|
||||
'Add emoji pack failed': 'Error al añadir paquete de emojis',
|
||||
'Emoji pack removed': 'Paquete de emojis eliminado',
|
||||
'Remove emoji pack failed': 'Error al eliminar paquete de emojis',
|
||||
Added: 'Añadido',
|
||||
'Emoji Packs': 'Paquetes de Emojis',
|
||||
'My Packs': 'Mis Paquetes',
|
||||
'Adding...': 'Añadiendo...',
|
||||
'Removing...': 'Eliminando...',
|
||||
Reload: 'Recargar'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,17 @@ export default {
|
||||
'Choose a feed': 'یک فید انتخاب کنید',
|
||||
'and {{x}} others': 'و {{x}} دیگر',
|
||||
selfZapWarning:
|
||||
'Jumble مسئولیتی در قبال اتفاقاتی که در صورت ارسال zap به خودتان میافتد ندارد. با مسئولیت خود ادامه دهید. 😉⚡'
|
||||
'Jumble مسئولیتی در قبال اتفاقاتی که در صورت ارسال zap به خودتان میافتد ندارد. با مسئولیت خود ادامه دهید. 😉⚡',
|
||||
'Emoji Pack': 'بسته ایموجی',
|
||||
'Emoji pack added': 'بسته ایموجی اضافه شد',
|
||||
'Add emoji pack failed': 'افزودن بسته ایموجی ناموفق بود',
|
||||
'Emoji pack removed': 'بسته ایموجی حذف شد',
|
||||
'Remove emoji pack failed': 'حذف بسته ایموجی ناموفق بود',
|
||||
Added: 'اضافه شد',
|
||||
'Emoji Packs': 'بستههای ایموجی',
|
||||
'My Packs': 'بستههای من',
|
||||
'Adding...': 'در حال افزودن...',
|
||||
'Removing...': 'در حال حذف...',
|
||||
Reload: 'بازخوانی'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +505,17 @@ export default {
|
||||
'Choose a feed': 'Choisir un fil',
|
||||
'and {{x}} others': 'et {{x}} autres',
|
||||
selfZapWarning:
|
||||
"Jumble n'est pas responsable de ce qui se passe si vous vous zappez vous-même. Procédez à vos risques et périls. 😉⚡"
|
||||
"Jumble n'est pas responsable de ce qui se passe si vous vous zappez vous-même. Procédez à vos risques et périls. 😉⚡",
|
||||
'Emoji Pack': "Pack d'Emojis",
|
||||
'Emoji pack added': "Pack d'emojis ajouté",
|
||||
'Add emoji pack failed': "Échec de l'ajout du pack d'emojis",
|
||||
'Emoji pack removed': "Pack d'emojis supprimé",
|
||||
'Remove emoji pack failed': "Échec de la suppression du pack d'emojis",
|
||||
Added: 'Ajouté',
|
||||
'Emoji Packs': "Packs d'Emojis",
|
||||
'My Packs': 'Mes Packs',
|
||||
'Adding...': 'Ajout...',
|
||||
'Removing...': 'Suppression...',
|
||||
Reload: 'Recharger'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +497,17 @@ export default {
|
||||
'Choose a feed': 'एक फीड चुनें',
|
||||
'and {{x}} others': 'और {{x}} अन्य',
|
||||
selfZapWarning:
|
||||
'Jumble आपके द्वारा स्वयं को zap करने पर क्या होता है, इसके लिए जिम्मेदार नहीं है। अपनी जोखिम पर आगे बढ़ें। 😉⚡'
|
||||
'Jumble आपके द्वारा स्वयं को zap करने पर क्या होता है, इसके लिए जिम्मेदार नहीं है। अपनी जोखिम पर आगे बढ़ें। 😉⚡',
|
||||
'Emoji Pack': 'इमोजी पैक',
|
||||
'Emoji pack added': 'इमोजी पैक जोड़ा गया',
|
||||
'Add emoji pack failed': 'इमोजी पैक जोड़ना विफल रहा',
|
||||
'Emoji pack removed': 'इमोजी पैक हटाया गया',
|
||||
'Remove emoji pack failed': 'इमोजी पैक हटाना विफल रहा',
|
||||
Added: 'जोड़ा गया',
|
||||
'Emoji Packs': 'इमोजी पैक',
|
||||
'My Packs': 'मेरे पैक',
|
||||
'Adding...': 'जोड़ा जा रहा है...',
|
||||
'Removing...': 'हटाया जा रहा है...',
|
||||
Reload: 'रीलोड करें'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +492,17 @@ export default {
|
||||
'Jumble egy kliens, amivel könnyen böngészhetsz csomópontokat. Kezdd az érdekes csomópontok felderítésével, vagy lépj be, hogy a követettek posztjait megnézd.',
|
||||
'Explore Relays': 'Csomópontok felderítése',
|
||||
'Choose a feed': 'Válassz hírfolyamot',
|
||||
'and {{x}} others': 'és {{x}} másik'
|
||||
'and {{x}} others': 'és {{x}} másik',
|
||||
'Emoji Pack': 'Emoji csomag',
|
||||
'Emoji pack added': 'Emoji csomag hozzáadva',
|
||||
'Add emoji pack failed': 'Emoji csomag hozzáadása sikertelen',
|
||||
'Emoji pack removed': 'Emoji csomag eltávolítva',
|
||||
'Remove emoji pack failed': 'Emoji csomag eltávolítása sikertelen',
|
||||
Added: 'Hozzáadva',
|
||||
'Emoji Packs': 'Emoji csomagok',
|
||||
'My Packs': 'Saját csomagjaim',
|
||||
'Adding...': 'Hozzáadás...',
|
||||
'Removing...': 'Eltávolítás...',
|
||||
Reload: 'Újratöltés'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +500,17 @@ export default {
|
||||
'Choose a feed': 'Scegli un feed',
|
||||
'and {{x}} others': 'e altri {{x}}',
|
||||
selfZapWarning:
|
||||
'Jumble non è responsabile di ciò che accade se zappi te stesso. Procedi a tuo rischio e pericolo. 😉⚡'
|
||||
'Jumble non è responsabile di ciò che accade se zappi te stesso. Procedi a tuo rischio e pericolo. 😉⚡',
|
||||
'Emoji Pack': 'Pacchetto Emoji',
|
||||
'Emoji pack added': 'Pacchetto emoji aggiunto',
|
||||
'Add emoji pack failed': 'Aggiunta del pacchetto emoji non riuscita',
|
||||
'Emoji pack removed': 'Pacchetto emoji rimosso',
|
||||
'Remove emoji pack failed': 'Rimozione del pacchetto emoji non riuscita',
|
||||
Added: 'Aggiunto',
|
||||
'Emoji Packs': 'Pacchetti Emoji',
|
||||
'My Packs': 'I Miei Pacchetti',
|
||||
'Adding...': 'Aggiunta...',
|
||||
'Removing...': 'Rimozione...',
|
||||
Reload: 'Ricarica'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +496,17 @@ export default {
|
||||
'Choose a feed': 'フィードを選択',
|
||||
'and {{x}} others': 'および他{{x}}人',
|
||||
selfZapWarning:
|
||||
'Jumble は、あなたが自分自身にザップした場合の結果について責任を負いません。自己責任で続行してください。😉⚡'
|
||||
'Jumble は、あなたが自分自身にザップした場合の結果について責任を負いません。自己責任で続行してください。😉⚡',
|
||||
'Emoji Pack': '絵文字パック',
|
||||
'Emoji pack added': '絵文字パックを追加しました',
|
||||
'Add emoji pack failed': '絵文字パックの追加に失敗しました',
|
||||
'Emoji pack removed': '絵文字パックを削除しました',
|
||||
'Remove emoji pack failed': '絵文字パックの削除に失敗しました',
|
||||
Added: '追加済み',
|
||||
'Emoji Packs': '絵文字パック',
|
||||
'My Packs': 'マイパック',
|
||||
'Adding...': '追加中...',
|
||||
'Removing...': '削除中...',
|
||||
Reload: '再読み込み'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +496,17 @@ export default {
|
||||
'Choose a feed': '피드 선택',
|
||||
'and {{x}} others': '및 기타 {{x}}명',
|
||||
selfZapWarning:
|
||||
'Jumble은 자신에게 Zap을 보낼 때 발생하는 일에 대해 책임을 지지 않습니다. 본인의 책임 하에 진행하세요. 😉⚡'
|
||||
'Jumble은 자신에게 Zap을 보낼 때 발생하는 일에 대해 책임을 지지 않습니다. 본인의 책임 하에 진행하세요. 😉⚡',
|
||||
'Emoji Pack': '이모지 팩',
|
||||
'Emoji pack added': '이모지 팩이 추가되었습니다',
|
||||
'Add emoji pack failed': '이모지 팩 추가 실패',
|
||||
'Emoji pack removed': '이모지 팩이 제거되었습니다',
|
||||
'Remove emoji pack failed': '이모지 팩 제거 실패',
|
||||
Added: '추가됨',
|
||||
'Emoji Packs': '이모지 팩',
|
||||
'My Packs': '내 팩',
|
||||
'Adding...': '추가 중...',
|
||||
'Removing...': '제거 중...',
|
||||
Reload: '다시 불러오기'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +500,17 @@ export default {
|
||||
'Choose a feed': 'Wybierz feed',
|
||||
'and {{x}} others': 'i {{x}} innych',
|
||||
selfZapWarning:
|
||||
'Jumble nie ponosi odpowiedzialności za to, co się stanie, jeśli zappujesz samego siebie. Kontynuuj na własne ryzyko. 😉⚡'
|
||||
'Jumble nie ponosi odpowiedzialności za to, co się stanie, jeśli zappujesz samego siebie. Kontynuuj na własne ryzyko. 😉⚡',
|
||||
'Emoji Pack': 'Pakiet Emoji',
|
||||
'Emoji pack added': 'Pakiet emoji dodany',
|
||||
'Add emoji pack failed': 'Dodawanie pakietu emoji nie powiodło się',
|
||||
'Emoji pack removed': 'Pakiet emoji usunięty',
|
||||
'Remove emoji pack failed': 'Usuwanie pakietu emoji nie powiodło się',
|
||||
Added: 'Dodano',
|
||||
'Emoji Packs': 'Pakiety Emoji',
|
||||
'My Packs': 'Moje Pakiety',
|
||||
'Adding...': 'Dodawanie...',
|
||||
'Removing...': 'Usuwanie...',
|
||||
Reload: 'Przeładuj'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +497,17 @@ export default {
|
||||
'Choose a feed': 'Escolha um feed',
|
||||
'and {{x}} others': 'e {{x}} outros',
|
||||
selfZapWarning:
|
||||
'Jumble não é responsável pelo que acontece se você zap a si mesmo. Prossiga por sua conta e risco. 😉⚡'
|
||||
'Jumble não é responsável pelo que acontece se você zap a si mesmo. Prossiga por sua conta e risco. 😉⚡',
|
||||
'Emoji Pack': 'Pacote de Emojis',
|
||||
'Emoji pack added': 'Pacote de emojis adicionado',
|
||||
'Add emoji pack failed': 'Falha ao adicionar pacote de emojis',
|
||||
'Emoji pack removed': 'Pacote de emojis removido',
|
||||
'Remove emoji pack failed': 'Falha ao remover pacote de emojis',
|
||||
Added: 'Adicionado',
|
||||
'Emoji Packs': 'Pacotes de Emojis',
|
||||
'My Packs': 'Meus Pacotes',
|
||||
'Adding...': 'Adicionando...',
|
||||
'Removing...': 'Removendo...',
|
||||
Reload: 'Recarregar'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +500,17 @@ export default {
|
||||
'Choose a feed': 'Escolha um feed',
|
||||
'and {{x}} others': 'e {{x}} outros',
|
||||
selfZapWarning:
|
||||
'Jumble não é responsável pelo que acontece se você zap a si mesmo. Prossiga por sua conta e risco. 😉⚡'
|
||||
'Jumble não é responsável pelo que acontece se você zap a si mesmo. Prossiga por sua conta e risco. 😉⚡',
|
||||
'Emoji Pack': 'Pacote de Emojis',
|
||||
'Emoji pack added': 'Pacote de emojis adicionado',
|
||||
'Add emoji pack failed': 'Falha ao adicionar pacote de emojis',
|
||||
'Emoji pack removed': 'Pacote de emojis removido',
|
||||
'Remove emoji pack failed': 'Falha ao remover pacote de emojis',
|
||||
Added: 'Adicionado',
|
||||
'Emoji Packs': 'Pacotes de Emojis',
|
||||
'My Packs': 'Os Meus Pacotes',
|
||||
'Adding...': 'A adicionar...',
|
||||
'Removing...': 'A remover...',
|
||||
Reload: 'Recarregar'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +502,17 @@ export default {
|
||||
'Choose a feed': 'Выберите ленту',
|
||||
'and {{x}} others': 'и {{x}} других',
|
||||
selfZapWarning:
|
||||
'Jumble не несет ответственности за то, что произойдет, если вы отправите zap самому себе. Продолжайте на свой страх и риск. 😉⚡'
|
||||
'Jumble не несет ответственности за то, что произойдет, если вы отправите zap самому себе. Продолжайте на свой страх и риск. 😉⚡',
|
||||
'Emoji Pack': 'Набор эмодзи',
|
||||
'Emoji pack added': 'Набор эмодзи добавлен',
|
||||
'Add emoji pack failed': 'Не удалось добавить набор эмодзи',
|
||||
'Emoji pack removed': 'Набор эмодзи удален',
|
||||
'Remove emoji pack failed': 'Не удалось удалить набор эмодзи',
|
||||
Added: 'Добавлено',
|
||||
'Emoji Packs': 'Наборы эмодзи',
|
||||
'My Packs': 'Мои наборы',
|
||||
'Adding...': 'Добавление...',
|
||||
'Removing...': 'Удаление...',
|
||||
Reload: 'Перезагрузить'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,17 @@ export default {
|
||||
'Choose a feed': 'เลือกฟีด',
|
||||
'and {{x}} others': 'และอื่น ๆ {{x}} รายการ',
|
||||
selfZapWarning:
|
||||
'Jumble ไม่รับผิดชอบต่อสิ่งที่เกิดขึ้นหากคุณ zap ตัวเอง ดำเนินการด้วยความเสี่ยงของคุณเอง 😉⚡'
|
||||
'Jumble ไม่รับผิดชอบต่อสิ่งที่เกิดขึ้นหากคุณ zap ตัวเอง ดำเนินการด้วยความเสี่ยงของคุณเอง 😉⚡',
|
||||
'Emoji Pack': 'แพ็คอีโมจิ',
|
||||
'Emoji pack added': 'เพิ่มแพ็คอีโมจิแล้ว',
|
||||
'Add emoji pack failed': 'การเพิ่มแพ็คอีโมจิล้มเหลว',
|
||||
'Emoji pack removed': 'ลบแพ็คอีโมจิแล้ว',
|
||||
'Remove emoji pack failed': 'การลบแพ็คอีโมจิล้มเหลว',
|
||||
Added: 'เพิ่มแล้ว',
|
||||
'Emoji Packs': 'แพ็คอีโมจิ',
|
||||
'My Packs': 'แพ็คของฉัน',
|
||||
'Adding...': 'กำลังเพิ่ม...',
|
||||
'Removing...': 'กำลังลบ...',
|
||||
Reload: 'โหลดใหม่'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,6 +487,17 @@ export default {
|
||||
'Explore Relays': '探索服务器',
|
||||
'Choose a feed': '选择一个动态',
|
||||
'and {{x}} others': '和其他 {{x}} 人',
|
||||
selfZapWarning: 'Jumble 对您给自己打赏所发生的事情概不负责。风险自负。😉⚡'
|
||||
selfZapWarning: 'Jumble 对您给自己打赏所发生的事情概不负责。风险自负。😉⚡',
|
||||
'Emoji Pack': '表情包',
|
||||
'Emoji pack added': '表情包已添加',
|
||||
'Add emoji pack failed': '添加表情包失败',
|
||||
'Emoji pack removed': '表情包已移除',
|
||||
'Remove emoji pack failed': '移除表情包失败',
|
||||
Added: '已添加',
|
||||
'Emoji Packs': '表情包',
|
||||
'My Packs': '我的表情包',
|
||||
'Adding...': '添加中...',
|
||||
'Removing...': '移除中...',
|
||||
Reload: '重新加载'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,15 @@ export function createPinListDraftEvent(tags: string[][], content = ''): TDraftE
|
||||
}
|
||||
}
|
||||
|
||||
export function createUserEmojiListDraftEvent(tags: string[][], content = ''): TDraftEvent {
|
||||
return {
|
||||
kind: kinds.UserEmojiList,
|
||||
content,
|
||||
tags,
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlossomServerListDraftEvent(servers: string[]): TDraftEvent {
|
||||
return {
|
||||
kind: ExtendedKind.BLOSSOM_SERVER_LIST,
|
||||
|
||||
@@ -355,11 +355,14 @@ export function getEmojisAndEmojiSetsFromEvent(event: Event) {
|
||||
return { emojis, emojiSetPointers }
|
||||
}
|
||||
|
||||
export function getEmojisFromEvent(event: Event): TEmoji[] {
|
||||
export function getEmojiPackInfoFromEvent(event: Event) {
|
||||
let title: string | undefined
|
||||
const emojis: TEmoji[] = []
|
||||
|
||||
event.tags.forEach(([tagName, ...tagValues]) => {
|
||||
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 {
|
||||
|
||||
@@ -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`
|
||||
|
||||
43
src/pages/secondary/EmojiPackSettingsPage/index.tsx
Normal file
43
src/pages/secondary/EmojiPackSettingsPage/index.tsx
Normal file
@@ -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<TTab>('my-packs')
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout ref={ref} index={index} title={t('Emoji Packs')} displayScrollToTopButton>
|
||||
<Tabs
|
||||
value={tab}
|
||||
tabs={[
|
||||
{ value: 'my-packs', label: 'My Packs' },
|
||||
{ value: 'explore', label: 'Explore' }
|
||||
]}
|
||||
onTabChange={(tab) => {
|
||||
setTab(tab as TTab)
|
||||
}}
|
||||
/>
|
||||
{tab === 'my-packs' ? (
|
||||
<EmojiPackList />
|
||||
) : (
|
||||
<NoteList
|
||||
showKinds={[kinds.Emojisets]}
|
||||
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
||||
hideUntrustedNotes={hideUntrustedNotes}
|
||||
/>
|
||||
)}
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
})
|
||||
EmojiPackSettingsPage.displayName = 'EmojiPackSettingsPage'
|
||||
export default EmojiPackSettingsPage
|
||||
@@ -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) => {
|
||||
</Label>
|
||||
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} />
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<div>
|
||||
<a
|
||||
className="flex items-center gap-1 cursor-pointer hover:underline"
|
||||
href="https://emojito.meme/browse"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('Custom emoji management')}
|
||||
<ExternalLink />
|
||||
</a>
|
||||
<div className="text-muted-foreground">
|
||||
{t('After changing emojis, you may need to refresh the page')}
|
||||
</div>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
|
||||
90
src/providers/EmojiPackProvider.tsx
Normal file
90
src/providers/EmojiPackProvider.tsx
Normal file
@@ -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<string>
|
||||
addEmojiPack: (event: Event) => Promise<void>
|
||||
removeEmojiPack: (event: Event) => Promise<void>
|
||||
}
|
||||
|
||||
const EmojiPackContext = createContext<TEmojiPackContext | undefined>(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<string>()
|
||||
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 (
|
||||
<EmojiPackContext.Provider
|
||||
value={{
|
||||
emojiPackCoordinateSet,
|
||||
addEmojiPack,
|
||||
removeEmojiPack
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</EmojiPackContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -86,6 +86,7 @@ type TNostrContext = {
|
||||
updateMuteListEvent: (muteListEvent: Event, privateTags: string[][]) => Promise<void>
|
||||
updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
|
||||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||
updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise<void>
|
||||
updatePinListEvent: (pinListEvent: Event) => Promise<void>
|
||||
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
|
||||
}
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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: <GeneralSettingsPage /> },
|
||||
{ path: '/settings/appearance', element: <AppearanceSettingsPage /> },
|
||||
{ path: '/settings/translation', element: <TranslationPage /> },
|
||||
{ path: '/settings/emoji-packs', element: <EmojiPackSettingsPage /> },
|
||||
{ path: '/profile-editor', element: <ProfileEditorPage /> },
|
||||
{ path: '/mutes', element: <MuteListPage /> },
|
||||
{ path: '/rizful', element: <RizfulPage /> },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -189,6 +189,12 @@ class IndexedDbService {
|
||||
})
|
||||
}
|
||||
|
||||
async getReplaceableEventByCoordinate(coordinate: string): Promise<Event | undefined | null> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user