Skip to content

Commit

Permalink
feat: comment reply support
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jun 28, 2023
1 parent 8f99190 commit 8c1a631
Show file tree
Hide file tree
Showing 23 changed files with 487 additions and 148 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"qrcode.react": "3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.10",
"react-intersection-observer": "9.5.1",
"react-toastify": "9.1.3",
"react-tweet": "2.0.2",
Expand Down
18 changes: 15 additions & 3 deletions pnpm-lock.yaml

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

2 changes: 1 addition & 1 deletion src/app/(page-detail)/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Metadata } from 'next'
import { RequestError } from '@mx-space/api-client'

import { NotSupport } from '~/components/common/NotSupport'
import { CommentAreaRoot } from '~/components/widgets/comment/CommentAreaRoot'
import { CommentAreaRoot } from '~/components/widgets/comment/CommentRoot'
import { REQUEST_GEO } from '~/constants/system'
import { attachUA } from '~/lib/attach-ua'
import { getSummaryFromMd } from '~/lib/markdown'
Expand Down
7 changes: 6 additions & 1 deletion src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export default () => {
e.preventDefault()
const { login } = await import('~/atoms/owner')
login(username, password).then(() => {
router.push(Routes.Home)
const redirectPath = new URLSearchParams(location.search).get('redirect')
if (redirectPath) {
router.push(decodeURIComponent(redirectPath))
} else {
router.push(Routes.Home)
}
})
}
return (
Expand Down
4 changes: 3 additions & 1 deletion src/app/notes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default async (props: PropsWithChildren) => {
'mt-12 md:mt-24',
)}
>
<NoteLeftSidebar className="relative hidden lg:block" />
<div className="relative hidden lg:block">
<NoteLeftSidebar />
</div>

{props.children}

Expand Down
15 changes: 15 additions & 0 deletions src/components/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ErrorBoundary as ErrorBoundaryLib } from 'react-error-boundary'
import type { FC, PropsWithChildren } from 'react'

export const ErrorBoundary: FC<PropsWithChildren> = ({ children }) => {
return (
<ErrorBoundaryLib
fallback={null}
onError={(e) => {
console.error(e)
}}
>
{children}
</ErrorBoundaryLib>
)
}
4 changes: 2 additions & 2 deletions src/components/common/ReadIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export const ReadIndicator: Component<{
[y, h],
)
const As = as || 'span'
return (
return h > 0 ? (
<As className={clsxm('text-gray-800 dark:text-neutral-300', className)}>
{readPercent}%
</As>
)
) : null
}
4 changes: 3 additions & 1 deletion src/components/layout/header/internal/AnimatedLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const TapableLogo = () => {
}
return
}
router.push(Routes.Login)
router.push(
`${Routes.Login}?redirect=${encodeURIComponent(location.pathname)}`,
)
},
)
return <Logo onClick={fn} className="cursor-pointer" />
Expand Down
173 changes: 115 additions & 58 deletions src/components/widgets/comment/Comment.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,124 @@
import { memo } from 'react'
import {
createContext,
memo,
useContext,
useLayoutEffect,
useMemo,
useRef,
} from 'react'
import { createPortal } from 'react-dom'
import clsx from 'clsx'
import { atom, useAtomValue } from 'jotai'
import Markdown from 'markdown-to-jsx'
import Image from 'next/image'
import type { CommentModel } from '@mx-space/api-client'
import type { MarkdownToJSX } from 'markdown-to-jsx'
import type { PropsWithChildren } from 'react'

import { RelativeTime } from '~/components/ui/relative-time'
import { jotaiStore } from '~/lib/store'

import styles from './Comment.module.css'
import { CommentPinButton, OcticonGistSecret } from './CommentPinButton'
import { CommentReplyButton } from './CommentReplyButton'

