From 49541d3eec310a168d894e4fd26736b47f30f26c Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 9 Aug 2022 10:08:14 -0700 Subject: [PATCH 01/22] Stop interpolating on portfolio value graph --- web/components/portfolio/portfolio-value-graph.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index 50a6b59a..d1dae0bd 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -61,7 +61,8 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { min: Math.min(...points.map((p) => p.y)), }} gridYValues={numYTickValues} - curve="monotoneX" + curve="stepAfter" + enablePoints={false} colors={{ datum: 'color' }} axisBottom={{ tickValues: numXTickValues, From 914fc476ce3e00a06af3d387dc18e7e8a704c3ad Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Tue, 9 Aug 2022 10:17:44 -0700 Subject: [PATCH 02/22] Remove top/bottom margin from indented list items (#733) --- web/components/editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 2371bbf8..3bee8298 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -32,7 +32,7 @@ import { Row } from './layout/row' import { Spacer } from './layout/spacer' const proseClass = clsx( - 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', + 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'font-light prose-a:font-light prose-blockquote:font-light' ) From 1e3c5cb9369efa2163857c7a657d1896146f7c29 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Tue, 9 Aug 2022 12:27:52 -0500 Subject: [PATCH 03/22] add qr code to referrals --- web/pages/referrals.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx index b2666309..c30418cf 100644 --- a/web/pages/referrals.tsx +++ b/web/pages/referrals.tsx @@ -9,6 +9,7 @@ import { REFERRAL_AMOUNT } from 'common/user' import { CopyLinkButton } from 'web/components/copy-link-button' import { ENV_CONFIG } from 'common/envs/constants' import { InfoBox } from 'web/components/info-box' +import { QRCode } from 'web/components/qr-code' export const getServerSideProps = redirectIfLoggedOut('/') @@ -50,6 +51,8 @@ export default function ReferralsPage() { toastClassName={'-left-28 mt-1'} /> + + Date: Tue, 9 Aug 2022 13:25:42 -0700 Subject: [PATCH 04/22] Random contract page fixups (#712) * Remove some divs and so on * Correctly align bet avatars and text in feed * Extract sidebar component on contract page --- web/components/avatar.tsx | 13 +++-- web/components/feed/feed-bets.tsx | 64 +++++++++++----------- web/pages/[username]/[contractSlug].tsx | 71 ++++++++++++++----------- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 0436d61c..19b6066e 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -47,14 +47,21 @@ export function Avatar(props: { ) } -export function EmptyAvatar(props: { size?: number; multi?: boolean }) { - const { size = 8, multi } = props +export function EmptyAvatar(props: { + className?: string + size?: number + multi?: boolean +}) { + const { className, size = 8, multi } = props const insize = size - 3 const Icon = multi ? UsersIcon : UserIcon return (
diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 29645136..ffa53de3 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -36,38 +36,33 @@ export function FeedBet(props: { const isSelf = user?.id === userId return ( - <> - - {isSelf ? ( - - ) : bettor ? ( - - ) : ( -
- -
- )} -
- -
-
- + + {isSelf ? ( + + ) : bettor ? ( + + ) : ( + + )} + + ) } @@ -77,8 +72,9 @@ export function BetStatusText(props: { isSelf: boolean bettor?: User hideOutcome?: boolean + className?: string }) { - const { bet, contract, bettor, isSelf, hideOutcome } = props + const { bet, contract, bettor, isSelf, hideOutcome, className } = props const { outcomeType } = contract const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isFreeResponse = outcomeType === 'FREE_RESPONSE' @@ -123,7 +119,7 @@ export function BetStatusText(props: { : formatPercent(bet.limitProb ?? bet.probAfter) return ( -
+
{bettor ? ( ) : ( diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 5866f899..8d12e9c0 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -106,6 +106,43 @@ export default function ContractPage(props: { return } +export function ContractPageSidebar(props: { + user: User | null | undefined + contract: Contract +}) { + const { contract, user } = props + const { creatorId, isResolved, outcomeType } = contract + + const isCreator = user?.id === creatorId + const isBinary = outcomeType === 'BINARY' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const isNumeric = outcomeType === 'NUMERIC' + const allowTrade = tradingAllowed(contract) + const allowResolve = !isResolved && isCreator && !!user + const hasSidePanel = + (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) + + return hasSidePanel ? ( + + {allowTrade && + (isNumeric ? ( + + ) : ( + + ))} + {allowResolve && + (isNumeric || isPseudoNumeric ? ( + + ) : ( + + ))} + + ) : null +} + export function ContractPageContent( props: Parameters[0] & { contract: Contract } ) { @@ -142,16 +179,9 @@ export function ContractPageContent( setShowConfetti(shouldSeeConfetti) }, [contract, user]) - const { creatorId, isResolved, question, outcomeType } = contract + const { isResolved, question, outcomeType } = contract - const isCreator = user?.id === creatorId - const isBinary = outcomeType === 'BINARY' - const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const isNumeric = outcomeType === 'NUMERIC' const allowTrade = tradingAllowed(contract) - const allowResolve = !isResolved && isCreator && !!user - const hasSidePanel = - (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) const ogCardProps = getOpenGraphProps(contract) @@ -160,26 +190,7 @@ export function ContractPageContent( contractId: contract.id, }) - const rightSidebar = hasSidePanel ? ( - - {allowTrade && - (isNumeric ? ( - - ) : ( - - ))} - {allowResolve && - (isNumeric || isPseudoNumeric ? ( - - ) : ( - - ))} - - ) : null - + const rightSidebar = return ( {showConfetti && ( @@ -216,7 +227,7 @@ export function ContractPageContent( bets={bets.filter((b) => !b.challengeSlug)} /> - {isNumeric && ( + {outcomeType === 'NUMERIC' && ( )} - {isNumeric && allowTrade && ( + {outcomeType === 'NUMERIC' && allowTrade && ( )} From 847d3d0f2770e0da440e23848d5a4c9c3d6e921a Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 9 Aug 2022 15:28:27 -0700 Subject: [PATCH 05/22] Fix efficiency problems with visibility checking code (#730) * Fix problems with visibility checking code * Tear out old contract tracking stuff per James * Use `useEvent` in VisibilityObserver per James suggestion --- web/components/contract-search.tsx | 1 - web/components/contract/contracts-grid.tsx | 30 +++++++------- web/components/feed/feed-items.tsx | 8 +--- web/components/landing-page-panel.tsx | 6 +-- web/components/visibility-observer.tsx | 24 +++++++++++ web/hooks/use-is-visible.ts | 28 ------------- web/hooks/use-seen-contracts.ts | 47 ---------------------- web/pages/contract-search-firestore.tsx | 7 +--- 8 files changed, 43 insertions(+), 108 deletions(-) create mode 100644 web/components/visibility-observer.tsx delete mode 100644 web/hooks/use-is-visible.ts delete mode 100644 web/hooks/use-seen-contracts.ts diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 265b25c6..8bc1341f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -345,7 +345,6 @@ export function ContractSearch(props: { void - hasMore: boolean + loadMore?: () => void showTime?: ShowTime onContractClick?: (contract: Contract) => void overrideGridClassName?: string @@ -31,7 +30,6 @@ export function ContractsGrid(props: { const { contracts, showTime, - hasMore, loadMore, onContractClick, overrideGridClassName, @@ -39,16 +37,15 @@ export function ContractsGrid(props: { highlightOptions, } = props const { hideQuickBet, hideGroupLink } = cardHideOptions || {} - const { contractIds, highlightClassName } = highlightOptions || {} - const [elem, setElem] = useState(null) - const isBottomVisible = useIsVisible(elem) - - useEffect(() => { - if (isBottomVisible && hasMore) { - loadMore() - } - }, [isBottomVisible, hasMore, loadMore]) + const onVisibilityUpdated = useCallback( + (visible) => { + if (visible && loadMore) { + loadMore() + } + }, + [loadMore] + ) if (contracts === undefined) { return @@ -92,7 +89,10 @@ export function ContractsGrid(props: { /> ))} -
+ ) } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index b1cd765c..d60fb8da 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -1,5 +1,5 @@ // From https://tailwindui.com/components/application-ui/lists/feeds -import React, { useState } from 'react' +import React from 'react' import { BanIcon, CheckIcon, @@ -22,7 +22,6 @@ import { UserLink } from '../user-page' import BetRow from '../bet-row' import { Avatar } from '../avatar' import { ActivityItem } from './activity-items' -import { useSaveSeenContract } from 'web/hooks/use-seen-contracts' import { useUser } from 'web/hooks/use-user' import { trackClick } from 'web/lib/firebase/tracking' import { DAY_MS } from 'common/util/time' @@ -50,11 +49,8 @@ export function FeedItems(props: { const { contract, items, className, betRowClassName, user } = props const { outcomeType } = contract - const [elem, setElem] = useState(null) - useSaveSeenContract(elem, contract) - return ( -
+
{items.map((item, activityItemIdx) => (
diff --git a/web/components/landing-page-panel.tsx b/web/components/landing-page-panel.tsx index 4b436442..2e3d85e2 100644 --- a/web/components/landing-page-panel.tsx +++ b/web/components/landing-page-panel.tsx @@ -59,11 +59,7 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
- {}} - hasMore={false} - showTime={showTime} - /> +
) } From c07daafb8dc0b36484c0cfc815d517d757845415 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 9 Aug 2022 15:28:52 -0700 Subject: [PATCH 06/22] Make homepage load user via SSR, pass it to contract stuff (#729) --- web/components/contract-search.tsx | 5 +++-- web/components/contract/contracts-grid.tsx | 8 ++++++-- web/components/user-page.tsx | 4 +++- web/pages/[username]/[contractSlug].tsx | 12 +++++++----- web/pages/group/[...slugs]/index.tsx | 2 ++ web/pages/home.tsx | 12 ++++++++++-- web/pages/markets.tsx | 4 +++- web/pages/tag/[tag].tsx | 3 +++ 8 files changed, 37 insertions(+), 13 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 8bc1341f..f3bdbd6a 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -2,6 +2,7 @@ import algoliasearch from 'algoliasearch/lite' import { Contract } from 'common/contract' +import { User } from 'common/user' import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, @@ -11,7 +12,6 @@ import { Row } from './layout/row' import { useEffect, useMemo, useState } from 'react' import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' -import { useUser } from 'web/hooks/use-user' import { useFollows } from 'web/hooks/use-follows' import { track, trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' @@ -45,6 +45,7 @@ export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' export function ContractSearch(props: { + user: User | null | undefined querySortOptions?: { defaultSort: Sort defaultFilter?: filter @@ -67,6 +68,7 @@ export function ContractSearch(props: { } }) { const { + user, querySortOptions, additionalFilter, onContractClick, @@ -77,7 +79,6 @@ export function ContractSearch(props: { highlightOptions, } = props - const user = useUser() const memberGroups = (useMemberGroups(user?.id) ?? []).filter( (group) => !NEW_USER_GROUP_SLUGS.includes(group.slug) ) diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 77269ea3..f62c3c85 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -97,11 +97,15 @@ export function ContractsGrid(props: { ) } -export function CreatorContractsList(props: { creator: User }) { - const { creator } = props +export function CreatorContractsList(props: { + user: User | null | undefined + creator: User +}) { + const { user, creator } = props return ( , + content: ( + + ), tabIcon: ( {usersContracts.length} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 8d12e9c0..c35f5d98 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -92,6 +92,7 @@ export default function ContractPage(props: { slug: '', } + const user = useUser() const inIframe = useIsIframe() if (inIframe) { return @@ -103,7 +104,7 @@ export default function ContractPage(props: { return } - return + return } export function ContractPageSidebar(props: { @@ -144,9 +145,12 @@ export function ContractPageSidebar(props: { } export function ContractPageContent( - props: Parameters[0] & { contract: Contract } + props: Parameters[0] & { + contract: Contract + user?: User | null + } ) { - const { backToHome, comments } = props + const { backToHome, comments, user } = props const contract = useContractWithPreload(props.contract) ?? props.contract @@ -164,8 +168,6 @@ export function ContractPageContent( const tips = useTipTxns({ contractId: contract.id }) - const user = useUser() - const { width, height } = useWindowSize() const [showConfetti, setShowConfetti] = useState(false) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index b96d6436..8e7ec19d 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -201,6 +201,7 @@ export default function GroupPage(props: { const questionsTab = ( { + const user = await getUser(creds.user.uid) + return { props: { user } } +}) -const Home = () => { +const Home = (props: { user: User }) => { + const { user } = props const [contract, setContract] = useContractPage() const router = useRouter() @@ -29,6 +35,7 @@ const Home = () => { { {contract && ( - + ) } diff --git a/web/pages/tag/[tag].tsx b/web/pages/tag/[tag].tsx index 476afecf..c1dce29e 100644 --- a/web/pages/tag/[tag].tsx +++ b/web/pages/tag/[tag].tsx @@ -1,10 +1,12 @@ import { useRouter } from 'next/router' +import { useUser } from 'web/hooks/use-user' import { ContractSearch } from '../../components/contract-search' import { Page } from '../../components/page' import { Title } from '../../components/title' export default function TagPage() { const router = useRouter() + const user = useUser() const { tag } = router.query as { tag: string } if (!router.isReady) return
@@ -12,6 +14,7 @@ export default function TagPage() { <ContractSearch + user={user} querySortOptions={{ defaultSort: 'newest', defaultFilter: 'all', From 0b9ca6b7ee15a58adab8a1cac7967f2ee05d6eda Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Tue, 9 Aug 2022 19:04:55 -0700 Subject: [PATCH 07/22] Editor improvements (#735) * Allow focus on all parts of editor * Fix background and text colors * Restrict height of image in comment * Remove "Type *markdown*" placeholder it's a little misleading (can't do markdown links) and messes with focus to be replaced with a highlight menu in the future --- web/components/comments-list.tsx | 2 +- web/components/editor.tsx | 64 ++++++++++++++++----------- web/components/feed/feed-comments.tsx | 6 +-- web/components/groups/group-chat.tsx | 2 +- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index 2a467f6d..de4ea67f 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -65,7 +65,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) { />{' '} <RelativeTimestamp time={createdTime} /> </p> - <Content content={content || text} /> + <Content content={content || text} smallImage /> </div> </Row> ) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 3bee8298..cef1aa36 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -3,7 +3,6 @@ import Placeholder from '@tiptap/extension-placeholder' import { useEditor, EditorContent, - FloatingMenu, JSONContent, Content, Editor, @@ -11,13 +10,11 @@ import { import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' -import { Mention } from '@tiptap/extension-mention' import clsx from 'clsx' import { useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' -import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import { useUsers } from 'web/hooks/use-users' @@ -31,6 +28,18 @@ import { Button } from './button' import { Row } from './layout/row' import { Spacer } from './layout/spacer' +const DisplayImage = Image.configure({ + HTMLAttributes: { + class: 'max-h-60', + }, +}) + +const DisplayLink = Link.configure({ + HTMLAttributes: { + class: clsx('no-underline !text-indigo-700', linkClass), + }, +}) + const proseClass = clsx( 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'font-light prose-a:font-light prose-blockquote:font-light' @@ -64,15 +73,11 @@ export function useTextEditor(props: { Placeholder.configure({ placeholder, emptyEditorClass: - 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', + 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text', }), CharacterCount.configure({ limit: max }), - Image, - Link.configure({ - HTMLAttributes: { - class: clsx('no-underline !text-indigo-700', linkClass), - }, - }), + simple ? DisplayImage : Image, + DisplayLink, DisplayMention.configure({ suggestion: mentionSuggestion(users), }), @@ -132,15 +137,7 @@ export function TextEditor(props: { <> {/* hide placeholder when focused */} <div className="relative w-full [&:focus-within_p.is-empty]:before:content-none"> - {editor && ( - <FloatingMenu - editor={editor} - className={clsx(proseClass, '-ml-2 mr-2 w-full text-slate-300 ')} - > - Type <em>*markdown*</em> - </FloatingMenu> - )} - <div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <div className="rounded-lg border border-gray-300 bg-white shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> <EditorContent editor={editor} /> {/* Toolbar, with buttons for images and embeds */} <div className="flex h-9 items-center gap-5 pl-4 pr-1"> @@ -168,7 +165,14 @@ export function TextEditor(props: { <span className="sr-only">Embed an iframe</span> </button> </div> - <div className="ml-auto" /> + {/* Spacer that also focuses editor on click */} + <div + className="grow cursor-text self-stretch" + onMouseDown={() => + editor?.chain().focus('end').createParagraphNear().run() + } + aria-hidden + /> {children} </div> </div> @@ -258,14 +262,19 @@ const useUploadMutation = (editor: Editor | null) => } ) -function RichContent(props: { content: JSONContent | string }) { - const { content } = props +function RichContent(props: { + content: JSONContent | string + smallImage?: boolean +}) { + const { content, smallImage } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, extensions: [ - // replace tiptap's Mention with ours, to add style and link - ...exhibitExts.filter((ex) => ex.name !== Mention.name), + StarterKit, + smallImage ? DisplayImage : Image, + DisplayLink, DisplayMention, + Iframe, ], content, editable: false, @@ -276,13 +285,16 @@ function RichContent(props: { content: JSONContent | string }) { } // backwards compatibility: we used to store content as strings -export function Content(props: { content: JSONContent | string }) { +export function Content(props: { + content: JSONContent | string + smallImage?: boolean +}) { const { content } = props return typeof content === 'string' ? ( <div className="whitespace-pre-line font-light leading-relaxed"> <Linkify text={content} /> </div> ) : ( - <RichContent content={content} /> + <RichContent {...props} /> ) } diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 8c84039e..d4ba98b6 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -254,7 +254,7 @@ export function FeedComment(props: { /> </div> <div className="mt-2 text-[15px] text-gray-700"> - <Content content={content || text} /> + <Content content={content || text} smallImage /> </div> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> @@ -394,8 +394,8 @@ export function CommentInput(props: { /> </div> <div className={'min-w-0 flex-1'}> - <div className="pl-0.5 text-sm text-gray-500"> - <div className={'mb-1'}> + <div className="pl-0.5 text-sm"> + <div className="mb-1 text-gray-500"> {mostRecentCommentableBet && ( <BetStatusText contract={contract} diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2d25351a..d872c980 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -338,7 +338,7 @@ const GroupMessage = memo(function GroupMessage_(props: { </Row> <div className="mt-2 text-black"> {comments.map((comment) => ( - <Content content={comment.content || comment.text} /> + <Content content={comment.content || comment.text} smallImage /> ))} </div> <Row> From 63538ae925cd46057df5b0392cf4841442813396 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 9 Aug 2022 21:51:01 -0500 Subject: [PATCH 08/22] referral link on user page, manalinks, market share dialog; native sharer on mobile --- web/components/contract/share-modal.tsx | 44 ++++++++++++++++--------- web/components/share-icon-button.tsx | 4 +-- web/components/user-page.tsx | 6 ++-- web/pages/links.tsx | 9 +++-- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 017d3174..6651db13 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -14,7 +14,9 @@ import { Button } from '../button' import { copyToClipboard } from 'web/lib/util/copy' import { track } from 'web/lib/service/analytics' import { ENV_CONFIG } from 'common/envs/constants' -import { User } from 'common/user' +import { REFERRAL_AMOUNT, User } from 'common/user' +import { SiteLink } from '../site-link' +import { formatMoney } from 'common/util/format' export function ShareModal(props: { contract: Contract @@ -26,36 +28,50 @@ export function ShareModal(props: { const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" /> - const copyPayload = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ + const shareUrl = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ user?.username && contract.creatorUsername !== user?.username ? '?referrer=' + user?.username : '' }` return ( - <Modal open={isOpen} setOpen={setOpen}> + <Modal open={isOpen} setOpen={setOpen} size="md"> <Col className="gap-4 rounded bg-white p-4"> - <Title className="!mt-0 mb-2" text="Share this market" /> + <Title className="!mt-0 !mb-2" text="Share this market" /> + <p> + Earn{' '} + <SiteLink href="/referrals"> + {formatMoney(REFERRAL_AMOUNT)} referral bonus + </SiteLink>{' '} + if a new user signs up using the link! + </p> <Button size="2xl" color="gradient" className={'mb-2 flex max-w-xs self-center'} onClick={() => { - copyToClipboard(copyPayload) + if (window.navigator.share) { + window.navigator.share({ + url: shareUrl, + title: contract.question, + }) + } else { + copyToClipboard(shareUrl) + toast.success('Link copied!', { + icon: linkIcon, + }) + } track('copy share link') - toast.success('Link copied!', { - icon: linkIcon, - }) }} > - {linkIcon} Copy link + {!!window.navigator.share ? 'Share' : <>{linkIcon} Copy link</>} </Button> - <Row className="justify-start gap-4 self-center"> + <Row className="z-0 justify-start gap-4 self-center"> <TweetButton className="self-start" - tweetText={getTweetText(contract)} + tweetText={getTweetText(contract, shareUrl)} /> <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> <DuplicateContractButton contract={contract} /> @@ -65,13 +81,9 @@ export function ShareModal(props: { ) } -const getTweetText = (contract: Contract) => { +const getTweetText = (contract: Contract, url: string) => { const { question, resolution } = contract - const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' - const timeParam = `${Date.now()}`.substring(7) - const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` - return `${question}\n\n${url}${tweetDescription}` } diff --git a/web/components/share-icon-button.tsx b/web/components/share-icon-button.tsx index 4db192a9..da1fc570 100644 --- a/web/components/share-icon-button.tsx +++ b/web/components/share-icon-button.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { ShareIcon } from '@heroicons/react/outline' +import { LinkIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { copyToClipboard } from 'web/lib/util/copy' @@ -40,7 +40,7 @@ export function ShareIconButton(props: { setTimeout(() => setShowToast(false), 2000) }} > - <ShareIcon + <LinkIcon className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} aria-hidden="true" /> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index cd220cab..5415ed39 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -280,8 +280,10 @@ export function UserPage(props: { user: User; currentUser?: User }) { } > <span> - Refer a friend and earn {formatMoney(500)} when they sign up! You - have <ReferralsButton user={user} currentUser={currentUser} /> + <SiteLink href="/referrals"> + Refer a friend and earn {formatMoney(500)} when they sign up! + </SiteLink>{' '} + You have <ReferralsButton user={user} currentUser={currentUser} /> </span> <ShareIconButton copyPayload={`https://${ENV_CONFIG.domain}?referrer=${currentUser.username}`} diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 55939b19..258c782a 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -26,6 +26,7 @@ import { ManalinkCardFromView } from 'web/components/manalink-card' import { Pagination } from 'web/components/pagination' import { Manalink } from 'common/manalink' import { REFERRAL_AMOUNT } from 'common/user' +import { SiteLink } from 'web/components/site-link' const LINKS_PER_PAGE = 24 @@ -69,9 +70,11 @@ export default function LinkPage(props: { user: User }) { </Row> <p> You can use manalinks to send mana (M$) to other people, even if they - don't yet have a Manifold account. Manalinks are also eligible - for the referral bonus. Invite a new user to Manifold and get M$ - {REFERRAL_AMOUNT} if they sign up! + don't yet have a Manifold account.{' '} + <SiteLink href="/referrals"> + Eligible for {formatMoney(REFERRAL_AMOUNT)} referral bonus if a new + user signs up! + </SiteLink> </p> <Subtitle text="Your Manalinks" /> <ManalinksDisplay From 5f77a026aa1d4e30d6e5c8b2c49b8bdb0a8d7006 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 9 Aug 2022 21:59:40 -0500 Subject: [PATCH 09/22] fix modal --- web/components/contract/share-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 6651db13..c462e78b 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -65,7 +65,7 @@ export function ShareModal(props: { track('copy share link') }} > - {!!window.navigator.share ? 'Share' : <>{linkIcon} Copy link</>} + {linkIcon} Copy link </Button> <Row className="z-0 justify-start gap-4 self-center"> From 818c90a95ece1b40e7316603322ca11ef118450f Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 23:05:56 -0700 Subject: [PATCH 10/22] Refactor tipper (#734) * Clean up tipping components * Pass comment into tip callback --- web/components/tipper.tsx | 70 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 68ca5308..1c76c3e7 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -37,7 +37,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { // declare debounced function only on first render const [saveTip] = useState(() => - debounce(async (user: User, change: number) => { + debounce(async (user: User, comment: Comment, change: number) => { if (change === 0) { return } @@ -71,30 +71,24 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { // instant save on unrender useEffect(() => () => void saveTip.flush(), [saveTip]) - const changeTip = (tip: number) => { - setLocalTip(tip) - me && saveTip(me, tip - savedTip) + const addTip = (delta: number) => { + setLocalTip(localTip + delta) + me && saveTip(me, comment, localTip - savedTip + delta) } + const canDown = me && localTip > savedTip + const canUp = me && me.id !== comment.userId && me.balance >= localTip + 5 return ( <Row className="items-center gap-0.5"> - <DownTip - value={localTip} - onChange={changeTip} - disabled={!me || localTip <= savedTip} - /> + <DownTip onClick={canDown ? () => addTip(-5) : undefined} /> <span className="font-bold">{Math.floor(total)}</span> - <UpTip - value={localTip} - onChange={changeTip} - disabled={!me || me.id === comment.userId || me.balance < localTip + 5} - /> + <UpTip onClick={canUp ? () => addTip(+5) : undefined} value={localTip} /> {localTip === 0 ? ( '' ) : ( <span className={clsx( - 'font-semibold', + 'ml-1 font-semibold', localTip > 0 ? 'text-primary' : 'text-red-400' )} > @@ -105,21 +99,17 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { ) } -function DownTip(prop: { - value: number - onChange: (tip: number) => void - disabled?: boolean -}) { - const { onChange, value, disabled } = prop +function DownTip(props: { onClick?: () => void }) { + const { onClick } = props return ( <Tooltip - className="tooltip-bottom" - text={!disabled && `-${formatMoney(5)}`} + className="tooltip-bottom h-6 w-6" + text={onClick && `-${formatMoney(5)}`} > <button - className="flex h-max items-center hover:text-red-600 disabled:text-gray-300" - disabled={disabled} - onClick={() => onChange(value - 5)} + className="hover:text-red-600 disabled:text-gray-300" + disabled={!onClick} + onClick={onClick} > <ChevronLeftIcon className="h-6 w-6" /> </button> @@ -127,30 +117,20 @@ function DownTip(prop: { ) } -function UpTip(prop: { - value: number - onChange: (tip: number) => void - disabled?: boolean -}) { - const { onChange, value, disabled } = prop - +function UpTip(props: { onClick?: () => void; value: number }) { + const { onClick, value } = props + const IconKind = value >= 10 ? ChevronDoubleRightIcon : ChevronRightIcon return ( <Tooltip - className="tooltip-bottom" - text={!disabled && `Tip ${formatMoney(5)}`} + className="tooltip-bottom h-6 w-6" + text={onClick && `Tip ${formatMoney(5)}`} > <button - className="hover:text-primary flex h-max items-center disabled:text-gray-300" - disabled={disabled} - onClick={() => onChange(value + 5)} + className="hover:text-primary disabled:text-gray-300" + disabled={!onClick} + onClick={onClick} > - {value >= 10 ? ( - <ChevronDoubleRightIcon className="text-primary mx-1 h-6 w-6" /> - ) : value > 0 ? ( - <ChevronRightIcon className="text-primary h-6 w-6" /> - ) : ( - <ChevronRightIcon className="h-6 w-6" /> - )} + <IconKind className={clsx('h-6 w-6', value ? 'text-primary' : '')} /> </button> </Tooltip> ) From 521c479abfdd5c22a7b762bf7f6e852341668c56 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 23:55:41 -0700 Subject: [PATCH 11/22] Fix an embarrassing bug in `getPrivateUser` --- web/lib/firebase/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 3096f00f..cf2415ab 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -54,7 +54,7 @@ export async function getUser(userId: string) { export async function getPrivateUser(userId: string) { /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - return (await getDoc(doc(users, userId))).data()! + return (await getDoc(doc(privateUsers, userId))).data()! } export async function getUserByUsername(username: string) { From 52a2a3d8421d9ac5128a54a5881b19bdd795be3d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 11:50:21 -0500 Subject: [PATCH 12/22] track search --- web/components/contract-search.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index f3bdbd6a..30be1f6e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -236,7 +236,7 @@ export function ContractSearch(props: { if (newFilter === filter) return setFilter(newFilter) setPage(0) - trackCallback('select search filter', { filter: newFilter }) + track('select search filter', { filter: newFilter }) } const selectSort = (newSort: Sort) => { @@ -244,7 +244,7 @@ export function ContractSearch(props: { setPage(0) setSort(newSort) - track('select sort', { sort: newSort }) + track('select search sort', { sort: newSort }) } if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { @@ -263,6 +263,7 @@ export function ContractSearch(props: { type="text" value={query} onChange={(e) => updateQuery(e.target.value)} + onBlur={trackCallback('search', { query })} placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} className="input input-bordered w-full" /> From 05c9d3513a1759f31f8a6df5645a41aa8381705c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:05:52 -0500 Subject: [PATCH 13/22] Don't reference window outside useEffect or click event. --- web/components/nav/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 713bc575..8879c052 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -329,7 +329,7 @@ function GroupsList(props: { const { height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) const remainingHeight = - (height ?? window.innerHeight) - (containerRef?.offsetTop ?? 0) + (height ?? 0) - (containerRef?.offsetTop ?? 0) const notifIsForThisItem = useMemo( () => (itemHref: string) => From 3d30a1adbc8287b1578a64fbeff2143fc6b25ccf Mon Sep 17 00:00:00 2001 From: jahooma <jahooma@users.noreply.github.com> Date: Wed, 10 Aug 2022 17:06:34 +0000 Subject: [PATCH 14/22] Auto-prettification --- web/components/nav/sidebar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 8879c052..d45fdf19 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -328,8 +328,7 @@ function GroupsList(props: { const { height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) - const remainingHeight = - (height ?? 0) - (containerRef?.offsetTop ?? 0) + const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) const notifIsForThisItem = useMemo( () => (itemHref: string) => From dc26db28642fb04d436148f62ba580c8f2807df6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 12:17:33 -0500 Subject: [PATCH 15/22] add salem to sidebar; clean up code --- common/util/array.ts | 16 +++++++ web/components/nav/sidebar.tsx | 87 +++++++++++++++------------------- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/common/util/array.ts b/common/util/array.ts index d81edba1..2ad86843 100644 --- a/common/util/array.ts +++ b/common/util/array.ts @@ -1,3 +1,19 @@ export function filterDefined<T>(array: (T | null | undefined)[]) { return array.filter((item) => item !== null && item !== undefined) as T[] } + +export function buildArray<T>( + ...params: (T | T[] | false | undefined | null)[] +) { + const array: T[] = [] + + for (const el of params) { + if (Array.isArray(el)) { + array.push(...el) + } else if (el) { + array.push(el) + } + } + + return array +} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index d45fdf19..3a3c932f 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -30,6 +30,7 @@ import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { PrivateUser } from 'common/user' import { useWindowSize } from 'web/hooks/use-window-size' import { CHALLENGES_ENABLED } from 'common/challenge' +import { buildArray } from 'common/util/array' const logout = async () => { // log out, and then reload the page, in case SSR wants to boot them out @@ -61,42 +62,31 @@ function getMoreNavigation(user?: User | null) { } if (!user) { - if (CHALLENGES_ENABLED) - return [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Charity', href: '/charity' }, - { name: 'Blog', href: 'https://news.manifold.markets' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, - ] - else - return [ + return buildArray( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ { name: 'Charity', href: '/charity' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, { name: 'Blog', href: 'https://news.manifold.markets' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, ] + ) } - if (CHALLENGES_ENABLED) - return [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, - { - name: 'Sign out', - href: '#', - onClick: logout, - }, - ] - else - return [ + return buildArray( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, { @@ -105,6 +95,7 @@ function getMoreNavigation(user?: User | null) { onClick: logout, }, ] + ) } const signedOutNavigation = [ @@ -141,29 +132,27 @@ const signedInMobileNavigation = [ ] function getMoreMobileNav() { - return [ - ...(IS_PRIVATE_MANIFOLD - ? [] - : CHALLENGES_ENABLED - ? [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - ] - : [ - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - ]), - { - name: 'Sign out', - href: '#', - onClick: logout, - }, - ] + const signOut = { + name: 'Sign out', + href: '#', + onClick: logout, + } + if (IS_PRIVATE_MANIFOLD) return [signOut] + + return buildArray<Item>( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ + { name: 'Referrals', href: '/referrals' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, + { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ], + signOut + ) } export type Item = { From 4d953d58a1c755e44539a8fa0af325461b5da68f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:27:59 -0500 Subject: [PATCH 16/22] Move group chat back into a tab --- web/pages/group/[...slugs]/index.tsx | 30 +++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 8e7ec19d..b9c8c277 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -16,7 +16,7 @@ import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' -import { usePrivateUser, useUser } from 'web/hooks/use-user' +import { useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' @@ -30,7 +30,6 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { CreateQuestionButton } from 'web/components/create-question-button' import React, { useState } from 'react' -import { GroupChatInBubble } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' @@ -51,6 +50,7 @@ import { useSaveReferral } from 'web/hooks/use-save-referral' import { Button } from 'web/components/button' import { listAllCommentsOnGroup } from 'web/lib/firebase/comments' import { Comment } from 'common/comment' +import { GroupChat } from 'web/components/groups/group-chat' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -157,16 +157,12 @@ export default function GroupPage(props: { const messages = useCommentsOnGroup(group?.id) ?? props.messages const user = useUser() - const privateUser = usePrivateUser(user?.id) useSaveReferral(user, { defaultReferrerUsername: creator.username, groupId: group?.id, }) - const chatDisabled = !group || group.chatDisabled - const showChatBubble = !chatDisabled - if (group === null || !groupSubpages.includes(page) || slugs[2]) { return <Custom404 /> } @@ -211,12 +207,21 @@ export default function GroupPage(props: { /> ) + const chatTab = ( + <GroupChat messages={messages} group={group} user={user} tips={tips} /> + ) + const tabs = [ { title: 'Markets', content: questionsTab, href: groupPath(group.slug, 'markets'), }, + { + title: 'Chat', + content: chatTab, + href: groupPath(group.slug, 'chat'), + }, { title: 'Leaderboards', content: leaderboard, @@ -229,7 +234,9 @@ export default function GroupPage(props: { }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) + const tabIndex = tabs + .map((t) => t.title.toLowerCase()) + .indexOf(page ?? 'markets') return ( <Page> @@ -265,15 +272,6 @@ export default function GroupPage(props: { defaultIndex={tabIndex > 0 ? tabIndex : 0} tabs={tabs} /> - {showChatBubble && ( - <GroupChatInBubble - group={group} - user={user} - privateUser={privateUser} - tips={tips} - messages={messages} - /> - )} </Page> ) } From e591de8b29530396bd2a62b3873f7a3aa2dc8b15 Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Thu, 11 Aug 2022 02:31:28 +0900 Subject: [PATCH 17/22] Increase description max length (#739) --- common/contract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/contract.ts b/common/contract.ts index 8bdab6fe..c414a332 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -139,7 +139,7 @@ export const OUTCOME_TYPES = [ ] as const export const MAX_QUESTION_LENGTH = 480 -export const MAX_DESCRIPTION_LENGTH = 10000 +export const MAX_DESCRIPTION_LENGTH = 16000 export const MAX_TAG_LENGTH = 60 export const CPMM_MIN_POOL_QTY = 0.01 From 35df201e2e7548b2c57280651c111fb91f0543e1 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 12:32:22 -0500 Subject: [PATCH 18/22] prob bar for multiple choice --- web/components/contract/quick-bet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 09a5d4bc..482aea47 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -319,7 +319,7 @@ function getProb(contract: Contract) { ? getBinaryProb(contract) : outcomeType === 'PSEUDO_NUMERIC' ? getProbability(contract) - : outcomeType === 'FREE_RESPONSE' + : outcomeType === 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE' ? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '') : outcomeType === 'NUMERIC' ? getNumericScale(contract) From 654790315c82f1f1c82bd6262199310dc14f291a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:33:21 -0500 Subject: [PATCH 19/22] Fix missing key console error --- web/components/groups/group-chat.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index d872c980..62d327f2 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -338,7 +338,11 @@ const GroupMessage = memo(function GroupMessage_(props: { </Row> <div className="mt-2 text-black"> {comments.map((comment) => ( - <Content content={comment.content || comment.text} smallImage /> + <Content + key={comment.id} + content={comment.content || comment.text} + smallImage + /> ))} </div> <Row> From d7b021b79fb911ccdc0cb5cf824924633f2ec3f5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:37:51 -0500 Subject: [PATCH 20/22] Clear entered limit probs on submit limit order --- web/components/bet-panel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index c0f7ff94..9c572e0c 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -484,6 +484,8 @@ function LimitOrderPanel(props: { setIsSubmitting(false) setWasSubmitted(true) setBetAmount(undefined) + setLowLimitProb(undefined) + setHighLimitProb(undefined) if (onBuySuccess) onBuySuccess() }) From b5b77be188bdbfa8ba3ef524b38ec9efc985353d Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 10 Aug 2022 11:03:39 -0700 Subject: [PATCH 21/22] Accept URLs in the iframe editor TODO: Update placeholder text to mention this --- web/components/editor.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index cef1aa36..d52913b3 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -125,6 +125,13 @@ function isValidIframe(text: string) { return /^<iframe.*<\/iframe>$/.test(text) } +function isValidUrl(text: string) { + // Conjured by Codex, not sure if it's actually good + return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test( + text + ) +} + export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> @@ -191,8 +198,9 @@ function IframeModal(props: { setOpen: (open: boolean) => void }) { const { editor, open, setOpen } = props - const [embedCode, setEmbedCode] = useState('') - const valid = isValidIframe(embedCode) + const [input, setInput] = useState('') + const valid = isValidIframe(input) || isValidUrl(input) + const embedCode = isValidIframe(input) ? input : `<iframe src="${input}" />` return ( <Modal open={open} setOpen={setOpen}> @@ -209,8 +217,8 @@ function IframeModal(props: { id="embed" className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder='e.g. <iframe src="..."></iframe>' - value={embedCode} - onChange={(e) => setEmbedCode(e.target.value)} + value={input} + onChange={(e) => setInput(e.target.value)} /> {/* Preview the embed if it's valid */} @@ -222,7 +230,7 @@ function IframeModal(props: { onClick={() => { if (editor && valid) { editor.chain().insertContent(embedCode).run() - setEmbedCode('') + setInput('') setOpen(false) } }} @@ -232,7 +240,7 @@ function IframeModal(props: { <Button color="gray" onClick={() => { - setEmbedCode('') + setInput('') setOpen(false) }} > From 8c537537a12c1300976b37a3afb3db1a27f527af Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Wed, 10 Aug 2022 11:03:55 -0700 Subject: [PATCH 22/22] Add cache headers to avatars (#737) * Set cache headers on newly uploaded avatars * Go fix up all the old avatars to have cache headers --- .../src/scripts/set-avatar-cache-headers.ts | 27 +++++++++++++++++++ web/lib/firebase/storage.ts | 6 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 functions/src/scripts/set-avatar-cache-headers.ts diff --git a/functions/src/scripts/set-avatar-cache-headers.ts b/functions/src/scripts/set-avatar-cache-headers.ts new file mode 100644 index 00000000..676ec62d --- /dev/null +++ b/functions/src/scripts/set-avatar-cache-headers.ts @@ -0,0 +1,27 @@ +import { initAdmin } from './script-init' +import { log } from '../utils' + +const app = initAdmin() +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 +const AVATAR_EXTENSION_RE = /\.(gif|tiff|jpe?g|png|webp)$/i + +const processAvatars = async () => { + const storage = app.storage() + const bucket = storage.bucket(`${app.options.projectId}.appspot.com`) + const [files] = await bucket.getFiles({ prefix: 'user-images' }) + log(`${files.length} avatar images to process.`) + for (const file of files) { + if (AVATAR_EXTENSION_RE.test(file.name)) { + log(`Updating metadata for ${file.name}.`) + await file.setMetadata({ + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + }) + } else { + log(`Skipping ${file.name} because it probably isn't an avatar.`) + } + } +} + +if (require.main === module) { + processAvatars().catch((e) => console.error(e)) +} diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index fcf4422d..3acfbae9 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -3,6 +3,8 @@ import imageCompression from 'browser-image-compression' import { nanoid } from 'nanoid' import { storage } from './init' +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 + export const uploadImage = async ( username: string, file: File, @@ -24,7 +26,9 @@ export const uploadImage = async ( }) } - const uploadTask = uploadBytesResumable(storageRef, file) + const uploadTask = uploadBytesResumable(storageRef, file, { + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + }) let resolvePromise: (url: string) => void let rejectPromise: (reason?: any) => void