Skip to content

Commit

Permalink
feat: emoji picker
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jul 2, 2023
1 parent bdb8a2e commit a6bdd3e
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 38 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
},
"dependencies": {
"@clerk/nextjs": "4.21.13",
"@emoji-mart/data": "1.1.2",
"@emoji-mart/react": "1.1.1",
"@floating-ui/react-dom": "2.0.1",
"@mx-space/api-client": "1.4.3",
"@radix-ui/react-dialog": "1.0.4",
Expand All @@ -52,6 +54,7 @@
"clsx": "1.2.1",
"daisyui": "3.1.7",
"dayjs": "1.11.9",
"emoji-mart": "5.5.2",
"foxact": "0.2.11",
"framer-motion": "^10.12.18",
"idb-keyval": "6.2.1",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/components/widgets/comment/CommentBox/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
useSetCommentBoxValues,
useUseCommentReply,
} from './hooks'
import { CommentBoxSlotProvider } from './providers'

const TextLengthIndicator = () => {
const isTextOversize = useCommentBoxTextIsOversize()
Expand Down Expand Up @@ -111,13 +112,13 @@ export const CommentBoxActionBar: Component = ({ className }) => {
<span
className={clsx(
'flex-1 select-none text-[10px] text-zinc-500 transition-opacity',
hasCommentText ? 'visible opacity-100' : 'invisible opacity-0',
)}
>
支持 <b>Markdown</b>{' '}
<MLink href="https://docs.github.com/zh/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax">
GFM
</MLink>
<CommentBoxSlotProvider />
</span>
<AnimatePresence>
{hasCommentText && (
Expand Down
60 changes: 32 additions & 28 deletions src/components/widgets/comment/CommentBox/SwitchCommentMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,15 @@ import {
export const SwitchCommentMode = () => {
const mode = useCommentMode()
const copy = `转换到${mode === CommentBoxMode.legacy ? '新' : '旧'}版评论`
const TriggerComponent = useRef<FC>(function SwitchCommentModeButton() {
const mode = useCommentMode()
const hasText = useCommentBoxHasText()

const hasText = useCommentBoxHasText()
const notLogged = !!useUser()

const notLogged = !!useUser()
const TriggerComponent = useRef<FC>(function SwitchCommentModeButton() {
const mode = useCommentMode()

return (
<MotionButtonBase
className={clsx(
'absolute left-0 top-0 z-10 rounded-full text-sm',
'h-6 w-6 border border-slate-200 dark:border-neutral-800',
'bg-slate-100 dark:bg-neutral-900',
'flex cursor-pointer text-base-100/50 center',
'text-base-content/50',
'opacity-0 transition-opacity duration-200 group-[:hover]:opacity-100',
mode === CommentBoxMode['legacy'] && 'bottom-0 top-auto',
hasText ||
(notLogged &&
mode === CommentBoxMode['with-auth'] &&
'invisible opacity-0'),
)}
onClick={() => {
setCommentMode(
mode === CommentBoxMode.legacy
? CommentBoxMode['with-auth']
: CommentBoxMode['legacy'],
)
}}
>
<>
<i
className={clsx(
mode === CommentBoxMode.legacy
Expand All @@ -57,8 +36,33 @@ export const SwitchCommentMode = () => {
)}
/>
<span className="sr-only">{copy}</span>
</MotionButtonBase>
</>
)
}).current
return <FloatPopover TriggerComponent={TriggerComponent}>{copy}</FloatPopover>
return (
<MotionButtonBase
className={clsx(
'absolute left-0 top-0 z-10 rounded-full text-sm',
'h-6 w-6 border border-slate-200 dark:border-neutral-800',
'bg-slate-100 dark:bg-neutral-900',
'flex cursor-pointer text-base-100/50 center',
'text-base-content/50',
'opacity-0 transition-opacity duration-200 group-[:hover]:opacity-100',
mode === CommentBoxMode['legacy'] && 'bottom-0 top-auto',
hasText ||
(notLogged &&
mode === CommentBoxMode['with-auth'] &&
'invisible opacity-0'),
)}
onClick={() => {
setCommentMode(
mode === CommentBoxMode.legacy
? CommentBoxMode['with-auth']
: CommentBoxMode['legacy'],
)
}}
>
<FloatPopover TriggerComponent={TriggerComponent}>{copy}</FloatPopover>
</MotionButtonBase>
)
}
62 changes: 57 additions & 5 deletions src/components/widgets/comment/CommentBox/UniversalTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
'use client'

import { useCallback, useRef } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import clsx from 'clsx'
import { m, useMotionTemplate, useMotionValue } from 'framer-motion'
import dynamic from 'next/dynamic'

import { useIsMobile } from '~/atoms'
import { FloatPopover } from '~/components/ui/float-popover'

import { getRandomPlaceholder } from './constants'
import { useCommentBoxTextValue, useSetCommentBoxValues } from './hooks'
import { CommentBoxSlotPortal } from './providers'

const EmojiPicker = dynamic(() =>
import('../../shared/EmojiPicker').then((mod) => mod.EmojiPicker),
)
export const UniversalTextArea = () => {
const placeholder = useRef(getRandomPlaceholder()).current
const setter = useSetCommentBoxValues()
Expand All @@ -26,6 +32,38 @@ export const UniversalTextArea = () => {
)
const background = useMotionTemplate`radial-gradient(320px circle at ${mouseX}px ${mouseY}px, var(--spotlight-color) 0%, transparent 85%)`
const isMobile = useIsMobile()
const taRef = useRef<HTMLTextAreaElement>(null)
const handleInsertEmoji = useCallback((emoji: string) => {
if (!taRef.current) {
return
}

const $ta = taRef.current
const start = $ta.selectionStart
const end = $ta.selectionEnd

$ta.value = `${$ta.value.substring(
0,
start,
)} ${emoji} ${$ta.value.substring(end, $ta.value.length)}`

setter('text', $ta.value)
requestAnimationFrame(() => {
const shouldMoveToPos = start + emoji.length + 2
$ta.selectionStart = shouldMoveToPos
$ta.selectionEnd = shouldMoveToPos

$ta.focus()
})
}, [])

useEffect(() => {
const $ta = taRef.current
if (!$ta) return
if (value !== $ta.value) {
$ta.value = value
}
}, [value])
return (
<div
className="group relative h-full [--spotlight-color:hsl(var(--a)_/_0.05)]"
Expand All @@ -39,17 +77,31 @@ export const UniversalTextArea = () => {
/>
)}
<textarea
value={value}
onChange={(e) => {
setter('text', e.target.value)
}}
ref={taRef}
defaultValue={value}
onChange={(e) => setter('text', e.target.value)}
placeholder={placeholder}
className={clsx(
'h-full w-full resize-none bg-transparent',
'overflow-auto px-3 py-4',
'text-neutral-900/80 dark:text-slate-100/80',
)}
/>

<CommentBoxSlotPortal>
<FloatPopover trigger="click" TriggerComponent={EmojiButton}>
<EmojiPicker onEmojiSelect={handleInsertEmoji} />
</FloatPopover>
</CommentBoxSlotPortal>
</div>
)
}

const EmojiButton = () => {
return (
<button className="ml-4 inline-flex h-5 w-5 translate-y-1 text-base center">
<i className="icon-[mingcute--emoji-2-line]" />
<span className="sr-only">表情</span>
</button>
)
}
11 changes: 10 additions & 1 deletion src/components/widgets/comment/CommentBox/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client'

import { useCallback, useContext } from 'react'
import { useAtomValue } from 'jotai'
import { atom, useAtomValue } from 'jotai'
import { atomWithStorage, selectAtom } from 'jotai/utils'
import type { ExtractAtomValue } from 'jotai'
import type React from 'react'
import type { createInitialValue } from './providers'

import { jotaiStore } from '~/lib/store'
Expand Down Expand Up @@ -32,6 +33,14 @@ export const useGetCommentBoxAtomValues = () => {
return useContext(CommentBoxContext)
}

// ReactNode 导致 tsx 无法推断,过于复杂
const commentActionLeftSlotAtom = atom(null as React.JSX.Element | null)
export const useCommentActionLeftSlot = () =>
useAtomValue(commentActionLeftSlotAtom)

export const setCommentActionLeftSlot = (slot: React.JSX.Element | null) =>
jotaiStore.set(commentActionLeftSlotAtom, slot)

export const useCommentBoxHasText = () =>
useAtomValue(
selectAtom(
Expand Down
26 changes: 24 additions & 2 deletions src/components/widgets/comment/CommentBox/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client'

import { createContext, useRef } from 'react'
import { createContext, memo, useEffect, useRef } from 'react'
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import type { CommentModel } from '@mx-space/api-client'
import type { PropsWithChildren } from 'react'
import type { FC, PropsWithChildren } from 'react'

import { setCommentActionLeftSlot, useCommentActionLeftSlot } from './hooks'

const commentStoragePrefix = 'comment-'
export const createInitialValue = () => ({
Expand Down Expand Up @@ -70,3 +72,23 @@ export const CommentIsReplyProvider = (
</CommentOriginalRefIdContext.Provider>
)
}

export const CommentBoxSlotPortal = memo(
(props: { children: React.JSX.Element }) => {
const { children } = props
useEffect(() => {
setCommentActionLeftSlot(children)
return () => {
setCommentActionLeftSlot(null)
}
}, [children])
return null
},
)

export const CommentBoxSlotProvider: FC = memo(() => {
return useCommentActionLeftSlot()
})

CommentBoxSlotProvider.displayName = 'CommentBoxSlotProvider'
CommentBoxSlotPortal.displayName = 'CommentBoxSlotPortal'
2 changes: 1 addition & 1 deletion src/components/widgets/comment/CommentRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const CommentAreaRoot: FC<
> = (props) => {
const { allowComment, refId } = props
// 兜下后端的数据,默认开
if (allowComment && typeof allowComment !== 'undefined') {
if (!allowComment && typeof allowComment !== 'undefined') {
return (
<p className="mt-[100px] text-center text-xl font-medium">评论已关闭</p>
)
Expand Down
27 changes: 27 additions & 0 deletions src/components/widgets/shared/EmojiPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Picker from '@emoji-mart/react'
import { useQuery } from '@tanstack/react-query'
import { memo, useCallback } from 'react'
import type { FC } from 'react'

import { Loading } from '~/components/ui/loading'

export const EmojiPicker: FC<{
onEmojiSelect: (emoji: string) => void
}> = memo(({ onEmojiSelect }) => {
const { data, isLoading } = useQuery({
queryKey: ['emojidata'],
queryFn: () =>
fetch('https://cdn.jsdelivr.net/npm/@emoji-mart/data').then((response) =>
response.json(),
),

staleTime: Infinity,
})
const handleSelect = useCallback((emoji: any) => {
return onEmojiSelect(emoji.native)
}, [])
if (isLoading) return <Loading className="w-[352px]" />

return <Picker data={data} onEmojiSelect={handleSelect} />
})
EmojiPicker.displayName = 'EmojiPicker'

1 comment on commit a6bdd3e

@vercel
Copy link

@vercel vercel bot commented on a6bdd3e Jul 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

shiro – ./

shiro-git-main-innei.vercel.app
innei.in
springtide.vercel.app
shiro-innei.vercel.app

Please sign in to comment.