Uncoupled CommentInput from Contracts

This commit is contained in:
Pico2x 2022-09-07 15:20:23 +01:00
parent 048de52bee
commit 60d207aa6a
7 changed files with 296 additions and 283 deletions

View File

@ -0,0 +1,175 @@
import { PaperAirplaneIcon } from '@heroicons/react/solid'
import { Editor } from '@tiptap/react'
import clsx from 'clsx'
import { User } from 'common/user'
import { useEffect, useState } from 'react'
import { useUser } from 'web/hooks/use-user'
import { useWindowSize } from 'web/hooks/use-window-size'
import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments'
import { Avatar } from './avatar'
import { TextEditor, useTextEditor } from './editor'
import { Row } from './layout/row'
import { LoadingIndicator } from './loading-indicator'
export function CommentInput(props: {
replyToUser?: { id: string; username: string }
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
parentCommentId?: string
onSubmitComment?: (editor: Editor, betId: string | undefined) => void
className?: string
presetId?: string
}) {
const {
parentAnswerOutcome,
parentCommentId,
replyToUser,
onSubmitComment,
presetId,
} = props
const user = useUser()
const { editor, upload } = useTextEditor({
simple: true,
max: MAX_COMMENT_LENGTH,
placeholder:
!!parentCommentId || !!parentAnswerOutcome
? 'Write a reply...'
: 'Write a comment...',
})
const [isSubmitting, setIsSubmitting] = useState(false)
async function submitComment(betId: string | undefined) {
if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true)
onSubmitComment?.(editor, betId)
setIsSubmitting(false)
}
if (user?.isBannedFromPosting) return <></>
return (
<Row className={clsx(props.className, 'mb-2 gap-1 sm:gap-2')}>
<Avatar
avatarUrl={user?.avatarUrl}
username={user?.username}
size="sm"
className="mt-2"
/>
<div className="min-w-0 flex-1 pl-0.5 text-sm">
<CommentInputTextArea
editor={editor}
upload={upload}
replyToUser={replyToUser}
user={user}
submitComment={submitComment}
isSubmitting={isSubmitting}
presetId={presetId}
/>
</div>
</Row>
)
}
export function CommentInputTextArea(props: {
user: User | undefined | null
replyToUser?: { id: string; username: string }
editor: Editor | null
upload: Parameters<typeof TextEditor>[0]['upload']
submitComment: (id?: string) => void
isSubmitting: boolean
submitOnEnter?: boolean
presetId?: string
}) {
const {
user,
editor,
upload,
submitComment,
presetId,
isSubmitting,
submitOnEnter,
replyToUser,
} = props
const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch)
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
const submit = () => {
submitComment(presetId)
editor?.commands?.clearContent()
}
useEffect(() => {
if (!editor) {
return
}
// submit on Enter key
editor.setOptions({
editorProps: {
handleKeyDown: (view, event) => {
if (
submitOnEnter &&
event.key === 'Enter' &&
!event.shiftKey &&
(!isMobile || event.ctrlKey || event.metaKey) &&
// mention list is closed
!(view.state as any).mention$.active
) {
submit()
event.preventDefault()
return true
}
return false
},
},
})
// insert at mention and focus
if (replyToUser) {
editor
.chain()
.setContent({
type: 'mention',
attrs: { label: replyToUser.username, id: replyToUser.id },
})
.insertContent(' ')
.focus()
.run()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor])
return (
<>
<TextEditor editor={editor} upload={upload}>
{user && !isSubmitting && (
<button
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
disabled={!editor || editor.isEmpty}
onClick={submit}
>
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
</button>
)}
{isSubmitting && (
<LoadingIndicator spinnerClassName={'border-gray-500'} />
)}
</TextEditor>
<Row>
{!user && (
<button
className={'btn btn-outline btn-sm mt-2 normal-case'}
onClick={() => submitComment(presetId)}
>
Add my comment
</button>
)}
</Row>
</>
)
}

View File

