From 60ebadbbe53788641b9060b164f0d12f8390d56e Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Fri, 5 Aug 2022 09:58:02 -0600 Subject: [PATCH 001/123] Add not about donating winnings to charity --- .../challenges/[username]/[contractSlug]/[challengeSlug].tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx index 0df5b7d7..55e78616 100644 --- a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx +++ b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx @@ -92,6 +92,7 @@ export default function ChallengePage(props: { useSaveReferral(currentUser, { defaultReferrerUsername: challenge?.creatorUsername, + contractId: challenge?.contractId, }) if (!contract || !challenge) return @@ -171,7 +172,8 @@ function FAQ() { {toggleWhatIsMana && ( Mana (M$) is the play-money used by our platform to keep track of your - bets. It's completely free for you and your friends to get started! + bets. It's completely free to get started, and you can donate your + winnings to charity! )} From ced404eb74993207ebcbef835f718db60c713c7c Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 5 Aug 2022 12:01:16 -0700 Subject: [PATCH 002/123] Local search filters on groups, exclude contractIds --- web/pages/contract-search-firestore.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index ea42b38a..9039aa50 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -20,6 +20,8 @@ export default function ContractSearchFirestore(props: { additionalFilter?: { creatorId?: string tag?: string + excludeContractIds?: string[] + groupSlug?: string } }) { const contracts = useContracts() @@ -63,7 +65,7 @@ export default function ContractSearchFirestore(props: { } if (additionalFilter) { - const { creatorId, tag } = additionalFilter + const { creatorId, tag, groupSlug, excludeContractIds } = additionalFilter if (creatorId) { matches = matches.filter((c) => c.creatorId === creatorId) @@ -74,6 +76,14 @@ export default function ContractSearchFirestore(props: { c.lowercaseTags.includes(tag.toLowerCase()) ) } + + if (groupSlug) { + matches = matches.filter((c) => c.groupSlugs?.includes(groupSlug)) + } + + if (excludeContractIds) { + matches = matches.filter((c) => !excludeContractIds.includes(c.id)) + } } matches = matches.slice(0, MAX_CONTRACTS_RENDERED) From f11c9a334122993778ecc09e14c43705f95c5583 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Fri, 5 Aug 2022 13:38:12 -0700 Subject: [PATCH 003/123] bouncing challenge button (temporary gimmick) --- web/components/contract/share-row.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx index fd872c5a..fa86094f 100644 --- a/web/components/contract/share-row.tsx +++ b/web/components/contract/share-row.tsx @@ -44,7 +44,12 @@ export function ShareRow(props: { {showChallenge && ( - +
+ {children}
diff --git a/web/components/editor/mention.tsx b/web/components/editor/mention.tsx index 3ad5de39..5ccea6f5 100644 --- a/web/components/editor/mention.tsx +++ b/web/components/editor/mention.tsx @@ -11,7 +11,7 @@ const name = 'mention-component' const MentionComponent = (props: any) => { return ( - + ) @@ -25,5 +25,6 @@ const MentionComponent = (props: any) => { export const DisplayMention = Mention.extend({ parseHTML: () => [{ tag: name }], renderHTML: ({ HTMLAttributes }) => [name, mergeAttributes(HTMLAttributes)], - addNodeView: () => ReactNodeViewRenderer(MentionComponent), + addNodeView: () => + ReactNodeViewRenderer(MentionComponent, { className: 'inline-block' }), }) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index aabb1081..edaf1fe5 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 [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState>() const [showReply, setShowReply] = useState(false) - const [inputRef, setInputRef] = useState(null) const [highlighted, setHighlighted] = useState(false) const router = useRouter() @@ -70,9 +70,14 @@ export function FeedAnswerCommentGroup(props: { const scrollAndOpenReplyInput = useEvent( (comment?: Comment, answer?: Answer) => { - setReplyToUsername(comment?.userUsername ?? answer?.username ?? '') + setReplyToUser( + comment + ? { id: comment.userId, username: comment.userUsername } + : answer + ? { id: answer.userId, username: answer.username } + : undefined + ) setShowReply(true) - inputRef?.focus() } ) @@ -80,7 +85,7 @@ export function FeedAnswerCommentGroup(props: { // Only show one comment input for a bet at a time if ( betsByCurrentUser.length > 1 && - inputRef?.textContent?.length === 0 && + // inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0] ?.outcome !== answer.number.toString() ) @@ -89,10 +94,6 @@ 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) @@ -154,7 +155,6 @@ export function FeedAnswerCommentGroup(props: { commentsList={commentsList} betsByUserId={betsByUserId} smallAvatar={true} - truncate={false} bets={bets} tips={tips} scrollAndOpenReplyInput={scrollAndOpenReplyInput} @@ -172,12 +172,8 @@ export function FeedAnswerCommentGroup(props: { betsByCurrentUser={betsByCurrentUser} commentsByCurrentUser={commentsByCurrentUser} parentAnswerOutcome={answer.number.toString()} - replyToUsername={replyToUsername} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + replyToUser={replyToUser} + onSubmitComment={() => setShowReply(false)} /> )} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index f4c6eb74..8c84039e 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -13,25 +13,22 @@ 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 @@ -39,20 +36,12 @@ export function FeedCommentThread(props: { tips: CommentTipMap parentComment: Comment bets: Bet[] - truncate?: boolean smallAvatar?: boolean }) { - const { - contract, - comments, - bets, - tips, - truncate, - smallAvatar, - parentComment, - } = props + const { contract, comments, bets, tips, smallAvatar, parentComment } = props const [showReply, setShowReply] = useState(false) - const [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState<{ id: string; username: string }>() const betsByUserId = groupBy(bets, (bet) => bet.userId) const user = useUser() const commentsList = comments.filter( @@ -60,15 +49,12 @@ export function FeedCommentThread(props: { parentComment.id && comment.replyToCommentId === parentComment.id ) commentsList.unshift(parentComment) - const [inputRef, setInputRef] = useState(null) + function scrollAndOpenReplyInput(comment: Comment) { - setReplyToUsername(comment.userUsername) + setReplyToUser({ id: comment.userId, username: comment.userUsername }) setShowReply(true) - inputRef?.focus() } - useEffect(() => { - if (showReply && inputRef) inputRef.focus() - }, [inputRef, showReply]) + return ( @@ -98,13 +83,9 @@ export function FeedCommentThread(props: { (c) => c.userId === user?.id )} parentCommentId={parentComment.id} - replyToUsername={replyToUsername} + replyToUser={replyToUser} parentAnswerOutcome={comments[0].answerOutcome} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + onSubmitComment={() => setShowReply(false)} /> )} @@ -121,14 +102,12 @@ export function CommentRepliesList(props: { bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean - truncate?: boolean }) { const { contract, commentsList, betsByUserId, tips, - truncate, smallAvatar, bets, scrollAndOpenReplyInput, @@ -168,7 +147,6 @@ export function CommentRepliesList(props: { : undefined } smallAvatar={smallAvatar} - truncate={truncate} /> ))} @@ -182,7 +160,6 @@ export function FeedComment(props: { tips: CommentTips betsBySameUser: Bet[] probAtCreatedTime?: number - truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { @@ -192,10 +169,10 @@ export function FeedComment(props: { tips, betsBySameUser, probAtCreatedTime, - truncate, onReplyClick, } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const { text, content, userUsername, userName, userAvatarUrl, createdTime } = + comment let betOutcome: string | undefined, bought: string | undefined, money: string | undefined @@ -276,11 +253,9 @@ export function FeedComment(props: { elementId={comment.id} /> - +
+ +
{onReplyClick && ( @@ -345,8 +320,7 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: Comment[] - replyToUsername?: string - setRef?: (ref: HTMLTextAreaElement) => void + replyToUser?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment @@ -359,12 +333,18 @@ export function CommentInput(props: { commentsByCurrentUser, parentAnswerOutcome, parentCommentId, - replyToUsername, + replyToUser, onSubmitComment, - setRef, } = props const user = useUser() - const [comment, setComment] = useState('') + 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( @@ -380,18 +360,17 @@ export function CommentInput(props: { track('sign in to comment') return await firebaseLogin() } - if (!comment || isSubmitting) return + if (!editor || editor.isEmpty || isSubmitting) return setIsSubmitting(true) await createCommentOnContract( contract.id, - comment, + editor.getJSON(), user, betId, parentAnswerOutcome, parentCommentId ) onSubmitComment?.() - setComment('') setIsSubmitting(false) } @@ -446,14 +425,12 @@ export function CommentInput(props: { )} @@ -465,94 +442,93 @@ export function CommentInput(props: { export function CommentInputTextArea(props: { user: User | undefined | null - isReply: boolean - replyToUsername: string - commentText: string - setComment: (text: string) => void + replyToUser?: { id: string; username: string } + editor: Editor | null + upload: Parameters[0]['upload'] submitComment: (id?: string) => void isSubmitting: boolean - setRef?: (ref: HTMLTextAreaElement) => void + submitOnEnter?: boolean presetId?: string - enterToSubmitOnDesktop?: boolean }) { const { - isReply, - setRef, user, - commentText, - setComment, + editor, + upload, submitComment, presetId, isSubmitting, - replyToUsername, - enterToSubmitOnDesktop, + submitOnEnter, + replyToUser, } = props - const { width } = useWindowSize() - const memoizedSetComment = useEvent(setComment) + const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch) + useEffect(() => { - if (!replyToUsername || !user || replyToUsername === user.username) return - const replacement = `@${replyToUsername} ` - memoizedSetComment(replacement + commentText.replace(replacement, '')) + 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 - }, [user, replyToUsername, memoizedSetComment]) + }, [editor]) + return ( <> - -