From beeca57d4e2056b517c6f0dd4c027d0e6fc60b81 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 12 Oct 2022 00:09:45 -0500 Subject: [PATCH 01/67] getting rid of daisy for limit order button (#1026) * getting rid of daisy for limit order button, got rid of betChoice in limit order panel --- web/components/bet-panel.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 72a4fec3..5a895472 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -47,6 +47,7 @@ import { Modal } from './layout/modal' import { Title } from './title' import toast from 'react-hot-toast' import { CheckIcon } from '@heroicons/react/solid' +import { Button } from './button' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -469,7 +470,6 @@ function LimitOrderPanel(props: { const [betAmount, setBetAmount] = useState(undefined) const [lowLimitProb, setLowLimitProb] = useState() const [highLimitProb, setHighLimitProb] = useState() - const betChoice = 'YES' const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) @@ -783,22 +783,18 @@ function LimitOrderPanel(props: { {(hasYesLimitBet || hasNoLimitBet) && } {user && ( - + )} ) From a6d5d5ad1558eacea0a6809ab415b290e0fea560 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 12 Oct 2022 00:24:59 -0500 Subject: [PATCH 02/67] made create a post button not daisy (#1027) yay no daisy --- web/components/create-post.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/components/create-post.tsx b/web/components/create-post.tsx index a4b65661..23a9c116 100644 --- a/web/components/create-post.tsx +++ b/web/components/create-post.tsx @@ -4,12 +4,12 @@ import { Title } from 'web/components/title' import { TextEditor, useTextEditor } from 'web/components/editor' import { createPost } from 'web/lib/firebase/api' -import clsx from 'clsx' import Router from 'next/router' import { MAX_POST_TITLE_LENGTH } from 'common/post' import { postPath } from 'web/lib/firebase/posts' import { Group } from 'common/group' import { ExpandingInput } from './expanding-input' +import { Button } from './button' export function CreatePost(props: { group?: Group }) { const [title, setTitle] = useState('') @@ -91,12 +91,10 @@ export function CreatePost(props: { group?: Group }) { - + {error !== '' &&
{error}
} From b2cd6bbe034e7f4b8f1cc4de39c6f26bd7709506 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:00:52 -0500 Subject: [PATCH 03/67] Inga/de daisy follow button (#1028) * de daisy follow button --- web/components/follow-button.tsx | 38 +++++++++++++------------------ web/components/sign-in-button.tsx | 2 +- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/web/components/follow-button.tsx b/web/components/follow-button.tsx index 6344757d..3c84ec26 100644 --- a/web/components/follow-button.tsx +++ b/web/components/follow-button.tsx @@ -5,57 +5,52 @@ import { useFollows } from 'web/hooks/use-follows' import { useUser } from 'web/hooks/use-user' import { follow, unfollow } from 'web/lib/firebase/users' import { withTracking } from 'web/lib/service/analytics' +import { Button } from './button' export function FollowButton(props: { isFollowing: boolean | undefined onFollow: () => void onUnfollow: () => void - small?: boolean className?: string }) { - const { isFollowing, onFollow, onUnfollow, small, className } = props + const { isFollowing, onFollow, onUnfollow, className } = props const user = useUser() - const smallStyle = - 'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500' - if (!user || isFollowing === undefined) return ( - + ) if (isFollowing) { return ( - + ) } return ( - + ) } -export function UserFollowButton(props: { userId: string; small?: boolean }) { - const { userId, small } = props +export function UserFollowButton(props: { userId: string }) { + const { userId } = props const user = useUser() const following = useFollows(user?.id) const isFollowing = following?.includes(userId) @@ -67,7 +62,6 @@ export function UserFollowButton(props: { userId: string; small?: boolean }) { isFollowing={isFollowing} onFollow={() => follow(user.id, userId)} onUnfollow={() => unfollow(user.id, userId)} - small={small} /> ) } diff --git a/web/components/sign-in-button.tsx b/web/components/sign-in-button.tsx index 48afb6c7..b2f0db69 100644 --- a/web/components/sign-in-button.tsx +++ b/web/components/sign-in-button.tsx @@ -10,7 +10,7 @@ export const SignInButton = (props: { className?: string }) => { return ( + )} )} From 1c209f68f6682790d47f4c160f044862b41977b5 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:31:32 -0500 Subject: [PATCH 05/67] de daisy sell button (#1030) * de daisy sell button --- web/components/sell-row.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/web/components/sell-row.tsx b/web/components/sell-row.tsx index 4c12c35c..54765fa2 100644 --- a/web/components/sell-row.tsx +++ b/web/components/sell-row.tsx @@ -8,6 +8,7 @@ import { OutcomeLabel } from './outcome-label' import { useUserContractBets } from 'web/hooks/use-user-bets' import { useSaveBinaryShares } from './use-save-binary-shares' import { SellSharesModal } from './sell-modal' +import { Button } from './button' export function SellRow(props: { contract: BinaryContract | PseudoNumericContract @@ -37,17 +38,14 @@ export function SellRow(props: { shares - + {showSellModal && ( From f587e0256d80e933a5a7265ab48a2611062719cf Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:58:20 -0500 Subject: [PATCH 06/67] standardizing red and green colors (#1032) --- web/components/answers/answer-item.tsx | 4 ++-- web/components/contract/prob-change-table.tsx | 2 +- web/components/contract/quick-bet.tsx | 8 ++++---- web/components/contract/tip-button.tsx | 2 +- web/components/user-page.tsx | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/components/answers/answer-item.tsx b/web/components/answers/answer-item.tsx index 323a8b9b..71ce677e 100644 --- a/web/components/answers/answer-item.tsx +++ b/web/components/answers/answer-item.tsx @@ -93,7 +93,7 @@ export function AnswerItem(props: {
{probPercent} @@ -144,7 +144,7 @@ export function AnswerItem(props: {
Chosen{' '} diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 48a39db1..594f0653 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -128,7 +128,7 @@ export function ProbChange(props: { probChanges: { day: change }, } = contract - const color = change >= 0 ? 'text-green-500' : 'text-red-500' + const color = change >= 0 ? 'text-teal-500' : 'text-red-400' return ( diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index e4a85139..4c364829 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -166,14 +166,14 @@ export function QuickBet(props: { ) : ( )} @@ -201,14 +201,14 @@ export function QuickBet(props: { ) : ( )} diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx index c34f88ff..b974938f 100644 --- a/web/components/contract/tip-button.tsx +++ b/web/components/contract/tip-button.tsx @@ -46,7 +46,7 @@ export function TipButton(props: { className={clsx( 'h-5 w-5 sm:h-6 sm:w-6', totalTipped > 0 ? 'mr-2' : '', - userTipped ? 'fill-green-700 text-green-700' : '' + userTipped ? 'fill-teal-500 text-teal-500' : '' )} /> {totalTipped > 0 && ( diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 3f2176dc..e722547c 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -303,7 +303,7 @@ export function ProfilePrivateStats(props: { } onClick={() => setShowLoansModal(true)} > - + 🏦 {formatMoney(user.nextLoanCached ?? 0)} next loan From 59cdc9f7760027892656591da099dbd49ea537cf Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 11 Oct 2022 23:59:11 -0700 Subject: [PATCH 07/67] Update FR colors, consolidate non-top answers into "Other" (#1031) * Update FR colors, consolidate non-top answers into "Other" * Fix answer panel coloration to not be weird and work on Firefox --- web/components/answers/answers-panel.tsx | 17 +-- web/components/charts/contract/choice.tsx | 148 +++++++++------------- 2 files changed, 66 insertions(+), 99 deletions(-) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 6b35f74e..a665a921 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -23,7 +23,7 @@ import { Linkify } from 'web/components/linkify' import { Button } from 'web/components/button' import { useAdmin } from 'web/hooks/use-admin' import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]' -import { CATEGORY_COLORS } from '../charts/contract/choice' +import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice' import { useChartAnswers } from '../charts/contract/choice' export function AnswersPanel(props: { @@ -190,7 +190,10 @@ function OpenAnswer(props: { const probPercent = formatPercent(prob) const [open, setOpen] = useState(false) const color = - colorIndex != undefined ? CATEGORY_COLORS[colorIndex] : '#B1B1C7' + colorIndex != undefined && colorIndex < CHOICE_ANSWER_COLORS.length + ? CHOICE_ANSWER_COLORS[colorIndex] + '55' // semi-transparent + : '#B1B1C755' + const colorWidth = 100 * Math.max(prob, 0.01) return ( @@ -206,9 +209,12 @@ function OpenAnswer(props: { @@ -236,11 +242,6 @@ function OpenAnswer(props: { )} -
) diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 0355b4b5..31ca9b47 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { last, sum, sortBy, groupBy } from 'lodash' +import { last, range, sum, sortBy, groupBy } from 'lodash' import { scaleTime, scaleLinear } from 'd3-scale' import { curveStepAfter } from 'd3-shape' @@ -19,83 +19,36 @@ import { MultiPoint, MultiValueHistoryChart } from '../generic-charts' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' -export const CATEGORY_COLORS = [ - '#7eb0d5', - '#fd7f6f', - '#b2e061', - '#bd7ebe', - '#ffb55a', - '#ffee65', - '#beb9db', - '#fdcce5', - '#8bd3c7', - '#bddfb7', - '#e2e3f3', - '#fafafa', - '#9fcdeb', - '#d3d3d3', - '#b1a296', - '#e1bdb6', - '#f2dbc0', - '#fae5d3', - '#c5e0ec', - '#e0f0ff', - '#ffddcd', - '#fbd5e2', - '#f2e7e5', - '#ffe7ba', - '#eed9c4', - '#ea9999', - '#f9cb9c', - '#ffe599', - '#b6d7a8', - '#a2c4c9', - '#9fc5e8', - '#b4a7d6', - '#d5a6bd', - '#e06666', - '#f6b26b', - '#ffd966', - '#93c47d', - '#76a5af', - '#6fa8dc', - '#8e7cc3', - '#c27ba0', - '#cc0000', - '#e69138', - '#f1c232', - '#6aa84f', - '#45818e', - '#3d85c6', - '#674ea7', - '#a64d79', - '#990000', - '#b45f06', - '#bf9000', +type ChoiceContract = FreeResponseContract | MultipleChoiceContract + +export const CHOICE_ANSWER_COLORS = [ + '#97C1EB', + '#F39F83', + '#F9EBA5', + '#FFC7D2', + '#C7ECFF', + '#8CDEC7', + '#DBE96F', ] +export const CHOICE_OTHER_COLOR = '#CCC' +export const CHOICE_ALL_COLORS = [...CHOICE_ANSWER_COLORS, CHOICE_OTHER_COLOR] const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 } const MARGIN_X = MARGIN.left + MARGIN.right const MARGIN_Y = MARGIN.top + MARGIN.bottom -const getTrackedAnswers = ( - contract: FreeResponseContract | MultipleChoiceContract, - topN: number -) => { - const { answers, outcomeType, totalBets } = contract - const validAnswers = answers.filter((answer) => { - return ( - (answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') && - totalBets[answer.id] > 0.000000001 - ) - }) +const getAnswers = (contract: ChoiceContract) => { + const { answers, outcomeType } = contract + const validAnswers = answers.filter( + (answer) => answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE' + ) return sortBy( validAnswers, (answer) => -1 * getOutcomeProbability(contract, answer.id) - ).slice(0, topN) + ) } -const getBetPoints = (answers: Answer[], bets: Bet[]) => { +const getBetPoints = (answers: Answer[], bets: Bet[], topN?: number) => { const sortedBets = sortBy(bets, (b) => b.createdTime) const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome) const sharesByOutcome = Object.fromEntries( @@ -109,11 +62,14 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => { const sharesSquared = sum( Object.values(sharesByOutcome).map((shares) => shares ** 2) ) - points.push({ - x: new Date(bet.createdTime), - y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared), - obj: bet, - }) + const probs = answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared) + + if (topN != null && answers.length > topN) { + const y = [...probs.slice(0, topN), sum(probs.slice(topN))] + points.push({ x: new Date(bet.createdTime), y, obj: bet }) + } else { + points.push({ x: new Date(bet.createdTime), y: probs, obj: bet }) + } } return points } @@ -141,17 +97,12 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => { ) } -export function useChartAnswers( - contract: FreeResponseContract | MultipleChoiceContract -) { - return useMemo( - () => getTrackedAnswers(contract, CATEGORY_COLORS.length), - [contract] - ) +export function useChartAnswers(contract: ChoiceContract) { + return useMemo(() => getAnswers(contract), [contract]) } export const ChoiceContractChart = (props: { - contract: FreeResponseContract | MultipleChoiceContract + contract: ChoiceContract bets: Bet[] width: number height: number @@ -160,18 +111,33 @@ export const ChoiceContractChart = (props: { const { contract, bets, width, height, onMouseOver } = props const [start, end] = getDateRange(contract) const answers = useChartAnswers(contract) - const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets]) - const data = useMemo( - () => [ - { x: new Date(start), y: answers.map((_) => 0) }, + const topN = Math.min(CHOICE_ANSWER_COLORS.length, answers.length) + const betPoints = useMemo( + () => getBetPoints(answers, bets, topN), + [answers, bets, topN] + ) + const endProbs = useMemo( + () => answers.map((a) => getOutcomeProbability(contract, a.id)), + [answers, contract] + ) + + const data = useMemo(() => { + const yCount = answers.length > topN ? topN + 1 : topN + const startY = range(0, yCount).map((_) => 0) + const endY = + answers.length > topN + ? [...endProbs.slice(0, topN), sum(endProbs.slice(topN))] + : endProbs + return [ + { x: new Date(start), y: startY }, ...betPoints, { x: new Date(end ?? Date.now() + DAY_MS), - y: answers.map((a) => getOutcomeProbability(contract, a.id)), + y: endY, }, - ], - [answers, contract, betPoints, start, end] - ) + ] + }, [answers.length, topN, betPoints, endProbs, start, end]) + const rightmostDate = getRightmostVisibleDate( end, last(betPoints)?.x?.getTime(), @@ -188,8 +154,8 @@ export const ChoiceContractChart = (props: { const d = xScale.invert(x) const legendItems = sortBy( data.y.map((p, i) => ({ - color: CATEGORY_COLORS[i], - label: answers[i].text, + color: CHOICE_ALL_COLORS[i], + label: i === CHOICE_ANSWER_COLORS.length ? 'Other' : answers[i].text, value: formatPct(p), p, })), @@ -221,7 +187,7 @@ export const ChoiceContractChart = (props: { yScale={yScale} yKind="percent" data={data} - colors={CATEGORY_COLORS} + colors={CHOICE_ALL_COLORS} curve={curveStepAfter} onMouseOver={onMouseOver} Tooltip={ChoiceTooltip} From 3fc53112b98c4eff29b47bee1cb092fdc5f9814c Mon Sep 17 00:00:00 2001 From: FRC Date: Wed, 12 Oct 2022 13:24:22 +0100 Subject: [PATCH 08/67] New implementation of market card embeddings (#1025) * Grids of cards now implemented by rendering component instead of iframe * Sinclair's nit --- web/components/editor.tsx | 4 ++ web/components/editor/market-modal.tsx | 7 ++- web/components/editor/tiptap-grid-cards.tsx | 55 +++++++++++++++++++++ web/hooks/use-contract.ts | 13 +++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 web/components/editor/tiptap-grid-cards.tsx diff --git a/web/components/editor.tsx b/web/components/editor.tsx index f0b6f4bb..4b42c6be 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -22,6 +22,8 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import { DisplayMention } from './editor/mention' import { DisplayContractMention } from './editor/contract-mention' +import GridComponent from './editor/tiptap-grid-cards' + import Iframe from 'common/util/tiptap-iframe' import TiptapTweet from './editor/tiptap-tweet' import { EmbedModal } from './editor/embed-modal' @@ -78,6 +80,7 @@ export const editorExtensions = (simple = false): Extensions => [ DisplayLink, DisplayMention, DisplayContractMention, + GridComponent, Iframe, TiptapTweet, TiptapSpoiler.configure({ @@ -355,6 +358,7 @@ export function RichContent(props: { DisplayLink.configure({ openOnClick: false }), // stop link opening twice (browser still opens) DisplayMention, DisplayContractMention, + GridComponent, Iframe, TiptapTweet, TiptapSpoiler.configure({ diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx index 1e2c1482..ae0f50e1 100644 --- a/web/components/editor/market-modal.tsx +++ b/web/components/editor/market-modal.tsx @@ -1,7 +1,7 @@ import { Editor } from '@tiptap/react' import { Contract } from 'common/contract' import { SelectMarketsModal } from '../contract-select-modal' -import { embedContractCode, embedContractGridCode } from '../share-embed-button' +import { embedContractCode } from '../share-embed-button' import { insertContent } from './utils' export function MarketModal(props: { @@ -15,7 +15,10 @@ export function MarketModal(props: { if (contracts.length == 1) { insertContent(editor, embedContractCode(contracts[0])) } else if (contracts.length > 1) { - insertContent(editor, embedContractGridCode(contracts)) + insertContent( + editor, + `` + ) } } diff --git a/web/components/editor/tiptap-grid-cards.tsx b/web/components/editor/tiptap-grid-cards.tsx new file mode 100644 index 00000000..48242ea2 --- /dev/null +++ b/web/components/editor/tiptap-grid-cards.tsx @@ -0,0 +1,55 @@ +import { mergeAttributes, Node } from '@tiptap/core' +import React from 'react' +import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' +import { ContractsGrid } from '../contract/contracts-grid' + +import { useContractsFromIds } from 'web/hooks/use-contract' +import { LoadingIndicator } from '../loading-indicator' + +export default Node.create({ + name: 'gridCardsComponent', + + group: 'block', + + atom: true, + + addAttributes() { + return { + contractIds: [], + } + }, + + parseHTML() { + return [ + { + tag: 'grid-cards-component', + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['grid-cards-component', mergeAttributes(HTMLAttributes)] + }, + + addNodeView() { + return ReactNodeViewRenderer(GridComponent) + }, +}) + +export function GridComponent(props: any) { + const contractIds = props.node.attrs.contractIds + const contracts = useContractsFromIds(contractIds.split(',')) + + return ( + + {contracts ? ( + + ) : ( + + )} + + ) +} diff --git a/web/hooks/use-contract.ts b/web/hooks/use-contract.ts index acaf7730..2e7c4f84 100644 --- a/web/hooks/use-contract.ts +++ b/web/hooks/use-contract.ts @@ -3,10 +3,12 @@ import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { Contract, contracts, + getContractFromId, listenForContract, } from 'web/lib/firebase/contracts' import { useStateCheckEquality } from './use-state-check-equality' import { doc, DocumentData } from 'firebase/firestore' +import { useQuery } from 'react-query' export const useContract = (contractId: string) => { const result = useFirestoreDocumentData( @@ -18,6 +20,17 @@ export const useContract = (contractId: string) => { return result.isLoading ? undefined : result.data } +export const useContractsFromIds = (contractIds: string[]) => { + const contractResult = useQuery(['contracts', contractIds], () => + Promise.all(contractIds.map(getContractFromId)) + ) + const contracts = contractResult.data?.filter( + (contract): contract is Contract => !!contract + ) + + return contractResult.isLoading ? undefined : contracts +} + export const useContractWithPreload = ( initial: Contract | null | undefined ) => { From ff6278b147626b3f315b70819cbe782f1092fdd2 Mon Sep 17 00:00:00 2001 From: FRC Date: Wed, 12 Oct 2022 15:04:39 +0100 Subject: [PATCH 09/67] Featured items to homepage (#1024) * Featured items to homepage * Fix nits --- common/globalConfig.ts | 3 + firestore.rules | 6 ++ web/components/groups/group-overview.tsx | 1 - web/components/pinned-select-modal.tsx | 6 +- web/hooks/use-global-config.ts | 12 +++ web/hooks/use-post.ts | 14 +++- web/lib/firebase/globalConfig.ts | 33 ++++++++ web/lib/firebase/posts.ts | 4 + web/pages/home/index.tsx | 99 +++++++++++++++++++++++- 9 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 common/globalConfig.ts create mode 100644 web/hooks/use-global-config.ts create mode 100644 web/lib/firebase/globalConfig.ts diff --git a/common/globalConfig.ts b/common/globalConfig.ts new file mode 100644 index 00000000..fc3e25e7 --- /dev/null +++ b/common/globalConfig.ts @@ -0,0 +1,3 @@ +export type GlobalConfig = { + pinnedItems: { itemId: string; type: 'post' | 'contract' }[] +} diff --git a/firestore.rules b/firestore.rules index 993791b2..0a0ecfe0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -23,6 +23,12 @@ service cloud.firestore { allow read; } + match /globalConfig/globalConfig { + allow read; + allow update: if isAdmin() + allow create: if isAdmin() + } + match /users/{userId} { allow read; allow update: if userId == request.auth.uid diff --git a/web/components/groups/group-overview.tsx b/web/components/groups/group-overview.tsx index d5cdaafa..c5a7a46a 100644 --- a/web/components/groups/group-overview.tsx +++ b/web/components/groups/group-overview.tsx @@ -231,7 +231,6 @@ export function PinnedItems(props: { return pinned.length > 0 || isEditable ? (
- {isEditable && ( + + {isDreaming && ( +
This may take ~10 seconds...
+ )} + {/* TODO: Allow the user to choose their own modifiers */} +
Modifiers: {MODIFIERS}
+ + {/* Show the current imageUrl */} + {/* TODO: Keep the other generated images, so the user can play with different attempts. */} + {imageUrl && ( + <> + Image + + + + + + )} + + ) +} diff --git a/web/package.json b/web/package.json index f64b79e8..5f39d174 100644 --- a/web/package.json +++ b/web/package.json @@ -63,6 +63,7 @@ "react-masonry-css": "1.0.16", "react-query": "3.39.0", "react-twitter-embed": "4.0.4", + "stability-client": "1.5.0", "string-similarity": "^4.0.4", "tippy.js": "6.3.7" }, diff --git a/web/pages/api/v0/dream.ts b/web/pages/api/v0/dream.ts new file mode 100644 index 00000000..df7905f3 --- /dev/null +++ b/web/pages/api/v0/dream.ts @@ -0,0 +1,90 @@ +import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage' +import { nanoid } from 'nanoid' +import { NextApiRequest, NextApiResponse } from 'next' +import { generateAsync } from 'stability-client' +import { storage } from 'web/lib/firebase/init' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' + +export const config = { api: { bodyParser: true } } + +// Highly experimental. Proxy for https://github.com/vpzomtrrfrt/stability-client +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + // Check that prompt and apiKey are included in the body + if (!req.body.prompt) { + res.status(400).json({ message: 'Missing prompt' }) + return + } + if (!req.body.apiKey) { + res.status(400).json({ message: 'Missing apiKey' }) + return + } + /** Optional params: + outDir: string + debug: boolean + requestId: string + samples: number + engine: string + host: string + seed: number + width: number + height: number + diffusion: keyof typeof diffusionMap + steps: number + cfgScale: number + noStore: boolean + imagePrompt: {mime: string; content: Buffer} | null + stepSchedule: {start?: number; end?: number} + */ + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { _dreamResponse, images } = await generateAsync({ + ...req.body, + // Don't actually write to disk, because we're going to upload it to Firestore + noStore: true, + }) + const buffer: Buffer = images[0].buffer + const url = await upload(buffer) + + res.status(200).json({ url }) + } catch (e) { + console.error(e) + res.status(500).json({ message: `Error running code: ${e}` }) + } +} + +// Loosely copied from web/lib/firebase/storage.ts +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 + +async function upload(buffer: Buffer) { + const filename = `${nanoid(10)}.png` + const storageRef = ref(storage, `dream/${filename}`) + + function promisifiedUploadBytes(...args: any[]) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const task = uploadBytesResumable(...args) + task.on( + 'state_changed', + null, + (e: Error) => reject(e), + () => getDownloadURL(task.snapshot.ref).then(resolve) + ) + }) + } + return promisifiedUploadBytes(storageRef, buffer, { + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + contentType: 'image/png', + }) +} diff --git a/yarn.lock b/yarn.lock index 4d57c590..64a5be0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2400,6 +2400,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@improbable-eng/grpc-web-node-http-transport@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web-node-http-transport/-/grpc-web-node-http-transport-0.15.0.tgz#5a064472ef43489cbd075a91fb831c2abeb09d68" + integrity sha512-HLgJfVolGGpjc9DWPhmMmXJx8YGzkek7jcCFO1YYkSOoO81MWRZentPOd/JiKiZuU08wtc4BG+WNuGzsQB5jZA== + +"@improbable-eng/grpc-web@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz#3e47e9fdd90381a74abd4b7d26e67422a2a04bef" + integrity sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg== + dependencies: + browser-headers "^0.4.1" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -4546,6 +4558,11 @@ broadcast-channel@^3.4.1: rimraf "3.0.2" unload "2.2.0" +browser-headers@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/browser-headers/-/browser-headers-0.4.1.tgz#4308a7ad3b240f4203dbb45acedb38dc2d65dd02" + integrity sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg== + browser-image-compression@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-2.0.0.tgz#f421381a76d474d4da7dcd82810daf595b09bef6" @@ -4943,6 +4960,11 @@ commander@^8.0.0, commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" + integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -5821,6 +5843,11 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dotenv@^16.0.2: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -7051,6 +7078,11 @@ google-p12-pem@^3.1.3: dependencies: node-forge "^1.3.1" +google-protobuf@^3.21.0: + version "3.21.2" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4" + integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA== + got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -8748,6 +8780,11 @@ mkdirp@0.3.0: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + module-alias@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" @@ -10822,6 +10859,13 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" +rxjs@^7.5.2: + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== + dependencies: + tslib "^2.1.0" + rxjs@^7.5.4: version "7.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" @@ -11272,6 +11316,22 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +stability-client@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/stability-client/-/stability-client-1.5.0.tgz#f221420a297c808f209c469a0df8fa2401f8f6ae" + integrity sha512-hXuDK6QW/msf50pu8M4L1hTCYG4w5ZrhLgxirigUrSZEARupIPT88O7qz4YSmUbbp9nKlzS8UJ6NjYUH7qy45w== + dependencies: + "@improbable-eng/grpc-web" "^0.15.0" + "@improbable-eng/grpc-web-node-http-transport" "^0.15.0" + commander "^9.4.0" + dotenv "^16.0.2" + google-protobuf "^3.21.0" + mime "^3.0.0" + mkdirp "^1.0.4" + read-pkg-up "^7.0.1" + typed-emitter "^2.1.0" + uuid4 "^2.0.3" + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -11810,6 +11870,13 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" + integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA== + optionalDependencies: + rxjs "^7.5.2" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -12108,6 +12175,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid4@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid4/-/uuid4-2.0.3.tgz#241e5dfe1704a79c52e2aa40e7e581a5e7b01ab4" + integrity sha512-CTpAkEVXMNJl2ojgtpLXHgz23dh8z81u6/HEPiQFOvBc/c2pde6TVHmH4uwY0d/GLF3tb7+VDAj4+2eJaQSdZQ== + uuid@^8.0.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" From e6a90e18e4d443348b239e39d936ed26de3181a7 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 12 Oct 2022 11:59:27 -0500 Subject: [PATCH 21/67] Add more padding and improve layout of post card --- web/components/post-card.tsx | 51 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/web/components/post-card.tsx b/web/components/post-card.tsx index 700f8301..7c268734 100644 --- a/web/components/post-card.tsx +++ b/web/components/post-card.tsx @@ -8,6 +8,7 @@ import { fromNow } from 'web/lib/util/time' import { Avatar } from './avatar' import { Card } from './card' import { CardHighlightOptions } from './contract/contracts-grid' +import { Col } from './layout/col' import { Row } from './layout/row' import { UserLink } from './user-link' @@ -22,39 +23,39 @@ export function PostCard(props: { return ( - -
- - - - • - {fromNow(post.createdTime)} + + + + + + + • + {fromNow(post.createdTime)} + +
+ + Post +
-
+
{post.title}
-
+
{post.subtitle}
-
-
- - - Post - -
+ {onPostClick ? (
Date: Wed, 12 Oct 2022 10:09:59 -0700 Subject: [PATCH 22/67] Simplify rich text to string parsing logic (#1022) * Simplify rich text to string parsing logic * lint --- common/util/parse.ts | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/common/util/parse.ts b/common/util/parse.ts index 53874c9e..04faffe4 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -23,7 +23,7 @@ import { Mention } from '@tiptap/extension-mention' import Iframe from './tiptap-iframe' import TiptapTweet from './tiptap-tweet-type' import { find } from 'linkifyjs' -import { cloneDeep, uniq } from 'lodash' +import { uniq } from 'lodash' import { TiptapSpoiler } from './tiptap-spoiler' /** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */ @@ -51,8 +51,8 @@ export function parseMentions(data: JSONContent): string[] { return uniq(mentions) } -// can't just do [StarterKit, Image...] because it doesn't work with cjs imports const stringParseExts = [ + // StarterKit extensions Blockquote, Bold, BulletList, @@ -69,38 +69,20 @@ const stringParseExts = [ Paragraph, Strike, Text, - - Image, + // other extensions Link, + Image.extend({ renderText: () => '[image]' }), Mention, // user @mention Mention.extend({ name: 'contract-mention' }), // market %mention - Iframe, - TiptapTweet, - TiptapSpoiler, + Iframe.extend({ + renderText: ({ node }) => + '[embed]' + node.attrs.src ? `(${node.attrs.src})` : '', + }), + TiptapTweet.extend({ renderText: () => '[tweet]' }), + TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }), ] export function richTextToString(text?: JSONContent) { if (!text) return '' - // remove spoiler tags. - const newText = cloneDeep(text) - dfs(newText, (current) => { - if (current.marks?.some((m) => m.type === TiptapSpoiler.name)) { - current.text = '[spoiler]' - } else if (current.type === 'image') { - current.text = '[Image]' - // This is a hack, I've no idea how to change a tiptap extenstion's schema - current.type = 'text' - } else if (current.type === 'iframe') { - const src = current.attrs?.['src'] ? current.attrs['src'] : '' - current.text = '[Iframe]' + (src ? ` url:${src}` : '') - // This is a hack, I've no idea how to change a tiptap extenstion's schema - current.type = 'text' - } - }) - return generateText(newText, stringParseExts) -} - -const dfs = (data: JSONContent, f: (current: JSONContent) => any) => { - data.content?.forEach((d) => dfs(d, f)) - f(data) + return generateText(text, stringParseExts) } From 2cda3a4d4f49b4f63f24d22a9210d809f06c0d88 Mon Sep 17 00:00:00 2001 From: Pico2x Date: Wed, 12 Oct 2022 18:12:32 +0100 Subject: [PATCH 23/67] Don't show spinner if pinned items is undefined or zero --- web/components/groups/group-overview.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/groups/group-overview.tsx b/web/components/groups/group-overview.tsx index 16fda8d1..4a3b5802 100644 --- a/web/components/groups/group-overview.tsx +++ b/web/components/groups/group-overview.tsx @@ -192,7 +192,9 @@ function GroupOverviewPinned(props: { updateGroup(group, { pinnedItems: newPinned }) } - return isEditable || (group.pinnedItems && group.pinnedItems.length > 0) ? ( + if (!group.pinnedItems || group.pinnedItems.length == 0) return <> + + return isEditable || (group.pinnedItems && group?.pinnedItems.length > 0) ? ( Date: Wed, 12 Oct 2022 13:05:58 -0500 Subject: [PATCH 24/67] Inga/fr remove double comments (#1019) incorporating answer comments into general comments section --- web/components/answers/answers-panel.tsx | 16 +- web/components/comment-input.tsx | 43 ++- web/components/comments/reply-toggle.tsx | 29 ++ web/components/contract/contract-tabs.tsx | 169 +++++---- web/components/contract/tip-button.tsx | 2 +- web/components/feed/copy-link-date-time.tsx | 2 +- .../feed/feed-answer-comment-group.tsx | 122 +------ web/components/feed/feed-comments.tsx | 322 ++++++++++++------ web/pages/[username]/[contractSlug].tsx | 39 ++- web/public/custom-components/curve.tsx | 19 ++ 10 files changed, 470 insertions(+), 293 deletions(-) create mode 100644 web/components/comments/reply-toggle.tsx create mode 100644 web/public/custom-components/curve.tsx diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index a665a921..f048d8e9 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -25,12 +25,14 @@ import { useAdmin } from 'web/hooks/use-admin' import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]' import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice' import { useChartAnswers } from '../charts/contract/choice' +import { ChatIcon } from '@heroicons/react/outline' export function AnswersPanel(props: { contract: FreeResponseContract | MultipleChoiceContract + onAnswerCommentClick: (answer: Answer) => void }) { const isAdmin = useAdmin() - const { contract } = props + const { contract, onAnswerCommentClick } = props const { creatorId, resolution, resolutions, totalBets, outcomeType } = contract const [showAllAnswers, setShowAllAnswers] = useState(false) @@ -138,6 +140,7 @@ export function AnswersPanel(props: { answer={item} contract={contract} colorIndex={colorSortedAnswer.indexOf(item.text)} + onAnswerCommentClick={onAnswerCommentClick} /> ))} {hasZeroBetAnswers && !showAllAnswers && ( @@ -183,8 +186,9 @@ function OpenAnswer(props: { contract: FreeResponseContract | MultipleChoiceContract answer: Answer colorIndex: number | undefined + onAnswerCommentClick: (answer: Answer) => void }) { - const { answer, contract, colorIndex } = props + const { answer, contract, colorIndex, onAnswerCommentClick } = props const { username, avatarUrl, text } = answer const prob = getDpmOutcomeProbability(contract.totalShares, answer.id) const probPercent = formatPercent(prob) @@ -240,6 +244,14 @@ function OpenAnswer(props: { BUY )} + { + + } diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index 460fa438..385c7828 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -1,12 +1,17 @@ -import { PaperAirplaneIcon } from '@heroicons/react/solid' +import { PaperAirplaneIcon, XCircleIcon } from '@heroicons/react/solid' import { Editor } from '@tiptap/react' import clsx from 'clsx' +import { Answer } from 'common/answer' +import { AnyContractType, Contract } from 'common/contract' import { User } from 'common/user' import { useEffect, useState } from 'react' import { useUser } from 'web/hooks/use-user' import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' +import Curve from 'web/public/custom-components/curve' import { Avatar } from './avatar' import { TextEditor, useTextEditor } from './editor' +import { CommentsAnswer } from './feed/feed-answer-comment-group' +import { ContractCommentInput } from './feed/feed-comments' import { Row } from './layout/row' import { LoadingIndicator } from './loading-indicator' @@ -72,6 +77,40 @@ export function CommentInput(props: { ) } +export function AnswerCommentInput(props: { + contract: Contract + answerResponse: Answer + onCancelAnswerResponse?: () => void +}) { + const { contract, answerResponse, onCancelAnswerResponse } = props + const replyTo = { + id: answerResponse.id, + username: answerResponse.username, + } + + return ( + <> + + +
+ +
+
+ + +
+
+ + ) +} export function CommentInputTextArea(props: { user: User | undefined | null @@ -123,7 +162,7 @@ export function CommentInputTextArea(props: { attrs: { label: replyTo.username, id: replyTo.id }, }) .insertContent(' ') - .focus() + .focus(undefined, { scrollIntoView: false }) .run() } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/components/comments/reply-toggle.tsx b/web/components/comments/reply-toggle.tsx new file mode 100644 index 00000000..934efe53 --- /dev/null +++ b/web/components/comments/reply-toggle.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx' +import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' +import { Row } from '../layout/row' + +export function ReplyToggle(props: { + seeReplies: boolean + numComments: number + onClick: () => void +}) { + const { seeReplies, numComments, onClick } = props + return ( + + ) +} diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 8531697b..b8b57510 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -1,9 +1,8 @@ import { memo, useState } from 'react' -import { getOutcomeProbability } from 'common/calculate' import { Pagination } from 'web/components/pagination' import { FeedBet } from '../feed/feed-bets' import { FeedLiquidity } from '../feed/feed-liquidity' -import { FeedAnswerCommentGroup } from '../feed/feed-answer-comment-group' +import { CommentsAnswer } from '../feed/feed-answer-comment-group' import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments' import { groupBy, sortBy, sum } from 'lodash' import { Bet } from 'common/bet' @@ -25,7 +24,6 @@ import { import { buildArray } from 'common/util/array' import { ContractComment } from 'common/comment' -import { Button } from 'web/components/button' import { MINUTE_MS } from 'common/util/time' import { useUser } from 'web/hooks/use-user' import { Tooltip } from 'web/components/tooltip' @@ -36,14 +34,27 @@ import { usePersistentState, } from 'web/hooks/use-persistent-state' import { safeLocalStorage } from 'web/lib/util/local' +import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' +import Curve from 'web/public/custom-components/curve' +import { Answer } from 'common/answer' +import { AnswerCommentInput } from '../comment-input' export function ContractTabs(props: { contract: Contract bets: Bet[] userBets: Bet[] comments: ContractComment[] + answerResponse?: Answer | undefined + onCancelAnswerResponse?: () => void }) { - const { contract, bets, userBets, comments } = props + const { + contract, + bets, + userBets, + comments, + answerResponse, + onCancelAnswerResponse, + } = props const yourTrades = (
@@ -56,7 +67,14 @@ export function ContractTabs(props: { const tabs = buildArray( { title: 'Comments', - content: , + content: ( + + ), }, bets.length > 0 && { title: capitalize(PAST_BETS), @@ -76,8 +94,10 @@ export function ContractTabs(props: { const CommentsTabContent = memo(function CommentsTabContent(props: { contract: Contract comments: ContractComment[] + answerResponse?: Answer + onCancelAnswerResponse?: () => void }) { - const { contract } = props + const { contract, answerResponse, onCancelAnswerResponse } = props const tips = useTipTxns({ contractId: contract.id }) const comments = useComments(contract.id) ?? props.comments const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Newest', { @@ -95,10 +115,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { // replied to answers/comments are NOT newest, otherwise newest first const shouldBeNewestFirst = (c: ContractComment) => - c.replyToCommentId == undefined && - (contract.outcomeType === 'FREE_RESPONSE' - ? c.betId === undefined && c.answerOutcome == undefined - : true) + c.replyToCommentId == undefined // TODO: links to comments are broken because tips load after render and // comments will reorganize themselves if there are tips/bounties awarded @@ -123,73 +140,85 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { const topLevelComments = commentsByParent['_'] ?? [] const sortRow = comments.length > 0 && ( - - - + + +
Sort by:
+ +
) - if (contract.outcomeType === 'FREE_RESPONSE') { - const sortedAnswers = sortBy( - contract.answers, - (a) => -getOutcomeProbability(contract, a.id) - ) - const commentsByOutcome = groupBy( - sortedComments, - (c) => c.answerOutcome ?? c.betOutcome ?? '_' - ) - const generalTopLevelComments = topLevelComments.filter( - (c) => c.answerOutcome === undefined && c.betId === undefined - ) - return ( <> + {sortRow} - {sortedAnswers.map((answer) => ( -
-
- ))} - -
General Comments
-
- - {sortRow} - - {generalTopLevelComments.map((comment) => ( - - ))} - + {answerResponse && ( + + )} + {topLevelComments.map((parent) => { + if (parent.answerOutcome === undefined) { + return ( + c.createdTime + )} + tips={tips} + /> + ) + } + const answer = contract.answers.find( + (answer) => answer.id === parent.answerOutcome + ) + if (answer === undefined) { + console.error('Could not find answer that matches ID') + return <> + } + return ( + <> + + + + +
+ +
+
+ c.createdTime + )} + tips={tips} + /> +
+
+ + ) + })} ) } else { diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx index b974938f..df245c68 100644 --- a/web/components/contract/tip-button.tsx +++ b/web/components/contract/tip-button.tsx @@ -44,7 +44,7 @@ export function TipButton(props: { 0 ? 'mr-2' : '', userTipped ? 'fill-teal-500 text-teal-500' : '' )} diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index d4401b8c..6b6b911a 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -33,7 +33,7 @@ export function CopyLinkDateTimeComponent(props: {
{fromNow(createdTime)} diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 11bc6139..e1b470a7 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,46 +1,21 @@ import { Answer } from 'common/answer' -import { FreeResponseContract } from 'common/contract' -import { ContractComment } from 'common/comment' -import React, { useEffect, useRef, useState } from 'react' -import { sum } from 'lodash' +import { Contract } from 'common/contract' +import React, { useEffect, useRef } from 'react' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' -import { Linkify } from 'web/components/linkify' -import clsx from 'clsx' -import { - ContractCommentInput, - FeedComment, - ReplyTo, -} from 'web/components/feed/feed-comments' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { useRouter } from 'next/router' -import { useUser } from 'web/hooks/use-user' -import { useEvent } from 'web/hooks/use-event' -import { CommentTipMap } from 'web/hooks/use-tip-txns' import { UserLink } from 'web/components/user-link' -export function FeedAnswerCommentGroup(props: { - contract: FreeResponseContract - answer: Answer - answerComments: ContractComment[] - tips: CommentTipMap -}) { - const { answer, contract, answerComments, tips } = props +export function CommentsAnswer(props: { answer: Answer; contract: Contract }) { + const { answer, contract } = props const { username, avatarUrl, name, text } = answer - - const [replyTo, setReplyTo] = useState() - const user = useUser() - const router = useRouter() const answerElementId = `answer-${answer.id}` + const router = useRouter() const highlighted = router.asPath.endsWith(`#${answerElementId}`) const answerRef = useRef(null) - const onSubmitComment = useEvent(() => setReplyTo(undefined)) - const onReplyClick = useEvent((comment: ContractComment) => { - setReplyTo({ id: comment.id, username: comment.userUsername }) - }) - useEffect(() => { if (highlighted && answerRef.current != null) { answerRef.current.scrollIntoView(true) @@ -48,83 +23,20 @@ export function FeedAnswerCommentGroup(props: { }, [highlighted]) return ( - - - - - -
- answered - -
- - - - - -
- -
- -
- -
- -
- - {answerComments.map((comment) => ( - - ))} - - {replyTo && ( -
-