export const Comment: Component<{
comment: CommentModel
showLine?: boolean
}> = memo((props) => {
const { comment, className, showLine } = props
const { id: cid, avatar, author, text } = comment
const elAtom = useMemo(() => atom<HTMLDivElement | null>(null), [])
// FIXME 兜一下后端给的脏数据
if (typeof comment === 'string') return null
const { id: cid, avatar, author, text, key, location, isWhispers } = comment
const parentId =
typeof comment.parent === 'string' ? comment.parent : comment.parent?.id
return (
<>
<li
data-comment-id={cid}
data-parent-id={parentId}
className={clsx('relative', className)}
>
<div className="flex w-full items-stretch gap-2">
<div className="flex w-9 shrink-0 items-end">
<Image
src={avatar}
alt=""
className="h-9 w-9 select-none rounded-full bg-zinc-200 ring-2 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-800 "
width={24}
height={24}
unoptimized
/>
</div>
<div className={clsx('flex flex-1 flex-col', 'items-start')}>
<span
className={clsx(
'flex items-center gap-2 font-semibold text-zinc-800 dark:text-zinc-200',
'mb-2',
)}
>
<span className="ml-2">{author}</span>
<span className="inline-flex select-none text-[10px] font-medium opacity-40">
<RelativeTime date={comment.created} />
</span>
</span>
<CommentHolderContext.Provider value={elAtom}>
<li
data-comment-id={cid}
data-parent-id={parentId}
className={clsx('relative', className)}
>
<div className="group flex w-full items-stretch gap-2">
<div className="flex w-9 shrink-0 items-end">
<Image
src={avatar}
alt=""
className="h-9 w-9 select-none rounded-full bg-zinc-200 ring-2 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-800 "
width={24}
height={24}
unoptimized
/>
</div>

{/* Header */}
<div
className={clsx(
styles['comment__message'],
'group relative inline-block rounded-xl px-2 py-1 text-zinc-800 dark:text-zinc-200',
'rounded-bl-sm bg-zinc-600/5 dark:bg-zinc-500/20',
'flex flex-1 flex-col',
'w-full min-w-0 items-start',
)}
>
<Markdown
// value={`${
// comment.parent
// ? `@${
// commentIdMap.get(comment.parent as any as string)?.id ??
// (comment.parent as any as CommentModel)?.id ??
// ''
// } `
// : ''
// }${comment.text}`}
<span
className={clsx(
'flex items-center gap-2 font-semibold text-zinc-800 dark:text-zinc-200',
'relative mb-2 w-full min-w-0 justify-center',
)}
>
<span className="flex flex-grow items-center gap-2">
<span className="ml-2">{author}</span>
<span className="flex select-none items-center space-x-2">
<span className="inline-flex text-[10px] font-medium opacity-40">
<RelativeTime date={comment.created} />
</span>
<span className="text-[10px] opacity-30">{key}</span>
{!!location && (
<span className="text-[10px] opacity-[0.35]">
来自:{location}
</span>
)}
{!!isWhispers && <OcticonGistSecret />}
</span>
</span>

<span className="flex-shrink-0">
<CommentPinButton comment={comment} />
</span>
</span>

options={{
disabledTypes,
disableParsingRawHTML: true,
forceBlock: true,
}}
{/* Content */}
<div
className={clsx(
styles['comment__message'],
'relative inline-block rounded-xl px-2 py-1 text-zinc-800 dark:text-zinc-200',
'rounded-bl-sm bg-zinc-600/5 dark:bg-zinc-500/20',
)}
>
{text}
</Markdown>
<Markdown
options={{
disabledTypes,
disableParsingRawHTML: true,
forceBlock: true,
}}
>
{text}
</Markdown>
<CommentReplyButton commentId={comment.id} />
</div>
</div>
</div>
</div>

{showLine && (
<span
className="absolute left-5 top-0 -ml-px h-[calc(100%-3rem)] w-0.5 rounded bg-zinc-200 dark:bg-neutral-700"
aria-hidden="true"
/>
)}
</li>
{showLine && (
<span
className="absolute left-5 top-0 -ml-px h-[calc(100%-3rem)] w-0.5 rounded bg-zinc-200 dark:bg-neutral-700"
aria-hidden="true"
/>
)}
</li>

<CommentBoxHolderProvider />
</CommentHolderContext.Provider>
{comment.children &&
comment.children.length > 0 &&
comment.children.map((child) => (
Expand All @@ -94,6 +128,29 @@ export const Comment: Component<{
)
})

const CommentHolderContext = createContext(atom(null as null | HTMLDivElement))

const CommentBoxHolderProvider = () => {
const ref = useRef<HTMLDivElement>(null)
const commentBoxHolderElementAtom = useContext(CommentHolderContext)
useLayoutEffect(() => {
jotaiStore.set(commentBoxHolderElementAtom, ref.current)

return () => {
jotaiStore.set(commentBoxHolderElementAtom, null)
}
}, [commentBoxHolderElementAtom])
return <div ref={ref} />
}

export const CommentBoxHolderPortal = (props: PropsWithChildren) => {
const portalElement = useAtomValue(useContext(CommentHolderContext))

if (!portalElement) return null

return createPortal(props.children, portalElement)
}

const disabledTypes = [
'heading',
'blockQuote',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import Image from 'next/image'

import { useUser } from '@clerk/nextjs'

import { getRandomPlaceholder } from '../constants'
import { CommentAuthedInputSkeleton } from './CommentAuthedInputSkeleton'
import { CommentBoxActionBar } from './CommentBoxActionBar'
import {
useCommentBoxTextValue,
useSetCommentBoxValues,
} from './CommentBoxProvider'
import { getRandomPlaceholder } from './constants'

export const CommentAuthedInput = () => {
const { user } = useUser()
Expand Down
Loading

0 comments on commit 8c1a631

Please sign in to comment.