Skip to content

Commit

Permalink
feat: reference comment
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jul 8, 2023
1 parent 6e4b5e9 commit 4a94877
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 43 deletions.
1 change: 1 addition & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ declare global {
declare module 'react' {
export interface AriaAttributes {
'data-hide-print'?: boolean
'data-event'?: string
'data-testid'?: string
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { NoteHideIfSecret } from '../../../components/widgets/note/NoteHideIfSec
import { NoteMetaBar } from '../../../components/widgets/note/NoteMetaBar'
import {
IndentArticleContainer,
MarkdownSelection,
NoteHeaderDate,
NoteHeaderMetaInfoSetting,
NoteMarkdown,
Expand Down Expand Up @@ -90,7 +91,9 @@ const NotePage = memo(function Notepage() {
<ReadIndicatorForMobile />
<NoteMarkdownImageRecordProvider>
<BanCopyWrapper>
<NoteMarkdown />
<MarkdownSelection>
<NoteMarkdown />
</MarkdownSelection>
</BanCopyWrapper>
</NoteMarkdownImageRecordProvider>

Expand Down
11 changes: 11 additions & 0 deletions src/app/notes/[id]/pageExtra.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks'
import { FloatPopover } from '~/components/ui/float-popover'
import { Markdown } from '~/components/ui/markdown'
import { GoToAdminEditingButton } from '~/components/widgets/shared/GoToAdminEditingButton'
import { WithArticleSelectionAction } from '~/components/widgets/shared/WithArticleSelectionAction'
import { parseDate } from '~/lib/datetime'
import { noopArr } from '~/lib/noop'
import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider'
import { useCurrentNoteDataSelector } from '~/providers/note/CurrentNoteDataProvider'

import styles from './page.module.css'

export const MarkdownSelection: Component = (props) => {
const id = useCurrentNoteDataSelector((data) => data?.data?.id)!
const title = useCurrentNoteDataSelector((data) => data?.data?.title)!
return (
<WithArticleSelectionAction refId={id} title={title}>
{props.children}
</WithArticleSelectionAction>
)
}

export const NoteTitle = () => {
const title = useCurrentNoteDataSelector((data) => data?.data.title)
const id = useCurrentNoteDataSelector((data) => data?.data.id)
Expand Down
5 changes: 4 additions & 1 deletion src/app/posts/(post-detail)/[category]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvide
import Loading from './loading'
import {
HeaderMetaInfoSetting,
MarkdownSelection,
PostMarkdown,
PostMarkdownImageRecordProvider,
PostMetaBarInternal,
Expand Down Expand Up @@ -61,7 +62,9 @@ const PostPage = () => {
<WrappedElementProvider>
<ReadIndicatorForMobile />
<PostMarkdownImageRecordProvider>
<PostMarkdown />
<MarkdownSelection>
<PostMarkdown />
</MarkdownSelection>
</PostMarkdownImageRecordProvider>

<LayoutRightSidePortal>
Expand Down
10 changes: 10 additions & 0 deletions src/app/posts/(post-detail)/[category]/[slug]/pageExtra.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import type { PropsWithChildren } from 'react'
import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks'
import { Markdown } from '~/components/ui/markdown'
import { PostMetaBar } from '~/components/widgets/post/PostMetaBar'
import { WithArticleSelectionAction } from '~/components/widgets/shared/WithArticleSelectionAction'
import { noopArr } from '~/lib/noop'
import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider'
import { useCurrentPostDataSelector } from '~/providers/post/CurrentPostDataProvider'

export const MarkdownSelection: Component = (props) => {
const id = useCurrentPostDataSelector((data) => data?.id)!
const title = useCurrentPostDataSelector((data) => data?.title)!
return (
<WithArticleSelectionAction refId={id} title={title}>
{props.children}
</WithArticleSelectionAction>
)
}
export const PostMarkdown = () => {
const text = useCurrentPostDataSelector((data) => data?.text)
if (!text) return null
Expand Down
8 changes: 6 additions & 2 deletions src/components/widgets/comment/CommentBox/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { CommentBoxSignedOutContent } from './SignedOutContent'
import { SwitchCommentMode } from './SwitchCommentMode'

export const CommentBoxRoot: Component<CommentBaseProps> = (props) => {
const { refId, className, afterSubmit } = props
const { refId, className, afterSubmit, initialValue } = props

const mode = useCommentMode()

Expand All @@ -27,7 +27,11 @@ export const CommentBoxRoot: Component<CommentBaseProps> = (props) => {
}, [isLogged])

return (
<CommentBoxProvider refId={refId}>
<CommentBoxProvider
refId={refId}
afterSubmit={afterSubmit}
initialValue={initialValue}
>
<div
className={clsxm('group relative w-full min-w-0', className)}
data-hide-print
Expand Down
10 changes: 9 additions & 1 deletion src/components/widgets/comment/CommentBox/UniversalTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useEffect, useRef } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
import dynamic from 'next/dynamic'

import { FloatPopover } from '~/components/ui/float-popover'
Expand Down Expand Up @@ -51,6 +51,14 @@ export const UniversalTextArea = () => {
$ta.value = value
}
}, [value])

useLayoutEffect(() => {
// autofocus
const $ta = taRef.current
if (!$ta) return
$ta.selectionStart = $ta.selectionEnd = $ta.value.length
$ta.focus()
}, [])
return (
<TextArea
ref={taRef}
Expand Down
31 changes: 20 additions & 11 deletions src/components/widgets/comment/CommentBox/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { atomWithStorage } from 'jotai/utils'
import type { CommentModel } from '@mx-space/api-client'
import type { FC, PropsWithChildren } from 'react'

import { useBeforeMounted } from '~/hooks/common/use-before-mounted'
import { jotaiStore } from '~/lib/store'

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

const commentStoragePrefix = 'comment-'
Expand Down Expand Up @@ -36,19 +39,25 @@ export const CommentBoxLifeCycleContext = createContext<{
}>(null!)

export const CommentBoxProvider = (
props: PropsWithChildren & { refId: string; afterSubmit?: () => void },
props: PropsWithChildren & {
refId: string
afterSubmit?: () => void
initialValue?: string
},
) => {
const { refId, children, afterSubmit } = props
const { refId, children, afterSubmit, initialValue } = props

const ctxValue = useRef({
...createInitialValue(),
refId: atom(refId),
}).current
useBeforeMounted(() => {
if (initialValue) {
jotaiStore.set(ctxValue.text, initialValue)
}
})
return (
<CommentBoxContext.Provider
key={refId}
value={
useRef({
...createInitialValue(),
refId: atom(refId),
}).current
}
>
<CommentBoxContext.Provider key={refId} value={ctxValue}>
<CommentBoxLifeCycleContext.Provider
value={useMemo(() => ({ afterSubmit }), [afterSubmit])}
>
Expand Down
1 change: 1 addition & 0 deletions src/components/widgets/comment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export interface CommentBaseProps {
refId: string

afterSubmit?: () => void
initialValue?: string
}
2 changes: 1 addition & 1 deletion src/components/widgets/post/PostOutdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const PostOutdate = () => {
return null
}
return dayjs().diff(dayjs(time), 'day') > 60 ? (
<Banner type="warning" className="mb-10">
<Banner type="warning" className="my-10">
<span className="leading-[1.8]">
这篇文章上次修改于 <RelativeTime date={time} />
,可能部分内容已经不适用,如有疑问可询问作者。
Expand Down
33 changes: 7 additions & 26 deletions src/components/widgets/shared/AsideCommentButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import type { ModalContentComponent } from '~/providers/root/modal-stack-provider'
import type { CommentModalProps } from './CommentModal'

import { MotionButtonBase } from '~/components/ui/button'
import { useIsClient } from '~/hooks/common/use-is-client'
import { useModalStack } from '~/providers/root/modal-stack-provider'

import { CommentBoxRoot } from '../comment/CommentBox'
import { Comments } from '../comment/Comments'
import { CommentModal } from './CommentModal'

interface AsideCommentButtonProps {
title: string
refId: string
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AsideCommentButtonProps {}

export const AsideCommentButton = (props: AsideCommentButtonProps) => {
export const AsideCommentButton = (
props: CommentModalProps & AsideCommentButtonProps,
) => {
const isClient = useIsClient()
const { present } = useModalStack()

Expand All @@ -33,21 +32,3 @@ export const AsideCommentButton = (props: AsideCommentButtonProps) => {
</MotionButtonBase>
)
}

const CommentModal: ModalContentComponent<AsideCommentButtonProps> = (
props,
) => {
const { refId, title, dismiss } = props

return (
<div className="max-w-95vw w-[700px] overflow-y-auto overflow-x-hidden">
<span>
回复: <h1 className="mt-4 text-lg font-medium">{title}</h1>
</span>

<CommentBoxRoot className="my-12" refId={refId} afterSubmit={dismiss} />

<Comments refId={refId} />
</div>
)
}
34 changes: 34 additions & 0 deletions src/components/widgets/shared/CommentModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ModalContentComponent } from '~/providers/root/modal-stack-provider'

import { CommentBoxRoot } from '../comment/CommentBox'
import { Comments } from '../comment/Comments'

export interface CommentModalProps {
title: string
refId: string

initialValue?: string
}

export const CommentModal: ModalContentComponent<CommentModalProps> = (
props,
) => {
const { refId, title, dismiss, initialValue } = props

return (
<div className="max-w-95vw w-[700px] overflow-y-auto overflow-x-hidden">
<span>
回复: <h1 className="mt-4 text-lg font-medium">{title}</h1>
</span>

<CommentBoxRoot
initialValue={initialValue}
className="my-12"
refId={refId}
afterSubmit={dismiss}
/>

<Comments refId={refId} />
</div>
)
}
108 changes: 108 additions & 0 deletions src/components/widgets/shared/WithArticleSelectionAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useEffect, useRef, useState } from 'react'
import { AnimatePresence, m } from 'framer-motion'

import { useIsMobile } from '~/atoms'
import { MotionButtonBase } from '~/components/ui/button'
import { useIsClient } from '~/hooks/common/use-is-client'
import { useModalStack } from '~/providers/root/modal-stack-provider'

import { CommentModal } from './CommentModal'

export const WithArticleSelectionAction: Component<{
refId: string
title: string
}> = ({ refId, title, children }) => {
const isMobile = useIsMobile()
const [pos, setPos] = useState({
x: 0,
y: 0,
})
const [show, setShow] = useState(false)
const [selectedText, setSelectedText] = useState('')
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
const handler = (e: MouseEvent): void => {
if (window.getSelection()?.toString().length === 0) {
setShow(false)
return
}
if (ref.current?.contains(e.target as Node)) return
setShow(false)
}
document.addEventListener('mousedown', handler)
document.addEventListener('mouseup', handler)
return () => {
document.removeEventListener('mousedown', handler)
document.removeEventListener('mouseup', handler)
}
}, [])

const isClient = useIsClient()
const { present } = useModalStack()

if (isMobile || !isClient) return children
return (
<div
className="relative"
ref={ref}
onMouseUp={(e) => {
const $ = ref.current
if (!$) return

const selection = window.getSelection()
if (!selection) return
const selectedText = selection.toString()
if (selectedText.length === 0) return
const { top, left } = $.getBoundingClientRect()
setShow(true)
setSelectedText(selectedText)
setPos({
x: e.clientX - left + 10,
y: e.clientY - top + 10,
})
}}
>
{children}

<AnimatePresence>
{show && (
<m.div
className="absolute z-10 rounded-md border border-slate-200/90 bg-slate-100 px-4 py-3 text-sm dark:border-neutral-800/90 dark:bg-zinc-900"
data-event="selection-action"
style={{
left: pos.x,
top: pos.y,
}}
initial={{ y: 10, opacity: 0.6, scale: 0.96 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
exit={{
y: 20,
opacity: 0,
}}
>
<MotionButtonBase
onClick={() => {
present({
title: '评论',
content: (rest) => (
<CommentModal
refId={refId}
title={title}
initialValue={`> ${selectedText
?.split('\n')
.join('')}\n\n`}
{...rest}
/>
),
})
}}
>
引用评论
</MotionButtonBase>
</m.div>
)}
</AnimatePresence>
</div>
)
}
Loading

1 comment on commit 4a94877

@vercel
Copy link

@vercel vercel bot commented on 4a94877 Jul 8, 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
springtide.vercel.app
shiro-innei.vercel.app
innei.in

Please sign in to comment.