From 33906adfe489cbc3489bc014c4a3253d96660ec0 Mon Sep 17 00:00:00 2001
From: James Grugett
Date: Thu, 4 Aug 2022 16:49:59 -0700
Subject: [PATCH] Revert "Switch comments/chat to rich text editor (#703)"
This reverts commit f52da72115bfacb0af5a4d54c137a936b33d9eee.
---
common/comment.ts | 6 +-
functions/src/create-notification.ts | 19 +-
functions/src/emails.ts | 4 +-
.../src/on-create-comment-on-contract.ts | 11 +-
web/components/comments-list.tsx | 7 +-
.../contract/contract-leaderboard.tsx | 1 +
web/components/editor.tsx | 30 ++-
.../feed/feed-answer-comment-group.tsx | 28 +-
web/components/feed/feed-comments.tsx | 242 +++++++++++-------
web/components/groups/group-chat.tsx | 104 ++++----
web/lib/firebase/comments.ts | 9 +-
web/pages/[username]/[contractSlug].tsx | 1 +
12 files changed, 266 insertions(+), 196 deletions(-)
diff --git a/common/comment.ts b/common/comment.ts
index a217b292..0d0c4daf 100644
--- a/common/comment.ts
+++ b/common/comment.ts
@@ -1,5 +1,3 @@
-import type { JSONContent } from '@tiptap/core'
-
// Currently, comments are created after the bet, not atomically with the bet.
// They're uniquely identified by the pair contractId/betId.
export type Comment = {
@@ -11,9 +9,7 @@ export type Comment = {
replyToCommentId?: string
userId: string
- /** @deprecated - content now stored as JSON in content*/
- text?: string
- content: JSONContent
+ text: string
createdTime: number
// Denormalized, for rendering comments
diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts
index 6e312906..e16920f7 100644
--- a/functions/src/create-notification.ts
+++ b/functions/src/create-notification.ts
@@ -7,7 +7,7 @@ import {
} from '../../common/notification'
import { User } from '../../common/user'
import { Contract } from '../../common/contract'
-import { getValues } from './utils'
+import { getUserByUsername, getValues } from './utils'
import { Comment } from '../../common/comment'
import { uniq } from 'lodash'
import { Bet, LimitBet } from '../../common/bet'
@@ -17,7 +17,6 @@ import { removeUndefinedProps } from '../../common/util/object'
import { TipTxn } from '../../common/txn'
import { Group, GROUP_CHAT_SLUG } from '../../common/group'
import { Challenge } from '../../common/challenge'
-import { richTextToString } from 'common/util/parse'
const firestore = admin.firestore()
type user_to_reason_texts = {
@@ -156,6 +155,17 @@ export const createNotification = async (
}
}
+ /** @deprecated parse from rich text instead */
+ const parseMentions = async (source: string) => {
+ const mentions = source.match(/@\w+/g)
+ if (!mentions) return []
+ return Promise.all(
+ mentions.map(
+ async (username) => (await getUserByUsername(username.slice(1)))?.id
+ )
+ )
+ }
+
const notifyTaggedUsers = (
userToReasonTexts: user_to_reason_texts,
userIds: (string | undefined)[]
@@ -291,7 +301,8 @@ export const createNotification = async (
if (sourceType === 'comment') {
if (recipients?.[0] && relatedSourceType)
notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType)
- if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? [])
+ if (sourceText)
+ notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText))
}
await notifyContractCreator(userToReasonTexts, sourceContract)
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
@@ -416,7 +427,7 @@ export const createGroupCommentNotification = async (
sourceUserName: fromUser.name,
sourceUserUsername: fromUser.username,
sourceUserAvatarUrl: fromUser.avatarUrl,
- sourceText: richTextToString(comment.content),
+ sourceText: comment.text,
sourceSlug,
sourceTitle: `${group.name}`,
isSeenOnHref: sourceSlug,
diff --git a/functions/src/emails.ts b/functions/src/emails.ts
index d594ae65..b7469e9f 100644
--- a/functions/src/emails.ts
+++ b/functions/src/emails.ts
@@ -17,7 +17,6 @@ import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail } from './send-email'
import { getPrivateUser, getUser } from './utils'
import { getFunctionUrl } from '../../common/api'
-import { richTextToString } from 'common/util/parse'
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
@@ -292,8 +291,7 @@ export const sendNewCommentEmail = async (
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
- const { content } = comment
- const text = richTextToString(content)
+ const { text } = comment
let betDescription = ''
if (bet) {
diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts
index a8bc567e..4719fd08 100644
--- a/functions/src/on-create-comment-on-contract.ts
+++ b/functions/src/on-create-comment-on-contract.ts
@@ -1,13 +1,13 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
-import { compact, uniq } from 'lodash'
+import { uniq } from 'lodash'
+
import { getContract, getUser, getValues } from './utils'
import { Comment } from '../../common/comment'
import { sendNewCommentEmail } from './emails'
import { Bet } from '../../common/bet'
import { Answer } from '../../common/answer'
import { createNotification } from './create-notification'
-import { parseMentions, richTextToString } from 'common/util/parse'
const firestore = admin.firestore()
@@ -71,10 +71,7 @@ export const onCreateCommentOnContract = functions
const repliedUserId = comment.replyToCommentId
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
: answer?.userId
-
- const recipients = uniq(
- compact([...parseMentions(comment.content), repliedUserId])
- )
+ const recipients = repliedUserId ? [repliedUserId] : []
await createNotification(
comment.id,
@@ -82,7 +79,7 @@ export const onCreateCommentOnContract = functions
'created',
commentCreator,
eventId,
- richTextToString(comment.content),
+ comment.text,
{ contract, relatedSourceType, recipients }
)
diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx
index 2a467f6d..f8e1d7e1 100644
--- a/web/components/comments-list.tsx
+++ b/web/components/comments-list.tsx
@@ -8,8 +8,8 @@ import { RelativeTimestamp } from './relative-timestamp'
import { UserLink } from './user-page'
import { User } from 'common/user'
import { Col } from './layout/col'
+import { Linkify } from './linkify'
import { groupBy } from 'lodash'
-import { Content } from './editor'
export function UserCommentsList(props: {
user: User
@@ -50,8 +50,7 @@ export function UserCommentsList(props: {
function ProfileComment(props: { comment: Comment; className?: string }) {
const { comment, className } = props
- const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
- comment
+ const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
// TODO: find and attach relevant bets by comment betId at some point
return (
@@ -65,7 +64,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) {
/>{' '}
-
+
)
diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx
index 6f1a778d..deb9b857 100644
--- a/web/components/contract/contract-leaderboard.tsx
+++ b/web/components/contract/contract-leaderboard.tsx
@@ -107,6 +107,7 @@ export function ContractTopTrades(props: {
comment={commentsById[topCommentId]}
tips={tips[topCommentId]}
betsBySameUser={[betsById[topCommentId]]}
+ truncate={false}
smallAvatar={false}
/>
diff --git a/web/components/editor.tsx b/web/components/editor.tsx
index f71e8589..963cea7e 100644
--- a/web/components/editor.tsx
+++ b/web/components/editor.tsx
@@ -41,16 +41,14 @@ export function useTextEditor(props: {
max?: number
defaultValue?: Content
disabled?: boolean
- simple?: boolean
}) {
- const { placeholder, max, defaultValue = '', disabled, simple } = props
+ const { placeholder, max, defaultValue = '', disabled } = props
const users = useUsers()
const editorClass = clsx(
proseClass,
- !simple && 'min-h-[6em]',
- 'outline-none pt-2 px-4'
+ 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0'
)
const editor = useEditor(
@@ -58,8 +56,7 @@ export function useTextEditor(props: {
editorProps: { attributes: { class: editorClass } },
extensions: [
StarterKit.configure({
- heading: simple ? false : { levels: [1, 2, 3] },
- horizontalRule: simple ? false : {},
+ heading: { levels: [1, 2, 3] },
}),
Placeholder.configure({
placeholder,
@@ -123,9 +120,8 @@ function isValidIframe(text: string) {
export function TextEditor(props: {
editor: Editor | null
upload: ReturnType
- children?: React.ReactNode // additional toolbar buttons
}) {
- const { editor, upload, children } = props
+ const { editor, upload } = props
const [iframeOpen, setIframeOpen] = useState(false)
return (
@@ -147,10 +143,20 @@ export function TextEditor(props: {
images!
)}
-
+
- {/* Toolbar, with buttons for images and embeds */}
-
+ {/* Spacer element to match the height of the toolbar */}
+
+ {/* Matches height of button in toolbar (1px border + 36px content height) */}
+
+
+
+
+ {/* Toolbar, with buttons for image and embeds */}
+
+
Embed an iframe
-
- {children}
diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx
index edaf1fe5..aabb1081 100644
--- a/web/components/feed/feed-answer-comment-group.tsx
+++ b/web/components/feed/feed-answer-comment-group.tsx
@@ -31,9 +31,9 @@ export function FeedAnswerCommentGroup(props: {
const { answer, contract, comments, tips, bets, user } = props
const { username, avatarUrl, name, text } = answer
- const [replyToUser, setReplyToUser] =
- useState
>()
+ const [replyToUsername, setReplyToUsername] = useState('')
const [showReply, setShowReply] = useState(false)
+ const [inputRef, setInputRef] = useState(null)
const [highlighted, setHighlighted] = useState(false)
const router = useRouter()
@@ -70,14 +70,9 @@ export function FeedAnswerCommentGroup(props: {
const scrollAndOpenReplyInput = useEvent(
(comment?: Comment, answer?: Answer) => {
- setReplyToUser(
- comment
- ? { id: comment.userId, username: comment.userUsername }
- : answer
- ? { id: answer.userId, username: answer.username }
- : undefined
- )
+ setReplyToUsername(comment?.userUsername ?? answer?.username ?? '')
setShowReply(true)
+ inputRef?.focus()
}
)
@@ -85,7 +80,7 @@ export function FeedAnswerCommentGroup(props: {
// Only show one comment input for a bet at a time
if (
betsByCurrentUser.length > 1 &&
- // inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty
+ inputRef?.textContent?.length === 0 &&
betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0]
?.outcome !== answer.number.toString()
)
@@ -94,6 +89,10 @@ export function FeedAnswerCommentGroup(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [betsByCurrentUser.length, user, answer.number])
+ useEffect(() => {
+ if (showReply && inputRef) inputRef.focus()
+ }, [inputRef, showReply])
+
useEffect(() => {
if (router.asPath.endsWith(`#${answerElementId}`)) {
setHighlighted(true)
@@ -155,6 +154,7 @@ export function FeedAnswerCommentGroup(props: {
commentsList={commentsList}
betsByUserId={betsByUserId}
smallAvatar={true}
+ truncate={false}
bets={bets}
tips={tips}
scrollAndOpenReplyInput={scrollAndOpenReplyInput}
@@ -172,8 +172,12 @@ export function FeedAnswerCommentGroup(props: {
betsByCurrentUser={betsByCurrentUser}
commentsByCurrentUser={commentsByCurrentUser}
parentAnswerOutcome={answer.number.toString()}
- replyToUser={replyToUser}
- onSubmitComment={() => setShowReply(false)}
+ replyToUsername={replyToUsername}
+ setRef={setInputRef}
+ onSubmitComment={() => {
+ setShowReply(false)
+ setReplyToUsername('')
+ }}
/>
)}
diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx
index fd2dbde2..f4c6eb74 100644
--- a/web/components/feed/feed-comments.tsx
+++ b/web/components/feed/feed-comments.tsx
@@ -13,22 +13,25 @@ import { Avatar } from 'web/components/avatar'
import { UserLink } from 'web/components/user-page'
import { OutcomeLabel } from 'web/components/outcome-label'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
+import { contractPath } from 'web/lib/firebase/contracts'
import { firebaseLogin } from 'web/lib/firebase/users'
import {
createCommentOnContract,
MAX_COMMENT_LENGTH,
} from 'web/lib/firebase/comments'
+import Textarea from 'react-expanding-textarea'
+import { Linkify } from 'web/components/linkify'
+import { SiteLink } from 'web/components/site-link'
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 { useEvent } from 'web/hooks/use-event'
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 { Editor } from '@tiptap/react'
export function FeedCommentThread(props: {
contract: Contract
@@ -36,12 +39,20 @@ export function FeedCommentThread(props: {
tips: CommentTipMap
parentComment: Comment
bets: Bet[]
+ truncate?: boolean
smallAvatar?: boolean
}) {
- const { contract, comments, bets, tips, smallAvatar, parentComment } = props
+ const {
+ contract,
+ comments,
+ bets,
+ tips,
+ truncate,
+ smallAvatar,
+ parentComment,
+ } = props
const [showReply, setShowReply] = useState(false)
- const [replyToUser, setReplyToUser] =
- useState<{ id: string; username: string }>()
+ const [replyToUsername, setReplyToUsername] = useState('')
const betsByUserId = groupBy(bets, (bet) => bet.userId)
const user = useUser()
const commentsList = comments.filter(
@@ -49,12 +60,15 @@ export function FeedCommentThread(props: {
parentComment.id && comment.replyToCommentId === parentComment.id
)
commentsList.unshift(parentComment)
-
+ const [inputRef, setInputRef] = useState(null)
function scrollAndOpenReplyInput(comment: Comment) {
- setReplyToUser({ id: comment.userId, username: comment.userUsername })
+ setReplyToUsername(comment.userUsername)
setShowReply(true)
+ inputRef?.focus()
}
-
+ useEffect(() => {
+ if (showReply && inputRef) inputRef.focus()
+ }, [inputRef, showReply])
return (
@@ -83,9 +98,13 @@ export function FeedCommentThread(props: {
(c) => c.userId === user?.id
)}
parentCommentId={parentComment.id}
- replyToUser={replyToUser}
+ replyToUsername={replyToUsername}
parentAnswerOutcome={comments[0].answerOutcome}
- onSubmitComment={() => setShowReply(false)}
+ setRef={setInputRef}
+ onSubmitComment={() => {
+ setShowReply(false)
+ setReplyToUsername('')
+ }}
/>
)}
@@ -102,12 +121,14 @@ export function CommentRepliesList(props: {
bets: Bet[]
treatFirstIndexEqually?: boolean
smallAvatar?: boolean
+ truncate?: boolean
}) {
const {
contract,
commentsList,
betsByUserId,
tips,
+ truncate,
smallAvatar,
bets,
scrollAndOpenReplyInput,
@@ -147,6 +168,7 @@ export function CommentRepliesList(props: {
: undefined
}
smallAvatar={smallAvatar}
+ truncate={truncate}
/>
))}
@@ -160,6 +182,7 @@ export function FeedComment(props: {
tips: CommentTips
betsBySameUser: Bet[]
probAtCreatedTime?: number
+ truncate?: boolean
smallAvatar?: boolean
onReplyClick?: (comment: Comment) => void
}) {
@@ -169,10 +192,10 @@ export function FeedComment(props: {
tips,
betsBySameUser,
probAtCreatedTime,
+ truncate,
onReplyClick,
} = props
- const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
- comment
+ const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
let betOutcome: string | undefined,
bought: string | undefined,
money: string | undefined
@@ -253,9 +276,11 @@ export function FeedComment(props: {
elementId={comment.id}
/>
-
-
-
+
{onReplyClick && (
@@ -320,7 +345,8 @@ export function CommentInput(props: {
contract: Contract
betsByCurrentUser: Bet[]
commentsByCurrentUser: Comment[]
- replyToUser?: { id: string; username: string }
+ replyToUsername?: string
+ setRef?: (ref: HTMLTextAreaElement) => void
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
@@ -333,18 +359,12 @@ export function CommentInput(props: {
commentsByCurrentUser,
parentAnswerOutcome,
parentCommentId,
- replyToUser,
+ replyToUsername,
onSubmitComment,
+ setRef,
} = props
const user = useUser()
- const { editor, upload } = useTextEditor({
- simple: true,
- max: MAX_COMMENT_LENGTH,
- placeholder:
- !!parentCommentId || !!parentAnswerOutcome
- ? 'Write a reply...'
- : 'Write a comment...',
- })
+ const [comment, setComment] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const mostRecentCommentableBet = getMostRecentCommentableBet(
@@ -360,17 +380,18 @@ export function CommentInput(props: {
track('sign in to comment')
return await firebaseLogin()
}
- if (!editor || editor.isEmpty || isSubmitting) return
+ if (!comment || isSubmitting) return
setIsSubmitting(true)
await createCommentOnContract(
contract.id,
- editor.getJSON(),
+ comment,
user,
betId,
parentAnswerOutcome,
parentCommentId
)
onSubmitComment?.()
+ setComment('')
setIsSubmitting(false)
}
@@ -425,12 +446,14 @@ export function CommentInput(props: {
)}
@@ -442,89 +465,94 @@ export function CommentInput(props: {
export function CommentInputTextArea(props: {
user: User | undefined | null
- replyToUser?: { id: string; username: string }
- editor: Editor | null
- upload: Parameters[0]['upload']
+ isReply: boolean
+ replyToUsername: string
+ commentText: string
+ setComment: (text: string) => void
submitComment: (id?: string) => void
isSubmitting: boolean
- submitOnEnter?: boolean
+ setRef?: (ref: HTMLTextAreaElement) => void
presetId?: string
+ enterToSubmitOnDesktop?: boolean
}) {
const {
+ isReply,
+ setRef,
user,
- editor,
- upload,
+ commentText,
+ setComment,
submitComment,
presetId,
isSubmitting,
- submitOnEnter,
- replyToUser,
+ replyToUsername,
+ enterToSubmitOnDesktop,
} = props
- const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch)
-
+ const { width } = useWindowSize()
+ const memoizedSetComment = useEvent(setComment)
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
- if (replyToUser) {
- editor.commands.insertContentAt(0, {
- type: 'mention',
- attrs: { label: replyToUser.username, id: replyToUser.id },
- })
- editor.commands.focus()
- }
+ if (!replyToUsername || !user || replyToUsername === user.username) return
+ const replacement = `@${replyToUsername} `
+ memoizedSetComment(replacement + commentText.replace(replacement, ''))
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [editor])
-
+ }, [user, replyToUsername, memoizedSetComment])
return (
<>
-
+
+
{!user && (
+
+
-
- {comments.map((comment) => (
-
- ))}
-
{!isCreatorsComment && onReplyClick && (
onReplyClick(first)}
+ onClick={() => onReplyClick(comment)}
>
Reply
@@ -357,7 +357,7 @@ const GroupMessage = memo(function GroupMessage_(props: {
{formatMoney(sum(Object.values(tips)))}
)}
- {!isCreatorsComment && }
+ {!isCreatorsComment && }
)
diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts
index e82c6d45..5775a2bb 100644
--- a/web/lib/firebase/comments.ts
+++ b/web/lib/firebase/comments.ts
@@ -14,7 +14,6 @@ import { User } from 'common/user'
import { Comment } from 'common/comment'
import { removeUndefinedProps } from 'common/util/object'
import { track } from '@amplitude/analytics-browser'
-import { JSONContent } from '@tiptap/react'
export type { Comment }
@@ -22,7 +21,7 @@ export const MAX_COMMENT_LENGTH = 10000
export async function createCommentOnContract(
contractId: string,
- content: JSONContent,
+ text: string,
commenter: User,
betId?: string,
answerOutcome?: string,
@@ -35,7 +34,7 @@ export async function createCommentOnContract(
id: ref.id,
contractId,
userId: commenter.id,
- content: content,
+ text: text.slice(0, MAX_COMMENT_LENGTH),
createdTime: Date.now(),
userName: commenter.name,
userUsername: commenter.username,
@@ -54,7 +53,7 @@ export async function createCommentOnContract(
}
export async function createCommentOnGroup(
groupId: string,
- content: JSONContent,
+ text: string,
user: User,
replyToCommentId?: string
) {
@@ -63,7 +62,7 @@ export async function createCommentOnGroup(
id: ref.id,
groupId,
userId: user.id,
- content: content,
+ text: text.slice(0, MAX_COMMENT_LENGTH),
createdTime: Date.now(),
userName: user.name,
userUsername: user.username,
diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx
index 5866f899..0da6c994 100644
--- a/web/pages/[username]/[contractSlug].tsx
+++ b/web/pages/[username]/[contractSlug].tsx
@@ -354,6 +354,7 @@ function ContractTopTrades(props: {
comment={commentsById[topCommentId]}
tips={tips[topCommentId]}
betsBySameUser={[betsById[topCommentId]]}
+ truncate={false}
smallAvatar={false}
/>