feat: add emoji picker to post editor

This commit is contained in:
codytseng
2025-05-27 22:39:57 +08:00
parent 061e38a78f
commit 9edd61db78
4 changed files with 78 additions and 9 deletions

View File

@@ -12,6 +12,7 @@ import {
RefObject, RefObject,
useContext, useContext,
useEffect, useEffect,
useRef,
useState useState
} from 'react' } from 'react'
import ExplorePage from './pages/primary/ExplorePage' import ExplorePage from './pages/primary/ExplorePage'
@@ -90,6 +91,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([]) const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
const [isShared, setIsShared] = useState(false) const [isShared, setIsShared] = useState(false)
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const ignorePopStateRef = useRef(false)
useEffect(() => { useEffect(() => {
window.history.pushState(null, '', window.location.href) window.history.pushState(null, '', window.location.href)
@@ -118,8 +120,14 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
} }
const onPopState = (e: PopStateEvent) => { const onPopState = (e: PopStateEvent) => {
if (ignorePopStateRef.current) {
ignorePopStateRef.current = false
return
}
const closeModal = modalManager.pop() const closeModal = modalManager.pop()
if (closeModal) { if (closeModal) {
ignorePopStateRef.current = true
window.history.forward() window.history.forward()
return return
} }

View File

@@ -0,0 +1,51 @@
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useState } from 'react'
import EmojiPicker from '../EmojiPicker'
export default function EmojiPickerDialog({
children,
onEmojiClick
}: {
children: React.ReactNode
onEmojiClick?: (emoji: string) => void
}) {
const { isSmallScreen } = useScreenSize()
const [open, setOpen] = useState(false)
if (isSmallScreen) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{children}</DrawerTrigger>
<DrawerContent>
<EmojiPicker
onEmojiClick={(data) => {
setOpen(false)
onEmojiClick?.(data.emoji)
}}
/>
</DrawerContent>
</Drawer>
)
}
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="p-0 w-fit">
<EmojiPicker
onEmojiClick={(data, e) => {
e.stopPropagation()
setOpen(false)
onEmojiClick?.(data.emoji)
}}
/>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -5,10 +5,11 @@ import { useToast } from '@/hooks/use-toast'
import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event' import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import postContentCache from '@/services/post-content-cache.service' import postContentCache from '@/services/post-content-cache.service'
import { ChevronDown, ImageUp, LoaderCircle } from 'lucide-react' import { ImageUp, LoaderCircle, Settings, Smile } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import EmojiPickerDialog from '../EmojiPickerDialog'
import Mentions from './Mentions' import Mentions from './Mentions'
import { usePostEditor } from './PostEditorProvider' import { usePostEditor } from './PostEditorProvider'
import PostOptions from './PostOptions' import PostOptions from './PostOptions'
@@ -88,7 +89,7 @@ export default function PostContent({
} }
return ( return (
<div className="space-y-4"> <div className="space-y-2">
{parentEvent && ( {parentEvent && (
<ScrollArea className="flex max-h-48 flex-col overflow-y-auto rounded-lg border bg-muted/40"> <ScrollArea className="flex max-h-48 flex-col overflow-y-auto rounded-lg border bg-muted/40">
<div className="p-2 sm:p-3 pointer-events-none"> <div className="p-2 sm:p-3 pointer-events-none">
@@ -120,19 +121,22 @@ export default function PostContent({
} }
accept="image/*,video/*,audio/*" accept="image/*,video/*,audio/*"
> >
<Button variant="secondary" disabled={uploadingFiles > 0}> <Button variant="ghost" size="icon" disabled={uploadingFiles > 0}>
{uploadingFiles > 0 ? <LoaderCircle className="animate-spin" /> : <ImageUp />} {uploadingFiles > 0 ? <LoaderCircle className="animate-spin" /> : <ImageUp />}
</Button> </Button>
</Uploader> </Uploader>
<EmojiPickerDialog onEmojiClick={(emoji) => textareaRef.current?.insertText(emoji)}>
<Button variant="ghost" size="icon">
<Smile />
</Button>
</EmojiPickerDialog>
<Button <Button
variant="link" variant="ghost"
className="text-foreground gap-0 px-0" size="icon"
className={showMoreOptions ? 'bg-accent' : ''}
onClick={() => setShowMoreOptions((pre) => !pre)} onClick={() => setShowMoreOptions((pre) => !pre)}
> >
{t('More options')} <Settings />
<ChevronDown
className={`transition-transform ${showMoreOptions ? 'rotate-180' : ''}`}
/>
</Button> </Button>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">

View File

@@ -19,6 +19,7 @@ import suggestion from './suggestion'
export type TPostTextareaHandle = { export type TPostTextareaHandle = {
appendText: (text: string) => void appendText: (text: string) => void
insertText: (text: string) => void
} }
const PostTextarea = forwardRef< const PostTextarea = forwardRef<
@@ -94,6 +95,11 @@ const PostTextarea = forwardRef<
.insertContent(text) .insertContent(text)
.run() .run()
} }
},
insertText: (text: string) => {
if (editor) {
editor.chain().focus().insertContent(text).run()
}
} }
})) }))