@ -6,7 +6,7 @@ import { getOutcomeProbability } from 'common/calculate'
import { FeedBet } from './feed-bets'
import { FeedLiquidity } from './feed-liquidity'
import { FeedAnswerCommentGroup } from './feed-answer-comment-group'
import { FeedCommentThread, CommentInput } from './feed-comments'
import { FeedCommentThread, ContractCommentInput } from './feed-comments'
import { User } from 'common/user'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { LiquidityProvision } from 'common/liquidity-provision'
@ -72,7 +72,7 @@ export function ContractCommentsActivity(props: {
return (
<>
<CommentInput
<ContractCommentInput
className="mb-5"
contract={contract}
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}

View File

@ -9,7 +9,7 @@ import { Avatar } from 'web/components/avatar'
import { Linkify } from 'web/components/linkify'
import clsx from 'clsx'
import {
CommentInput,
ContractCommentInput,
FeedComment,
getMostRecentCommentableBet,
} from 'web/components/feed/feed-comments'
@ -177,7 +177,7 @@ export function FeedAnswerCommentGroup(props: {
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
aria-hidden="true"
/>
<CommentInput
<ContractCommentInput
contract={contract}
betsByCurrentUser={betsByCurrentUser}
commentsByCurrentUser={commentsByCurrentUser}

View File

@ -13,22 +13,18 @@ import { Avatar } from 'web/components/avatar'
import { OutcomeLabel } from 'web/components/outcome-label'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { firebaseLogin } from 'web/lib/firebase/users'
import {
createCommentOnContract,
MAX_COMMENT_LENGTH,
} from 'web/lib/firebase/comments'
import { createCommentOnContract } from 'web/lib/firebase/comments'
import { BetStatusText } from 'web/components/feed/feed-bets'
import { Col } from 'web/components/layout/col'
import { getProbability } from 'common/calculate'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { PaperAirplaneIcon } from '@heroicons/react/outline'
import { track } from 'web/lib/service/analytics'
import { Tipper } from '../tipper'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { useWindowSize } from 'web/hooks/use-window-size'
import { Content, TextEditor, useTextEditor } from '../editor'
import { Content } from '../editor'
import { Editor } from '@tiptap/react'
import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input'
export function FeedCommentThread(props: {
user: User | null | undefined
@ -90,14 +86,16 @@ export function FeedCommentThread(props: {
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
aria-hidden="true"
/>
<CommentInput
<ContractCommentInput
contract={contract}
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
parentCommentId={parentComment.id}
replyToUser={replyTo}
parentAnswerOutcome={parentComment.answerOutcome}
onSubmitComment={() => setShowReply(false)}
onSubmitComment={() => {
setShowReply(false)
}}
/>
</Col>
)}
@ -271,67 +269,76 @@ function CommentStatus(props: {
)
}
//TODO: move commentinput and comment input text area into their own files
export function CommentInput(props: {
export function ContractCommentInput(props: {
contract: Contract
betsByCurrentUser: Bet[]
commentsByCurrentUser: ContractComment[]
className?: string
parentAnswerOutcome?: string | undefined
replyToUser?: { id: string; username: string }
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
parentCommentId?: string
onSubmitComment?: () => void
}) {
const {
contract,
betsByCurrentUser,
commentsByCurrentUser,
className,
parentAnswerOutcome,
parentCommentId,
replyToUser,
onSubmitComment,
} = props
const user = useUser()
const { editor, upload } = useTextEditor({
simple: true,
max: MAX_COMMENT_LENGTH,
placeholder:
!!parentCommentId || !!parentAnswerOutcome
? 'Write a reply...'
: 'Write a comment...',
})
const [isSubmitting, setIsSubmitting] = useState(false)
const mostRecentCommentableBet = getMostRecentCommentableBet(
betsByCurrentUser,
commentsByCurrentUser,
user,
parentAnswerOutcome
)
const { id } = mostRecentCommentableBet || { id: undefined }
async function submitComment(betId: string | undefined) {
async function onSubmitComment(editor: Editor, betId: string | undefined) {
if (!user) {
track('sign in to comment')
return await firebaseLogin()
}
if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true)
await createCommentOnContract(
contract.id,
props.contract.id,
editor.getJSON(),
user,
betId,
parentAnswerOutcome,
parentCommentId
props.parentAnswerOutcome,
props.parentCommentId
)
onSubmitComment?.()
setIsSubmitting(false)
props.onSubmitComment?.()
}
const mostRecentCommentableBet = getMostRecentCommentableBet(
props.betsByCurrentUser,
props.commentsByCurrentUser,
user,
props.parentAnswerOutcome
)
const { id } = mostRecentCommentableBet || { id: undefined }
return (
<Col>
<CommentBetArea
betsByCurrentUser={props.betsByCurrentUser}
contract={props.contract}
commentsByCurrentUser={props.commentsByCurrentUser}
parentAnswerOutcome={props.parentAnswerOutcome}
user={useUser()}
className={props.className}
mostRecentCommentableBet={mostRecentCommentableBet}
/>
<CommentInput
replyToUser={props.replyToUser}
parentAnswerOutcome={props.parentAnswerOutcome}
parentCommentId={props.parentCommentId}
onSubmitComment={onSubmitComment}
className={props.className}
presetId={id}
/>
</Col>
)
}
function CommentBetArea(props: {
betsByCurrentUser: Bet[]
contract: Contract
commentsByCurrentUser: ContractComment[]
parentAnswerOutcome?: string
user?: User | null
className?: string
mostRecentCommentableBet?: Bet
}) {
const { betsByCurrentUser, contract, user, mostRecentCommentableBet } = props
const { userPosition, outcome } = getBettorsLargestPositionBeforeTime(
contract,
Date.now(),
@ -340,158 +347,36 @@ export function CommentInput(props: {
const isNumeric = contract.outcomeType === 'NUMERIC'
if (user?.isBannedFromPosting) return <></>
return (
<Row className={clsx(className, 'mb-2 gap-1 sm:gap-2')}>
<Avatar
avatarUrl={user?.avatarUrl}
username={user?.username}
size="sm"
className="mt-2"
/>
<div className="min-w-0 flex-1 pl-0.5 text-sm">
<div className="mb-1 text-gray-500">
{mostRecentCommentableBet && (
<BetStatusText
<Row className={clsx(props.className, 'mb-2 gap-1 sm:gap-2')}>
<div className="mb-1 text-gray-500">
{mostRecentCommentableBet && (
<BetStatusText
contract={contract}
bet={mostRecentCommentableBet}
isSelf={true}
hideOutcome={isNumeric || contract.outcomeType === 'FREE_RESPONSE'}
/>
)}
{!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && (
<>
{"You're"}
<CommentStatus
outcome={outcome}
contract={contract}
bet={mostRecentCommentableBet}
isSelf={true}
hideOutcome={
isNumeric || contract.outcomeType === 'FREE_RESPONSE'
prob={
contract.outcomeType === 'BINARY'
? getProbability(contract)
: undefined
}
/>
)}
{!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && (
<>
{"You're"}
<CommentStatus
outcome={outcome}
contract={contract}
prob={
contract.outcomeType === 'BINARY'
? getProbability(contract)
: undefined
}
/>
</>
)}
</div>
<CommentInputTextArea
editor={editor}
upload={upload}
replyToUser={replyToUser}
user={user}
submitComment={submitComment}
isSubmitting={isSubmitting}
presetId={id}
/>
</>
)}
</div>
</Row>
)
}
export function CommentInputTextArea(props: {
user: User | undefined | null
replyToUser?: { id: string; username: string }
editor: Editor | null
upload: Parameters<typeof TextEditor>[0]['upload']
submitComment: (id?: string) => void
isSubmitting: boolean
submitOnEnter?: boolean
presetId?: string
}) {
const {
user,
editor,
upload,
submitComment,
presetId,
isSubmitting,
submitOnEnter,
replyToUser,
} = props
const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch)
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
const submit = () => {
submitComment(presetId)
editor?.commands?.clearContent()
}
useEffect(() => {
if (!editor) {
return
}
// submit on Enter key
editor.setOptions({
editorProps: {
handleKeyDown: (view, event) => {
if (
submitOnEnter &&
event.key === 'Enter' &&
!event.shiftKey &&
(!isMobile || event.ctrlKey || event.metaKey) &&
// mention list is closed
!(view.state as any).mention$.active
) {
submit()
event.preventDefault()
return true
}
return false
},
},
})
// insert at mention and focus
if (replyToUser) {
editor
.chain()
.setContent({
type: 'mention',
attrs: { label: replyToUser.username, id: replyToUser.id },
})
.insertContent(' ')
.focus()
.run()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor])
return (
<>
<TextEditor editor={editor} upload={upload}>
{user && !isSubmitting && (
<button
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
disabled={!editor || editor.isEmpty}
onClick={submit}
>
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
</button>
)}
{isSubmitting && (
<LoadingIndicator spinnerClassName={'border-gray-500'} />
)}
</TextEditor>
<Row>
{!user && (
<button
className={'btn btn-outline btn-sm mt-2 normal-case'}
onClick={() => submitComment(presetId)}
>
Add my comment
</button>
)}
</Row>
</>
)
}
function getBettorsLargestPositionBeforeTime(
contract: Contract,
createdTime: number,

View File

@ -6,7 +6,6 @@ import { Avatar } from 'web/components/avatar'
import { Group } from 'common/group'
import { Comment, GroupComment } from 'common/comment'
import { createCommentOnGroup } from 'web/lib/firebase/comments'
import { CommentInputTextArea } from 'web/components/feed/feed-comments'
import { track } from 'web/lib/service/analytics'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useRouter } from 'next/router'
@ -23,6 +22,7 @@ import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
import { setNotificationsAsSeen } from 'web/pages/notifications'
import { usePrivateUser } from 'web/hooks/use-user'
import { UserLink } from 'web/components/user-link'
import { CommentInputTextArea } from '../comment-input'
export function GroupChat(props: {
messages: GroupComment[]

View File

@ -20,7 +20,7 @@ import { listAllCommentsOnPost } from 'web/lib/firebase/comments'
import { PostComment } from 'common/comment'
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
import { groupBy, sortBy } from 'lodash'
import { PostCommentThread, CommentInput } from 'web/posts/post-comments'
import { PostCommentInput, PostCommentThread } from 'web/posts/post-comments'
import { useCommentsOnPost } from 'web/hooks/use-comments'
export async function getStaticProps(props: { params: { slugs: string[] } }) {
@ -137,11 +137,7 @@ export function PostCommentsActivity(props: {
return (
<>
<CommentInput
className="mb-5"
post={post}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
/>
<PostCommentInput post={post} />
{topLevelComments.map((parent) => (
<PostCommentThread
key={parent.id}

View File

@ -1,4 +1,5 @@
import { track } from '@amplitude/analytics-browser'
import { Editor } from '@tiptap/core'
import clsx from 'clsx'
import { PostComment } from 'common/comment'
import { Post } from 'common/post'
@ -7,19 +8,16 @@ import { Dictionary } from 'lodash'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Avatar } from 'web/components/avatar'
import { Content, useTextEditor } from 'web/components/editor'
import { CommentInput } from 'web/components/comment-input'
import { Content } from 'web/components/editor'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { CommentInputTextArea } from 'web/components/feed/feed-comments'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Tipper } from 'web/components/tipper'
import { UserLink } from 'web/components/user-link'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { useUser } from 'web/hooks/use-user'
import {
createCommentOnPost,
MAX_COMMENT_LENGTH,
} from 'web/lib/firebase/comments'
import { createCommentOnPost } from 'web/lib/firebase/comments'
import { firebaseLogin } from 'web/lib/firebase/users'
export function PostCommentThread(props: {
@ -30,8 +28,7 @@ export function PostCommentThread(props: {
parentComment: PostComment
commentsByUserId: Dictionary<PostComment[]>
}) {
const { user, post, threadComments, commentsByUserId, tips, parentComment } =
props
const { post, threadComments, tips, parentComment } = props
const [showReply, setShowReply] = useState(false)
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
@ -63,9 +60,8 @@ export function PostCommentThread(props: {
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
aria-hidden="true"
/>
<CommentInput
<PostCommentInput
post={post}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
parentCommentId={parentComment.id}
replyToUser={replyTo}
onSubmitComment={() => setShowReply(false)}
@ -76,6 +72,34 @@ export function PostCommentThread(props: {
)
}
export function PostCommentInput(props: {
post: Post
parentCommentId?: string
replyToUser?: { id: string; username: string }
onSubmitComment?: () => void
}) {
const user = useUser()
const { post, parentCommentId, replyToUser } = props
async function onSubmitComment(editor: Editor) {
if (!user) {
track('sign in to comment')
return await firebaseLogin()
}
await createCommentOnPost(post.id, editor.getJSON(), user, parentCommentId)
props.onSubmitComment?.()
}
return (
<CommentInput
replyToUser={replyToUser}
parentCommentId={parentCommentId}
onSubmitComment={onSubmitComment}
/>
)
}
export function PostComment(props: {
post: Post
comment: PostComment
@ -84,7 +108,7 @@ export function PostComment(props: {
probAtCreatedTime?: number
onReplyClick?: (comment: PostComment) => void
}) {
const { post, comment, tips, indent, probAtCreatedTime, onReplyClick } = props
const { post, comment, tips, indent, onReplyClick } = props
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
comment
@ -147,70 +171,3 @@ export function PostComment(props: {
</Row>
)
}
export function CommentInput(props: {
post: Post
commentsByCurrentUser: PostComment[]
className?: string
replyToUser?: { id: string; username: string }
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
parentCommentId?: string
onSubmitComment?: () => void
}) {
const {
post,
className,
parentAnswerOutcome,
parentCommentId,
replyToUser,
onSubmitComment,
} = props
const user = useUser()
const { editor, upload } = useTextEditor({
simple: true,
max: MAX_COMMENT_LENGTH,
placeholder:
!!parentCommentId || !!parentAnswerOutcome
? 'Write a reply...'
: 'Write a comment...',
})
const [isSubmitting, setIsSubmitting] = useState(false)
async function submitComment(betId: string | undefined) {
if (!user) {
track('sign in to comment')
return await firebaseLogin()
}
if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true)
await createCommentOnPost(post.id, editor.getJSON(), user, parentCommentId)
onSubmitComment?.()
setIsSubmitting(false)
}
if (user?.isBannedFromPosting) return <></>
return (
<Row className={clsx(className, 'mb-2 gap-1 sm:gap-2')}>
<Avatar
avatarUrl={user?.avatarUrl}
username={user?.username}
size="sm"
className="mt-2"
/>
<div className="min-w-0 flex-1 pl-0.5 text-sm">
<div className="mb-1 text-gray-500"></div>
<CommentInputTextArea
editor={editor}
upload={upload}
replyToUser={replyToUser}
user={user}
submitComment={submitComment}
isSubmitting={isSubmitting}
/>
</div>
</Row>
)
}