From e0806cf0e09d97a6494ae5fa18e40c35113941dd Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Fri, 16 Sep 2022 20:36:52 -0700 Subject: [PATCH 01/71] Fix links to group /about and /leaderboards --- web/pages/group/[...slugs]/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 1edcc638..73186e92 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -140,7 +140,10 @@ export default function GroupPage(props: { const user = useUser() const isAdmin = useAdmin() const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds - const [sidebarIndex, setSidebarIndex] = useState(0) + // Note: Keep in sync with sidebarPages + const [sidebarIndex, setSidebarIndex] = useState( + ['markets', 'leaderboards', 'about'].indexOf(page ?? 'markets') + ) useSaveReferral(user, { defaultReferrerUsername: creator.username, @@ -241,6 +244,12 @@ export default function GroupPage(props: { const onSidebarClick = (key: string) => { const index = sidebarPages.findIndex((t) => t.key === key) setSidebarIndex(index) + // Append the page to the URL, e.g. /group/mexifold/markets + router.replace( + { query: { ...router.query, slugs: [group.slug, key] } }, + undefined, + { shallow: true } + ) } const joinOrAddQuestionsButton = ( From fc5807ebbe8e52c036f0d380127eb267cb3f20b3 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 17 Sep 2022 14:35:49 -0500 Subject: [PATCH 02/71] halve MAX_QUESTION_LENGTH --- common/contract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/contract.ts b/common/contract.ts index 0d2a38ca..2f71bab7 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -148,7 +148,7 @@ export const OUTCOME_TYPES = [ 'NUMERIC', ] as const -export const MAX_QUESTION_LENGTH = 480 +export const MAX_QUESTION_LENGTH = 240 export const MAX_DESCRIPTION_LENGTH = 16000 export const MAX_TAG_LENGTH = 60 From 340b21c53e5dca0620632ad653f937830dae081a Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 17 Sep 2022 14:37:14 -0500 Subject: [PATCH 03/71] halve referral bonus --- common/economy.ts | 2 +- functions/src/email-templates/welcome.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/economy.ts b/common/economy.ts index a412d4de..7ec52b30 100644 --- a/common/economy.ts +++ b/common/economy.ts @@ -7,7 +7,7 @@ export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100 export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000 // for sus users, i.e. multiple sign ups for same person export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10 -export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 500 +export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250 export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10 export const BETTING_STREAK_BONUS_AMOUNT = diff --git a/functions/src/email-templates/welcome.html b/functions/src/email-templates/welcome.html index d6caaa0c..986ec7cc 100644 --- a/functions/src/email-templates/welcome.html +++ b/functions/src/email-templates/welcome.html @@ -210,7 +210,7 @@ class="link-build-content" style="color:inherit;; text-decoration: none;" target="_blank" href="https://manifold.markets/referrals">Refer - your friends and earn M$500 for each signup! + your friends and earn M$250 for each signup!
  • Date: Sat, 17 Sep 2022 14:46:59 -0500 Subject: [PATCH 04/71] update /about redirect --- web/next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/next.config.js b/web/next.config.js index 5438d206..21b375ba 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -1,6 +1,6 @@ const API_DOCS_URL = 'https://docs.manifold.markets/api' -const ABOUT_PAGE_URL = 'https://docs.manifold.markets/$how-to' +const ABOUT_PAGE_URL = 'https://help.manifold.markets/' /** @type {import('next').NextConfig} */ module.exports = { From f35799c129f73f68a1cd09ff31ce7af770466512 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 17 Sep 2022 14:54:55 -0500 Subject: [PATCH 05/71] Only autofocus search if no query params set --- web/pages/search.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/pages/search.tsx b/web/pages/search.tsx index 38b9760d..9c26f1a8 100644 --- a/web/pages/search.tsx +++ b/web/pages/search.tsx @@ -4,6 +4,7 @@ import { ContractSearch } from 'web/components/contract-search' import { useTracking } from 'web/hooks/use-tracking' import { useUser } from 'web/hooks/use-user' import { usePrefetch } from 'web/hooks/use-prefetch' +import { useRouter } from 'next/router' export default function Search() { const user = useUser() @@ -11,6 +12,10 @@ export default function Search() { useTracking('view search') + const { query } = useRouter() + const { q, s, p } = query + const autoFocus = !q && !s && !p + return ( @@ -18,7 +23,7 @@ export default function Search() { user={user} persistPrefix="search" useQueryUrlParam={true} - autoFocus + autoFocus={autoFocus} /> From d2471e2a020041f8df7aa18e17d5e5a0b8994299 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 17 Sep 2022 14:58:42 -0500 Subject: [PATCH 06/71] group selector dialog: loading indicator, tracking --- .../onboarding/group-selector-dialog.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/web/components/onboarding/group-selector-dialog.tsx b/web/components/onboarding/group-selector-dialog.tsx index e109e356..0a246403 100644 --- a/web/components/onboarding/group-selector-dialog.tsx +++ b/web/components/onboarding/group-selector-dialog.tsx @@ -10,6 +10,8 @@ import { Modal } from 'web/components/layout/modal' import { PillButton } from 'web/components/buttons/pill-button' import { Button } from 'web/components/button' import { Group } from 'common/group' +import { LoadingIndicator } from '../loading-indicator' +import { withTracking } from 'web/lib/service/analytics' export default function GroupSelectorDialog(props: { open: boolean @@ -65,20 +67,26 @@ export default function GroupSelectorDialog(props: {

    - {user && + {!user || displayedGroups.length === 0 ? ( + + ) : ( displayedGroups.map((group) => ( - memberGroupIds.includes(group.id) - ? leaveGroup(group, user.id) - : joinGroup(group, user.id) - } + onSelect={withTracking( + () => + memberGroupIds.includes(group.id) + ? leaveGroup(group, user.id) + : joinGroup(group, user.id), + 'toggle group pill', + { group: group.slug } + )} className="mr-1 mb-2 max-w-[12rem] truncate" > {group.name} - ))} + )) + )}
    From fde90be5a2c275c8bedfbcd15c9ab62145d2a0d9 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 17 Sep 2022 15:01:45 -0500 Subject: [PATCH 07/71] fix resolve prob notification text --- web/pages/notifications.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2f5c0bf9..15863919 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -971,13 +971,20 @@ function ContractResolvedNotification(props: { const { sourceText, data } = notification const { userInvestment, userPayout } = (data as ContractResolutionData) ?? {} const subtitle = 'resolved the market' + const resolutionDescription = () => { if (!sourceText) return
    + if (sourceText === 'YES' || sourceText == 'NO') { return } + if (sourceText.includes('%')) - return + return ( + + ) if (sourceText === 'CANCEL') return if (sourceText === 'MKT' || sourceText === 'PROB') return @@ -996,7 +1003,7 @@ function ContractResolvedNotification(props: { const description = userInvestment && userPayout !== undefined ? ( - {resolutionDescription()} + Resolved: {resolutionDescription()} Invested: {formatMoney(userInvestment)} Payout: @@ -1013,7 +1020,7 @@ function ContractResolvedNotification(props: { ) : ( - {resolutionDescription()} + Resolved {resolutionDescription()} ) if (justSummary) { From fdde57e3349b843abd71869ff849b02ca37d2f47 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 17 Sep 2022 15:10:16 -0500 Subject: [PATCH 08/71] 'predictor' => 'trader' --- common/envs/prod.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/envs/prod.ts b/common/envs/prod.ts index a9d1ffc3..73bd6029 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -82,9 +82,9 @@ export const PROD_CONFIG: EnvConfig = { visibility: 'PUBLIC', moneyMoniker: 'M$', - bettor: 'predictor', - pastBet: 'prediction', - presentBet: 'predict', + bettor: 'trader', + pastBet: 'trade', + presentBet: 'trade', navbarLogoPath: '', faviconPath: '/favicon.ico', newQuestionPlaceholders: [ From a54f060ccb6a4fb6beaeab06dda36515c6109cd8 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 17 Sep 2022 15:15:37 -0500 Subject: [PATCH 09/71] New for you => New --- web/pages/home/index.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index f486fa4c..d3009a84 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -102,7 +102,7 @@ export default function Home() { const HOME_SECTIONS = [ { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, - { label: 'New for you', id: 'new-for-you' }, + { label: 'New', id: 'newest' }, { label: 'Recently updated', id: 'recently-updated-for-you' }, ] @@ -139,16 +139,6 @@ function renderSection( if (id === 'daily-movers') { return } - if (id === 'new-for-you') - return ( - - ) if (id === 'recently-updated-for-you') return ( Date: Sat, 17 Sep 2022 17:58:08 -0500 Subject: [PATCH 10/71] Show absolute prob in daily movers as well --- web/components/contract/prob-change-table.tsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 16de0d44..ceff8060 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -6,6 +6,7 @@ import { SiteLink } from '../site-link' import { Col } from '../layout/col' import { Row } from '../layout/row' import { LoadingIndicator } from '../loading-indicator' +import { ProfitBadge } from '../bets-list' export function ProbChangeTable(props: { changes: @@ -54,14 +55,14 @@ export function ProbChangeTable(props: { function ProbChangeRow(props: { contract: CPMMContract }) { const { contract } = props return ( - - + {contract.question} + ) } @@ -72,19 +73,15 @@ export function ProbChange(props: { }) { const { contract, className } = props const { + prob, probChanges: { day: change }, } = contract - - const color = - change > 0 - ? 'text-green-500' - : change < 0 - ? 'text-red-500' - : 'text-gray-600' - - const str = - change === 0 - ? '+0%' - : `${change > 0 ? '+' : '-'}${formatPercent(Math.abs(change))}` - return
    {str}
    + return ( + + + {formatPercent(Math.round(100 * prob) / 100)} + + + + ) } From 191ec9535ca28bb9cd16cdc811fd9fd4849a8369 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 17 Sep 2022 18:00:24 -0500 Subject: [PATCH 11/71] Show more rows on daily movers all --- web/components/contract/prob-change-table.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index ceff8060..8a3ed87f 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -26,10 +26,9 @@ export function ProbChangeTable(props: { negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1 ) const maxRows = Math.min(positiveChanges.length, negativeChanges.length) - const rows = Math.min( - full ? Infinity : 3, - Math.min(maxRows, countOverThreshold) - ) + const rows = full + ? maxRows + : Math.min(3, Math.min(maxRows, countOverThreshold)) const filteredPositiveChanges = positiveChanges.slice(0, rows) const filteredNegativeChanges = negativeChanges.slice(0, rows) @@ -55,7 +54,7 @@ export function ProbChangeTable(props: { function ProbChangeRow(props: { contract: CPMMContract }) { const { contract } = props return ( - + Date: Sat, 17 Sep 2022 18:25:53 -0500 Subject: [PATCH 12/71] refactor sidebar; add to mobile navbar --- web/components/nav/more-button.tsx | 23 +++ web/components/nav/sidebar-item.tsx | 63 ++++++++ web/components/nav/sidebar.tsx | 218 ++++++++++------------------ 3 files changed, 161 insertions(+), 143 deletions(-) create mode 100644 web/components/nav/more-button.tsx create mode 100644 web/components/nav/sidebar-item.tsx diff --git a/web/components/nav/more-button.tsx b/web/components/nav/more-button.tsx new file mode 100644 index 00000000..5e6653f3 --- /dev/null +++ b/web/components/nav/more-button.tsx @@ -0,0 +1,23 @@ +import { DotsHorizontalIcon } from '@heroicons/react/outline' + +export function MoreButton() { + return +} + +function SidebarButton(props: { + text: string + icon: React.ComponentType<{ className?: string }> + children?: React.ReactNode +}) { + const { text, children } = props + return ( +
    + + ) +} diff --git a/web/components/nav/sidebar-item.tsx b/web/components/nav/sidebar-item.tsx new file mode 100644 index 00000000..0023511f --- /dev/null +++ b/web/components/nav/sidebar-item.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import clsx from 'clsx' +import Link from 'next/link' + +import { trackCallback } from 'web/lib/service/analytics' + +export type Item = { + name: string + trackingEventName?: string + href?: string + key?: string + icon?: React.ComponentType<{ className?: string }> +} + +export function SidebarItem(props: { + item: Item + currentPage: string + onClick?: (key: string) => void +}) { + const { item, currentPage, onClick } = props + const isCurrentPage = + item.href != null ? item.href === currentPage : item.key === currentPage + + const sidebarItem = ( + + {item.icon && ( + + ) + + if (item.href) { + return ( + + {sidebarItem} + + ) + } else { + return onClick ? ( + + ) : ( + <> + ) + } +} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index ae03655b..618bbe94 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -1,29 +1,93 @@ +import React from 'react' import { HomeIcon, SearchIcon, BookOpenIcon, - DotsHorizontalIcon, CashIcon, HeartIcon, ChatIcon, + ChartBarIcon, } from '@heroicons/react/outline' import clsx from 'clsx' -import Link from 'next/link' import Router, { useRouter } from 'next/router' + import { useUser } from 'web/hooks/use-user' import { firebaseLogout, User } from 'web/lib/firebase/users' import { ManifoldLogo } from './manifold-logo' import { MenuButton, MenuItem } from './menu' import { ProfileSummary } from './profile-menu' import NotificationsIcon from 'web/components/notifications-icon' -import React from 'react' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { CreateQuestionButton } from 'web/components/create-question-button' -import { trackCallback, withTracking } from 'web/lib/service/analytics' +import { withTracking } from 'web/lib/service/analytics' import { CHALLENGES_ENABLED } from 'common/challenge' import { buildArray } from 'common/util/array' import TrophyIcon from 'web/lib/icons/trophy-icon' import { SignInButton } from '../sign-in-button' +import { SidebarItem } from './sidebar-item' +import { MoreButton } from './more-button' + +export default function Sidebar(props: { className?: string }) { + const { className } = props + const router = useRouter() + const currentPage = router.pathname + + const user = useUser() + + const desktopNavOptions = !user + ? signedOutDesktopNavigation + : getDesktopNavigation() + + const mobileNavOptions = !user + ? signedOutMobileNavigation + : signedInMobileNavigation + + const createMarketButton = user && !user.isBannedFromPosting && ( + + ) + + return ( + + ) +} const logout = async () => { // log out, and then reload the page, in case SSR wants to boot them out @@ -32,7 +96,7 @@ const logout = async () => { await Router.replace(Router.asPath) } -function getNavigation() { +function getDesktopNavigation() { return [ { name: 'Home', href: '/home', icon: HomeIcon }, { name: 'Search', href: '/search', icon: SearchIcon }, @@ -51,10 +115,10 @@ function getNavigation() { ] } -function getMoreNavigation(user?: User | null) { +function getMoreDesktopNavigation(user?: User | null) { if (IS_PRIVATE_MANIFOLD) { return [ - { name: 'Leaderboards', href: '/leaderboards' }, + { name: 'Leaderboards', href: '/leaderboards', icon: ChartBarIcon }, { name: 'Sign out', href: '#', @@ -81,7 +145,6 @@ function getMoreNavigation(user?: User | null) { // Signed in "More" return buildArray( - { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Groups', href: '/groups' }, CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, [ @@ -99,7 +162,7 @@ function getMoreNavigation(user?: User | null) { ) } -const signedOutNavigation = [ +const signedOutDesktopNavigation = [ { name: 'Home', href: '/', icon: HomeIcon }, { name: 'Explore', href: '/search', icon: SearchIcon }, { @@ -117,11 +180,14 @@ const signedOutMobileNavigation = [ }, { name: 'Charity', href: '/charity', icon: HeartIcon }, { name: 'Tournaments', href: '/tournaments', icon: TrophyIcon }, + { name: 'Leaderboards', href: '/leaderboards', icon: ChartBarIcon }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon }, ] const signedInMobileNavigation = [ + { name: 'Search', href: '/search', icon: SearchIcon }, { name: 'Tournaments', href: '/tournaments', icon: TrophyIcon }, + { name: 'Leaderboards', href: '/leaderboards', icon: ChartBarIcon }, ...(IS_PRIVATE_MANIFOLD ? [] : [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]), @@ -145,7 +211,6 @@ function getMoreMobileNav() { [ { name: 'Groups', href: '/groups' }, { name: 'Referrals', href: '/referrals' }, - { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, @@ -153,136 +218,3 @@ function getMoreMobileNav() { signOut ) } - -export type Item = { - name: string - trackingEventName?: string - href?: string - key?: string - icon?: React.ComponentType<{ className?: string }> -} - -export function SidebarItem(props: { - item: Item - currentPage: string - onClick?: (key: string) => void -}) { - const { item, currentPage, onClick } = props - const isCurrentPage = - item.href != null ? item.href === currentPage : item.key === currentPage - - const sidebarItem = ( - - {item.icon && ( - - ) - - if (item.href) { - return ( - - {sidebarItem} - - ) - } else { - return onClick ? ( - - ) : ( - <> - ) - } -} - -function SidebarButton(props: { - text: string - icon: React.ComponentType<{ className?: string }> - children?: React.ReactNode -}) { - const { text, children } = props - return ( - - - ) -} - -function MoreButton() { - return -} - -export default function Sidebar(props: { className?: string }) { - const { className } = props - const router = useRouter() - const currentPage = router.pathname - - const user = useUser() - - const navigationOptions = !user ? signedOutNavigation : getNavigation() - const mobileNavigationOptions = !user - ? signedOutMobileNavigation - : signedInMobileNavigation - - return ( - - ) -} From 1fbadf8181d69cc37037d51105ae54369d509922 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 17 Sep 2022 18:30:27 -0500 Subject: [PATCH 13/71] Improve Customize UI --- web/components/arrange-home.tsx | 63 ++++++++++++++++++++++++++------- web/hooks/use-group.ts | 16 +++++++++ web/pages/home/edit.tsx | 16 ++++++--- web/pages/home/index.tsx | 56 ++++++++++++++--------------- 4 files changed, 105 insertions(+), 46 deletions(-) diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index 96fe0e6f..4bc88f14 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -1,14 +1,22 @@ import clsx from 'clsx' import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' import { MenuIcon } from '@heroicons/react/solid' +import { toast } from 'react-hot-toast' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Subtitle } from 'web/components/subtitle' import { keyBy } from 'lodash' +import { XCircleIcon } from '@heroicons/react/outline' +import { Button } from './button' +import { updateUser } from 'web/lib/firebase/users' +import { leaveGroup } from 'web/lib/firebase/groups' +import { User } from 'common/user' +import { useUser } from 'web/hooks/use-user' +import { Group } from 'common/group' export function ArrangeHome(props: { - sections: { label: string; id: string }[] + sections: { label: string; id: string; group?: Group }[] setSectionIds: (sections: string[]) => void }) { const { sections, setSectionIds } = props @@ -40,8 +48,9 @@ export function ArrangeHome(props: { function DraggableList(props: { title: string - items: { id: string; label: string }[] + items: { id: string; label: string; group?: Group }[] }) { + const user = useUser() const { title, items } = props return ( @@ -66,6 +75,7 @@ function DraggableList(props: { snapshot.isDragging && 'z-[9000] bg-gray-200' )} item={item} + user={user} />
    )} @@ -79,23 +89,52 @@ function DraggableList(props: { } const SectionItem = (props: { - item: { id: string; label: string } + item: { id: string; label: string; group?: Group } + user: User | null | undefined className?: string }) => { - const { item, className } = props + const { item, user, className } = props + const { group } = item return ( -
    -
    + + + + {group && ( + + )} + ) } diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index d3d8dd9f..59b36b27 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { Group } from 'common/group' import { User } from 'common/user' import { + getGroup, getMemberGroups, GroupMemberDoc, groupMembers, @@ -102,6 +103,21 @@ export const useMemberGroupIds = (user: User | null | undefined) => { return memberGroupIds } +export function useMemberGroupsSubscription(user: User | null | undefined) { + const cachedGroups = useMemberGroups(user?.id) ?? [] + const groupIds = useMemberGroupIds(user) + const [groups, setGroups] = useState(cachedGroups) + + useEffect(() => { + if (groupIds) { + Promise.all(groupIds.map((id) => getGroup(id))).then((groups) => + setGroups(filterDefined(groups)) + ) + } + }, [groupIds]) + return groups +} + export function useMembers(groupId: string | undefined) { const [members, setMembers] = useState([]) useEffect(() => { diff --git a/web/pages/home/edit.tsx b/web/pages/home/edit.tsx index 9670181b..48e10c6c 100644 --- a/web/pages/home/edit.tsx +++ b/web/pages/home/edit.tsx @@ -7,12 +7,12 @@ import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' import { SiteLink } from 'web/components/site-link' import { Title } from 'web/components/title' -import { useMemberGroups } from 'web/hooks/use-group' +import { useMemberGroupsSubscription } from 'web/hooks/use-group' import { useTracking } from 'web/hooks/use-tracking' import { useUser } from 'web/hooks/use-user' import { updateUser } from 'web/lib/firebase/users' import { track } from 'web/lib/service/analytics' -import { getHomeItems } from '.' +import { getHomeItems, TrendingGroupsSection } from '.' export default function Home() { const user = useUser() @@ -27,7 +27,7 @@ export default function Home() { setHomeSections(newHomeSections) } - const groups = useMemberGroups(user?.id) ?? [] + const groups = useMemberGroupsSubscription(user) const { sections } = getHomeItems(groups, homeSections) return ( @@ -38,7 +38,15 @@ export default function Home() { - + + + + + + ) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index d3009a84..17d55d56 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useEffect, useState } from 'react' +import React, { ReactNode } from 'react' import Router from 'next/router' import { AdjustmentsIcon, @@ -22,18 +22,13 @@ import { SiteLink } from 'web/components/site-link' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useMemberGroupIds, - useMemberGroups, + useMemberGroupsSubscription, useTrendingGroups, } from 'web/hooks/use-group' import { Button } from 'web/components/button' import { Row } from 'web/components/layout/row' import { ProbChangeTable } from 'web/components/contract/prob-change-table' -import { - getGroup, - groupPath, - joinGroup, - leaveGroup, -} from 'web/lib/firebase/groups' +import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { formatMoney } from 'common/util/format' import { useProbChanges } from 'web/hooks/use-prob-changes' @@ -57,17 +52,7 @@ export default function Home() { useSaveReferral() usePrefetch(user?.id) - const cachedGroups = useMemberGroups(user?.id) ?? [] - const groupIds = useMemberGroupIds(user) - const [groups, setGroups] = useState(cachedGroups) - - useEffect(() => { - if (groupIds) { - Promise.all(groupIds.map((id) => getGroup(id))).then((groups) => - setGroups(filterDefined(groups)) - ) - } - }, [groupIds]) + const groups = useMemberGroupsSubscription(user) const { sections } = getHomeItems(groups, user?.homeSections ?? []) @@ -77,7 +62,10 @@ export default function Home() { - + <Row className="items-center gap-2"> + <Title className="!mt-0 !mb-0" text="Home" /> + <CustomizeButton justIcon /> + </Row> <DailyStats user={user} /> </Row> @@ -110,11 +98,12 @@ export const getHomeItems = (groups: Group[], sections: string[]) => { // Accommodate old home sections. if (!isArray(sections)) sections = [] - const items = [ + const items: { id: string; label: string; group?: Group }[] = [ ...HOME_SECTIONS, ...groups.map((g) => ({ label: g.name, id: g.id, + group: g, })), ] const itemsById = keyBy(items, 'id') @@ -225,7 +214,6 @@ function GroupSection(props: { <Col> <SectionHeader label={group.name} href={groupPath(group.slug)}> <Button - className="" color="gray-white" onClick={() => { if (user) { @@ -312,20 +300,24 @@ function DailyStats(props: { ) } -function TrendingGroupsSection(props: { user: User | null | undefined }) { - const { user } = props +export function TrendingGroupsSection(props: { + user: User | null | undefined + full?: boolean + className?: string +}) { + const { user, full, className } = props const memberGroupIds = useMemberGroupIds(user) || [] const groups = useTrendingGroups().filter( (g) => !memberGroupIds.includes(g.id) ) - const count = 25 + const count = full ? 100 : 25 const chosenGroups = groups.slice(0, count) return ( - <Col> + <Col className={className}> <SectionHeader label="Trending groups" href="/explore-groups"> - <CustomizeButton /> + {!full && <CustomizeButton className="mb-1" />} </SectionHeader> <Row className="flex-wrap gap-2"> {chosenGroups.map((g) => ( @@ -359,10 +351,14 @@ function TrendingGroupsSection(props: { user: User | null | undefined }) { ) } -function CustomizeButton() { +function CustomizeButton(props: { justIcon?: boolean; className?: string }) { + const { justIcon, className } = props return ( <SiteLink - className="mb-2 flex flex-row items-center text-xl hover:no-underline" + className={clsx( + className, + 'flex flex-row items-center text-xl hover:no-underline' + )} href="/home/edit" > <Button size="lg" color="gray" className={clsx('flex gap-2')}> @@ -370,7 +366,7 @@ function CustomizeButton() { className={clsx('h-[24px] w-5 text-gray-500')} aria-hidden="true" /> - Customize + {!justIcon && 'Customize'} </Button> </SiteLink> ) From 8f30ef38d95931f6c466f5ea2b1cb3058faad8d3 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 17 Sep 2022 18:40:45 -0500 Subject: [PATCH 14/71] fix imports --- web/components/nav/bottom-nav-bar.tsx | 3 ++- web/components/nav/group-nav-bar.tsx | 2 +- web/components/nav/group-sidebar.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/components/nav/bottom-nav-bar.tsx b/web/components/nav/bottom-nav-bar.tsx index aeb5a2bc..f906b21d 100644 --- a/web/components/nav/bottom-nav-bar.tsx +++ b/web/components/nav/bottom-nav-bar.tsx @@ -8,7 +8,8 @@ import { } from '@heroicons/react/outline' import { Transition, Dialog } from '@headlessui/react' import { useState, Fragment } from 'react' -import Sidebar, { Item } from './sidebar' +import Sidebar from './sidebar' +import { Item } from './sidebar-item' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { Avatar } from '../avatar' diff --git a/web/components/nav/group-nav-bar.tsx b/web/components/nav/group-nav-bar.tsx index 986f35a1..8c20fee9 100644 --- a/web/components/nav/group-nav-bar.tsx +++ b/web/components/nav/group-nav-bar.tsx @@ -1,5 +1,5 @@ import { ClipboardIcon, HomeIcon } from '@heroicons/react/outline' -import { Item } from './sidebar' +import { Item } from './sidebar-item' import clsx from 'clsx' import { trackCallback } from 'web/lib/service/analytics' diff --git a/web/components/nav/group-sidebar.tsx b/web/components/nav/group-sidebar.tsx index 12f9e7a9..a68064e0 100644 --- a/web/components/nav/group-sidebar.tsx +++ b/web/components/nav/group-sidebar.tsx @@ -7,7 +7,7 @@ import React from 'react' import TrophyIcon from 'web/lib/icons/trophy-icon' import { SignInButton } from '../sign-in-button' import NotificationsIcon from '../notifications-icon' -import { SidebarItem } from './sidebar' +import { SidebarItem } from './sidebar-item' import { buildArray } from 'common/util/array' import { User } from 'common/user' import { Row } from '../layout/row' From e7ed893b788fd34ebd22ad01d35f9197f09766d4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 18:45:40 -0500 Subject: [PATCH 15/71] Round prob in Daily movers --- web/components/bets-list.tsx | 7 +++++-- web/components/contract/prob-change-table.tsx | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 9c76174b..97d11758 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -756,9 +756,10 @@ function SellButton(props: { export function ProfitBadge(props: { profitPercent: number + round?: boolean className?: string }) { - const { profitPercent, className } = props + const { profitPercent, round, className } = props if (!profitPercent) return null const colors = profitPercent > 0 @@ -773,7 +774,9 @@ export function ProfitBadge(props: { className )} > - {(profitPercent > 0 ? '+' : '') + profitPercent.toFixed(1) + '%'} + {(profitPercent > 0 ? '+' : '') + + profitPercent.toFixed(round ? 0 : 1) + + '%'} </span> ) } diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 8a3ed87f..644946c9 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -77,10 +77,10 @@ export function ProbChange(props: { } = contract return ( <Col className={clsx('flex flex-col items-end', className)}> - <span className="mr-1.5 mb-0.5 text-2xl"> + <span className="mb-0.5 mr-0.5 text-2xl"> {formatPercent(Math.round(100 * prob) / 100)} </span> - <ProfitBadge className="ml-0" profitPercent={100 * change} /> + <ProfitBadge className="ml-0" profitPercent={100 * change} round /> </Col> ) } From 37cff04e39f092724b3a3fdd2bda3b07acb357a9 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 17 Sep 2022 18:49:16 -0500 Subject: [PATCH 16/71] share dialog styling --- web/components/contract/share-modal.tsx | 59 +++++++++++++------------ web/components/share-embed-button.tsx | 9 +--- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index ff3f41ae..e1eb26eb 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -9,7 +9,6 @@ import { Row } from '../layout/row' import { ShareEmbedButton } from '../share-embed-button' import { Title } from '../title' import { TweetButton } from '../tweet-button' -import { DuplicateContractButton } from '../copy-contract-button' import { Button } from '../button' import { copyToClipboard } from 'web/lib/util/copy' import { track, withTracking } from 'web/lib/service/analytics' @@ -21,6 +20,7 @@ import { REFERRAL_AMOUNT } from 'common/economy' import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal' import { useState } from 'react' import { CHALLENGES_ENABLED } from 'common/challenge' +import ChallengeIcon from 'web/lib/icons/challenge-icon' export function ShareModal(props: { contract: Contract @@ -56,8 +56,8 @@ export function ShareModal(props: { </p> <Button size="2xl" - color="gradient" - className={'flex max-w-xs self-center'} + color="indigo" + className={'mb-2 flex max-w-xs self-center'} onClick={() => { copyToClipboard(shareUrl) toast.success('Link copied!', { @@ -68,38 +68,39 @@ export function ShareModal(props: { > {linkIcon} Copy link </Button> - <Row className={'justify-center'}>or</Row> - {showChallenge && ( - <Button - size="2xl" - color="gradient" - className={'mb-2 flex max-w-xs self-center'} - onClick={withTracking( - () => setOpenCreateChallengeModal(true), - 'click challenge button' - )} - > - <span>⚔️ Challenge</span> - <CreateChallengeModal - isOpen={openCreateChallengeModal} - setOpen={(open) => { - if (!open) { - setOpenCreateChallengeModal(false) - setOpen(false) - } else setOpenCreateChallengeModal(open) - }} - user={user} - contract={contract} - /> - </Button> - )} + <Row className="z-0 flex-wrap justify-center gap-4 self-center"> <TweetButton className="self-start" tweetText={getTweetText(contract, shareUrl)} /> + <ShareEmbedButton contract={contract} /> - <DuplicateContractButton contract={contract} /> + + {showChallenge && ( + <button + className={ + 'btn btn-xs flex-nowrap border-2 !border-indigo-500 !bg-white normal-case text-indigo-500' + } + onClick={withTracking( + () => setOpenCreateChallengeModal(true), + 'click challenge button' + )} + > + ️<ChallengeIcon className="mr-1 h-4 w-4" /> Challenge + <CreateChallengeModal + isOpen={openCreateChallengeModal} + setOpen={(open) => { + if (!open) { + setOpenCreateChallengeModal(false) + setOpen(false) + } else setOpenCreateChallengeModal(open) + }} + user={user} + contract={contract} + /> + </button> + )} </Row> </Col> </Modal> diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 79c63d5a..0a5dc0c9 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -40,14 +40,7 @@ export function ShareEmbedButton(props: { contract: Contract }) { track('copy embed code') }} > - <Menu.Button - className="btn btn-xs normal-case" - style={{ - backgroundColor: 'white', - border: '2px solid #9ca3af', - color: '#9ca3af', // text-gray-400 - }} - > + <Menu.Button className="btn btn-xs border-2 !border-gray-500 !bg-white normal-case text-gray-500"> {codeIcon} Embed </Menu.Button> From 350ab35856390c61f7296028752f8303cf6d5cab Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 18:52:13 -0500 Subject: [PATCH 17/71] Tweak padding --- web/components/arrange-home.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index 4bc88f14..45c8a588 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -113,6 +113,7 @@ const SectionItem = (props: { {group && ( <Button + className="pt-1 pb-1" color="gray-white" onClick={() => { if (user) { From f71791bdd50a74c94af0c3b5e761c13425e75de6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 17 Sep 2022 19:10:34 -0500 Subject: [PATCH 18/71] fix labels --- common/user.ts | 2 ++ web/components/bet-button.tsx | 2 +- web/components/feed/feed-comments.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/common/user.ts b/common/user.ts index 5ab07d35..3a5b91e1 100644 --- a/common/user.ts +++ b/common/user.ts @@ -86,6 +86,8 @@ export type PortfolioMetrics = { export const MANIFOLD_USERNAME = 'ManifoldMarkets' export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' +// TODO: remove. Hardcoding the strings would be better. +// Different views require different language. export const BETTOR = ENV_CONFIG.bettor ?? 'bettor' // aka predictor export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'bettors' export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'bet' // aka predict diff --git a/web/components/bet-button.tsx b/web/components/bet-button.tsx index c0177fb3..4ac4b85c 100644 --- a/web/components/bet-button.tsx +++ b/web/components/bet-button.tsx @@ -42,7 +42,7 @@ export default function BetButton(props: { )} onClick={() => setOpen(true)} > - {PRESENT_BET} + Predict </Button> ) : ( <BetSignUpPrompt /> diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 9d2ba85e..a56d3125 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -255,7 +255,7 @@ function CommentStatus(props: { const { contract, outcome, prob } = props return ( <> - {` ${PRESENT_BET}ing `} + {` predicting `} <OutcomeLabel outcome={outcome} contract={contract} truncate="short" /> {prob && ' at ' + Math.round(prob * 100) + '%'} </> From 44f9a1faa2c662967d895cad45d67e9520fb8a7c Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 17 Sep 2022 19:12:35 -0500 Subject: [PATCH 19/71] fix labels --- web/components/bet-button.tsx | 1 - web/components/feed/feed-comments.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/bet-button.tsx b/web/components/bet-button.tsx index 4ac4b85c..d207a7ab 100644 --- a/web/components/bet-button.tsx +++ b/web/components/bet-button.tsx @@ -10,7 +10,6 @@ import { useSaveBinaryShares } from './use-save-binary-shares' import { Col } from './layout/col' import { Button } from 'web/components/button' import { BetSignUpPrompt } from './sign-up-prompt' -import { PRESENT_BET } from 'common/user' /** Button that opens BetPanel in a new modal */ export default function BetButton(props: { diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index a56d3125..f9cb205c 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -1,6 +1,6 @@ import { Bet } from 'common/bet' import { ContractComment } from 'common/comment' -import { PRESENT_BET, User } from 'common/user' +import { User } from 'common/user' import { Contract } from 'common/contract' import React, { useEffect, useState } from 'react' import { minBy, maxBy, partition, sumBy, Dictionary } from 'lodash' From 47cc313aefd14c4993400280a47e8d540f232df6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 17 Sep 2022 19:15:26 -0500 Subject: [PATCH 20/71] add back leaderboards link --- web/components/nav/sidebar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 618bbe94..45347774 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -145,6 +145,7 @@ function getMoreDesktopNavigation(user?: User | null) { // Signed in "More" return buildArray( + { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Groups', href: '/groups' }, CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, [ From a14e7d394706388544cbdddef4d5be191773cd68 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 19:06:25 -0500 Subject: [PATCH 21/71] Move Algolia bits to own file in web/lib/service --- web/components/contract-search.tsx | 18 +++++++----------- web/lib/service/algolia.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 web/lib/service/algolia.ts diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 9ecb6a2c..3d25dcdd 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -1,5 +1,4 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import algoliasearch from 'algoliasearch/lite' import { SearchOptions } from '@algolia/client-search' import { useRouter } from 'next/router' import { Contract } from 'common/contract' @@ -11,7 +10,7 @@ import { import { ShowTime } from './contract/contract-details' import { Row } from './layout/row' import { useEffect, useLayoutEffect, useRef, useMemo, ReactNode } from 'react' -import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' +import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useFollows } from 'web/hooks/use-follows' import { historyStore, @@ -28,14 +27,11 @@ import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { Col } from './layout/col' import clsx from 'clsx' import { safeLocalStorage } from 'web/lib/util/local' - -const searchClient = algoliasearch( - 'GJQPAYENIF', - '75c28fc084a80e1129d427d470cf41a3' -) - -const indexPrefix = ENV === 'DEV' ? 'dev-' : '' -const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' +import { + getIndexName, + searchClient, + searchIndexName, +} from 'web/lib/service/algolia' export const SORTS = [ { label: 'Newest', value: 'newest' }, @@ -154,7 +150,7 @@ export function ContractSearch(props: { if (freshQuery || requestedPage < state.numPages) { const index = query ? searchIndex - : searchClient.initIndex(`${indexPrefix}contracts-${sort}`) + : searchClient.initIndex(getIndexName(sort)) const numericFilters = query ? [] : [ diff --git a/web/lib/service/algolia.ts b/web/lib/service/algolia.ts new file mode 100644 index 00000000..14c12fe5 --- /dev/null +++ b/web/lib/service/algolia.ts @@ -0,0 +1,14 @@ +import algoliasearch from 'algoliasearch/lite' +import { ENV } from 'common/envs/constants' + +export const searchClient = algoliasearch( + 'GJQPAYENIF', + '75c28fc084a80e1129d427d470cf41a3' +) + +const indexPrefix = ENV === 'DEV' ? 'dev-' : '' +export const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' + +export const getIndexName = (sort: string) => { + return `${indexPrefix}contracts-${sort}` +} From 436646cc47f886ee6adf0658d3378a678732afa7 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 19:18:46 -0500 Subject: [PATCH 22/71] Use algolia to fetch daily movers so it's faster. --- web/hooks/use-prefetch.ts | 2 -- web/hooks/use-prob-changes.tsx | 30 +++++++++++++++++++++++++++++- web/pages/daily-movers.tsx | 4 ++-- web/pages/home/index.tsx | 4 ++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/web/hooks/use-prefetch.ts b/web/hooks/use-prefetch.ts index 5d95baf4..46d78b3c 100644 --- a/web/hooks/use-prefetch.ts +++ b/web/hooks/use-prefetch.ts @@ -1,6 +1,5 @@ import { usePrefetchUserBetContracts } from './use-contracts' import { usePrefetchPortfolioHistory } from './use-portfolio-history' -import { usePrefetchProbChanges } from './use-prob-changes' import { usePrefetchUserBets } from './use-user-bets' export function usePrefetch(userId: string | undefined) { @@ -9,6 +8,5 @@ export function usePrefetch(userId: string | undefined) { usePrefetchUserBets(maybeUserId), usePrefetchUserBetContracts(maybeUserId), usePrefetchPortfolioHistory(maybeUserId, 'weekly'), - usePrefetchProbChanges(userId), ]) } diff --git a/web/hooks/use-prob-changes.tsx b/web/hooks/use-prob-changes.tsx index 699b67ee..698111b8 100644 --- a/web/hooks/use-prob-changes.tsx +++ b/web/hooks/use-prob-changes.tsx @@ -1,11 +1,39 @@ import { useFirestoreQueryData } from '@react-query-firebase/firestore' +import { CPMMContract } from 'common/contract' import { MINUTE_MS } from 'common/util/time' -import { useQueryClient } from 'react-query' +import { useQuery, useQueryClient } from 'react-query' import { getProbChangesNegative, getProbChangesPositive, } from 'web/lib/firebase/contracts' import { getValues } from 'web/lib/firebase/utils' +import { getIndexName, searchClient } from 'web/lib/service/algolia' + +export const useProbChangesAlgolia = (userId: string) => { + const { data: positiveData } = useQuery(['prob-change-day', userId], () => + searchClient + .initIndex(getIndexName('prob-change-day')) + .search<CPMMContract>('', { facetFilters: ['uniqueBettorIds:' + userId] }) + ) + const { data: negativeData } = useQuery( + ['prob-change-day-ascending', userId], + () => + searchClient + .initIndex(getIndexName('prob-change-day-ascending')) + .search<CPMMContract>('', { + facetFilters: ['uniqueBettorIds:' + userId], + }) + ) + + if (!positiveData || !negativeData) { + return undefined + } + + return { + positiveChanges: positiveData.hits, + negativeChanges: negativeData.hits, + } +} export const useProbChanges = (userId: string) => { const { data: positiveChanges } = useFirestoreQueryData( diff --git a/web/pages/daily-movers.tsx b/web/pages/daily-movers.tsx index 1e5b4c48..a925a425 100644 --- a/web/pages/daily-movers.tsx +++ b/web/pages/daily-movers.tsx @@ -2,13 +2,13 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table' import { Col } from 'web/components/layout/col' import { Page } from 'web/components/page' import { Title } from 'web/components/title' -import { useProbChanges } from 'web/hooks/use-prob-changes' +import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes' import { useUser } from 'web/hooks/use-user' export default function DailyMovers() { const user = useUser() - const changes = useProbChanges(user?.id ?? '') + const changes = useProbChangesAlgolia(user?.id ?? '') return ( <Page> diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 17d55d56..69914e53 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -31,7 +31,7 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table' import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { formatMoney } from 'common/util/format' -import { useProbChanges } from 'web/hooks/use-prob-changes' +import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes' import { ProfitBadge } from 'web/components/bets-list' import { calculatePortfolioProfit } from 'common/calculate-metrics' import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal' @@ -243,7 +243,7 @@ function GroupSection(props: { function DailyMoversSection(props: { userId: string | null | undefined }) { const { userId } = props - const changes = useProbChanges(userId ?? '') + const changes = useProbChangesAlgolia(userId ?? '') return ( <Col className="gap-2"> From 42f66b11f470866411ec480072219d9d1dd06bfd Mon Sep 17 00:00:00 2001 From: jahooma <jahooma@users.noreply.github.com> Date: Sun, 18 Sep 2022 00:20:50 +0000 Subject: [PATCH 23/71] Auto-prettification --- web/lib/service/algolia.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/lib/service/algolia.ts b/web/lib/service/algolia.ts index 14c12fe5..3b6648a1 100644 --- a/web/lib/service/algolia.ts +++ b/web/lib/service/algolia.ts @@ -7,7 +7,8 @@ export const searchClient = algoliasearch( ) const indexPrefix = ENV === 'DEV' ? 'dev-' : '' -export const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' +export const searchIndexName = + ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' export const getIndexName = (sort: string) => { return `${indexPrefix}contracts-${sort}` From 3bddda37d25294ccdcb542f9d380146201dcf6da Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 19:25:17 -0500 Subject: [PATCH 24/71] Add plus to trending group button --- web/pages/home/index.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 69914e53..5e10bc3c 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -5,7 +5,7 @@ import { PencilAltIcon, ArrowSmRightIcon, } from '@heroicons/react/solid' -import { XCircleIcon } from '@heroicons/react/outline' +import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { toast, Toaster } from 'react-hot-toast' @@ -230,10 +230,7 @@ function GroupSection(props: { } }} > - <XCircleIcon - className={clsx('h-5 w-5 flex-shrink-0')} - aria-hidden="true" - /> + <XCircleIcon className={'h-5 w-5 flex-shrink-0'} aria-hidden="true" /> </Button> </SectionHeader> <ContractsGrid contracts={contracts} /> @@ -322,6 +319,7 @@ export function TrendingGroupsSection(props: { <Row className="flex-wrap gap-2"> {chosenGroups.map((g) => ( <PillButton + className="flex flex-row items-center gap-1" key={g.id} selected={memberGroupIds.includes(g.id)} onSelect={() => { @@ -343,6 +341,11 @@ export function TrendingGroupsSection(props: { } }} > + <PlusCircleIcon + className={'h-5 w-5 flex-shrink-0 text-gray-500'} + aria-hidden="true" + /> + {g.name} </PillButton> ))} From d8e9e7812a404327dc99525bc50ec85022b24a94 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 19:47:04 -0500 Subject: [PATCH 25/71] Don't show Daily movers if there are none. Threshold is 1% --- web/components/contract/prob-change-table.tsx | 23 +++++++++++-------- web/pages/home/index.tsx | 9 ++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 644946c9..6a47a7b7 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -20,18 +20,21 @@ export function ProbChangeTable(props: { const { positiveChanges, negativeChanges } = changes - const threshold = 0.075 - const countOverThreshold = Math.max( - positiveChanges.findIndex((c) => c.probChanges.day < threshold) + 1, - negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1 + const threshold = 0.01 + const positiveAboveThreshold = positiveChanges.filter( + (c) => c.probChanges.day > threshold ) - const maxRows = Math.min(positiveChanges.length, negativeChanges.length) - const rows = full - ? maxRows - : Math.min(3, Math.min(maxRows, countOverThreshold)) + const negativeAboveThreshold = negativeChanges.filter( + (c) => c.probChanges.day < threshold + ) + const maxRows = Math.min( + positiveAboveThreshold.length, + negativeAboveThreshold.length + ) + const rows = full ? maxRows : Math.min(3, maxRows) - const filteredPositiveChanges = positiveChanges.slice(0, rows) - const filteredNegativeChanges = negativeChanges.slice(0, rows) + const filteredPositiveChanges = positiveAboveThreshold.slice(0, rows) + const filteredNegativeChanges = negativeAboveThreshold.slice(0, rows) if (rows === 0) return <div className="px-4 text-gray-500">None</div> diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 5e10bc3c..fe27a2a0 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -242,6 +242,15 @@ function DailyMoversSection(props: { userId: string | null | undefined }) { const { userId } = props const changes = useProbChangesAlgolia(userId ?? '') + if (changes) { + const { positiveChanges, negativeChanges } = changes + if ( + !positiveChanges.find((c) => c.probChanges.day >= 0.01) || + !negativeChanges.find((c) => c.probChanges.day <= -0.01) + ) + return null + } + return ( <Col className="gap-2"> <SectionHeader label="Daily movers" href="/daily-movers" /> From 216616960841cbf6cba27cc36f14b1a851038107 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 19:56:20 -0500 Subject: [PATCH 26/71] Fix bug --- web/hooks/use-prob-changes.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/hooks/use-prob-changes.tsx b/web/hooks/use-prob-changes.tsx index 698111b8..a6f89a2a 100644 --- a/web/hooks/use-prob-changes.tsx +++ b/web/hooks/use-prob-changes.tsx @@ -30,8 +30,12 @@ export const useProbChangesAlgolia = (userId: string) => { } return { - positiveChanges: positiveData.hits, - negativeChanges: negativeData.hits, + positiveChanges: positiveData.hits.filter( + (c) => c && c.probChanges.day > 0 + ), + negativeChanges: negativeData.hits.filter( + (c) => c && c.probChanges.day < 0 + ), } } From 987274ad2dfbb778e3316e81a9a46f1e9c789962 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 20:01:00 -0500 Subject: [PATCH 27/71] Fix bug part 2 --- web/hooks/use-prob-changes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/hooks/use-prob-changes.tsx b/web/hooks/use-prob-changes.tsx index a6f89a2a..3e0e1a1a 100644 --- a/web/hooks/use-prob-changes.tsx +++ b/web/hooks/use-prob-changes.tsx @@ -31,10 +31,10 @@ export const useProbChangesAlgolia = (userId: string) => { return { positiveChanges: positiveData.hits.filter( - (c) => c && c.probChanges.day > 0 + (c) => c.probChanges && c.probChanges.day > 0 ), negativeChanges: negativeData.hits.filter( - (c) => c && c.probChanges.day < 0 + (c) => c.probChanges && c.probChanges.day < 0 ), } } From 4aea3b96d7b4fddabe49dadb2ea6e15953b35181 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 17 Sep 2022 23:58:18 -0500 Subject: [PATCH 28/71] Save initial home sections for new users --- web/pages/home/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index fe27a2a0..8246f2f1 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import React, { ReactNode, useEffect } from 'react' import Router from 'next/router' import { AdjustmentsIcon, @@ -56,6 +56,18 @@ export default function Home() { const { sections } = getHomeItems(groups, user?.homeSections ?? []) + useEffect(() => { + if ( + user && + !user.homeSections && + sections.length > 0 && + groups.length > 0 + ) { + // Save initial home sections. + updateUser(user.id, { homeSections: sections.map((s) => s.id) }) + } + }, [user, sections, groups]) + return ( <Page> <Toaster /> From eb021f30f5fd65e9a83254d275c0c0d3ca192b46 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 01:05:55 -0500 Subject: [PATCH 29/71] Fix loans (user without a portfolio throws error) --- functions/src/update-loans.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/functions/src/update-loans.ts b/functions/src/update-loans.ts index 770315fd..a3b8f60c 100644 --- a/functions/src/update-loans.ts +++ b/functions/src/update-loans.ts @@ -7,6 +7,7 @@ import { Contract } from '../../common/contract' import { PortfolioMetrics, User } from '../../common/user' import { getLoanUpdates } from '../../common/loans' import { createLoanIncomeNotification } from './create-notification' +import { filterDefined } from 'common/util/array' const firestore = admin.firestore() @@ -30,16 +31,18 @@ async function updateLoansCore() { log( `Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.` ) - const userPortfolios = await Promise.all( - users.map(async (user) => { - const portfolio = await getValues<PortfolioMetrics>( - firestore - .collection(`users/${user.id}/portfolioHistory`) - .orderBy('timestamp', 'desc') - .limit(1) - ) - return portfolio[0] - }) + const userPortfolios = filterDefined( + await Promise.all( + users.map(async (user) => { + const portfolio = await getValues<PortfolioMetrics>( + firestore + .collection(`users/${user.id}/portfolioHistory`) + .orderBy('timestamp', 'desc') + .limit(1) + ) + return portfolio[0] + }) + ) ) log(`Loaded ${userPortfolios.length} portfolios`) const portfolioByUser = keyBy(userPortfolios, (portfolio) => portfolio.userId) From 65166f2fcbf25e5d5cf1fbcd40f39e6f90c2837f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 01:10:34 -0500 Subject: [PATCH 30/71] Fix import --- functions/src/update-loans.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/update-loans.ts b/functions/src/update-loans.ts index a3b8f60c..c35a0613 100644 --- a/functions/src/update-loans.ts +++ b/functions/src/update-loans.ts @@ -7,7 +7,7 @@ import { Contract } from '../../common/contract' import { PortfolioMetrics, User } from '../../common/user' import { getLoanUpdates } from '../../common/loans' import { createLoanIncomeNotification } from './create-notification' -import { filterDefined } from 'common/util/array' +import { filterDefined } from '../../common/util/array' const firestore = admin.firestore() From 39119a3419a2c9a2023e4e6e610e635cfa47f82d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 18 Sep 2022 01:13:10 -0700 Subject: [PATCH 31/71] Twitch bot deployment work (#892) * Point at production Twitch bot endpoint * Move Twitch endpoints into env config --- common/envs/dev.ts | 2 ++ common/envs/prod.ts | 2 ++ web/lib/twitch/link-twitch-account.ts | 33 +++++++++++++++------------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 719de36e..96ec4dc2 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -16,4 +16,6 @@ export const DEV_CONFIG: EnvConfig = { cloudRunId: 'w3txbmd3ba', cloudRunRegion: 'uc', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', + // this is Phil's deployment + twitchBotEndpoint: 'https://king-prawn-app-5btyw.ondigitalocean.app', } diff --git a/common/envs/prod.ts b/common/envs/prod.ts index 73bd6029..3014f4e3 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -2,6 +2,7 @@ export type EnvConfig = { domain: string firebaseConfig: FirebaseConfig amplitudeApiKey?: string + twitchBotEndpoint?: string // IDs for v2 cloud functions -- find these by deploying a cloud function and // examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app @@ -66,6 +67,7 @@ export const PROD_CONFIG: EnvConfig = { appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', measurementId: 'G-SSFK1Q138D', }, + twitchBotEndpoint: 'https://twitch-bot-nggbo3neva-uc.a.run.app', cloudRunId: 'nggbo3neva', cloudRunRegion: 'uc', adminEmails: [ diff --git a/web/lib/twitch/link-twitch-account.ts b/web/lib/twitch/link-twitch-account.ts index f36a03b3..c68a781b 100644 --- a/web/lib/twitch/link-twitch-account.ts +++ b/web/lib/twitch/link-twitch-account.ts @@ -1,7 +1,6 @@ import { PrivateUser, User } from 'common/user' import { generateNewApiKey } from '../api/api-key' - -const TWITCH_BOT_PUBLIC_URL = 'https://king-prawn-app-5btyw.ondigitalocean.app' // TODO: Add this to env config appropriately +import { ENV_CONFIG } from 'common/envs/constants' async function postToBot(url: string, body: unknown) { const result = await fetch(url, { @@ -21,13 +20,16 @@ export async function initLinkTwitchAccount( manifoldUserID: string, manifoldUserAPIKey: string ): Promise<[string, Promise<{ twitchName: string; controlToken: string }>]> { - const response = await postToBot(`${TWITCH_BOT_PUBLIC_URL}/api/linkInit`, { - manifoldID: manifoldUserID, - apiKey: manifoldUserAPIKey, - redirectURL: window.location.href, - }) + const response = await postToBot( + `${ENV_CONFIG.twitchBotEndpoint}/api/linkInit`, + { + manifoldID: manifoldUserID, + apiKey: manifoldUserAPIKey, + redirectURL: window.location.href, + } + ) const responseFetch = fetch( - `${TWITCH_BOT_PUBLIC_URL}/api/linkResult?userID=${manifoldUserID}` + `${ENV_CONFIG.twitchBotEndpoint}/api/linkResult?userID=${manifoldUserID}` ) return [response.twitchAuthURL, responseFetch.then((r) => r.json())] } @@ -50,15 +52,18 @@ export async function updateBotEnabledForUser( botEnabled: boolean ) { if (botEnabled) { - return postToBot(`${TWITCH_BOT_PUBLIC_URL}/registerchanneltwitch`, { + return postToBot(`${ENV_CONFIG.twitchBotEndpoint}/registerchanneltwitch`, { apiKey: privateUser.apiKey, }).then((r) => { if (!r.success) throw new Error(r.message) }) } else { - return postToBot(`${TWITCH_BOT_PUBLIC_URL}/unregisterchanneltwitch`, { - apiKey: privateUser.apiKey, - }).then((r) => { + return postToBot( + `${ENV_CONFIG.twitchBotEndpoint}/unregisterchanneltwitch`, + { + apiKey: privateUser.apiKey, + } + ).then((r) => { if (!r.success) throw new Error(r.message) }) } @@ -66,10 +71,10 @@ export async function updateBotEnabledForUser( export function getOverlayURLForUser(privateUser: PrivateUser) { const controlToken = privateUser?.twitchInfo?.controlToken - return `${TWITCH_BOT_PUBLIC_URL}/overlay?t=${controlToken}` + return `${ENV_CONFIG.twitchBotEndpoint}/overlay?t=${controlToken}` } export function getDockURLForUser(privateUser: PrivateUser) { const controlToken = privateUser?.twitchInfo?.controlToken - return `${TWITCH_BOT_PUBLIC_URL}/dock?t=${controlToken}` + return `${ENV_CONFIG.twitchBotEndpoint}/dock?t=${controlToken}` } From c9e782faa711f2e454ec908970db3665d1ab3ee0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 13:49:24 -0500 Subject: [PATCH 32/71] Simplify create group dialog --- web/components/groups/create-group-button.tsx | 60 +++++-------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/web/components/groups/create-group-button.tsx b/web/components/groups/create-group-button.tsx index 435dc741..7c7b6f1e 100644 --- a/web/components/groups/create-group-button.tsx +++ b/web/components/groups/create-group-button.tsx @@ -6,7 +6,6 @@ import { ConfirmationButton } from '../confirmation-button' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { Title } from '../title' -import { FilterSelectUsers } from 'web/components/filter-select-users' import { User } from 'common/user' import { MAX_GROUP_NAME_LENGTH } from 'common/group' import { createGroup } from 'web/lib/firebase/api' @@ -21,31 +20,18 @@ export function CreateGroupButton(props: { }) { const { user, className, label, onOpenStateChange, goToGroupOnSubmit, icon } = props - const [defaultName, setDefaultName] = useState(`${user.name}'s group`) + const [name, setName] = useState('') - const [memberUsers, setMemberUsers] = useState<User[]>([]) const [isSubmitting, setIsSubmitting] = useState(false) const [errorText, setErrorText] = useState('') const router = useRouter() - function updateMemberUsers(users: User[]) { - const usersFirstNames = users.map((user) => user.name.split(' ')[0]) - const postFix = - usersFirstNames.length > 3 ? ` & ${usersFirstNames.length - 3} more` : '' - const newName = `${user.name.split(' ')[0]}${ - users.length > 0 ? ', ' + usersFirstNames.slice(0, 3).join(', ') : '' - }${postFix}'s group` - setDefaultName(newName) - setMemberUsers(users) - } - const onSubmit = async () => { setIsSubmitting(true) - const groupName = name !== '' ? name : defaultName const newGroup = { - name: groupName, - memberIds: memberUsers.map((user) => user.id), + name, + memberIds: [], anyoneCanJoin: true, } const result = await createGroup(newGroup).catch((e) => { @@ -62,7 +48,6 @@ export function CreateGroupButton(props: { console.log(result.details) if (result.group) { - updateMemberUsers([]) if (goToGroupOnSubmit) router.push(groupPath(result.group.slug)).catch((e) => { console.log(e) @@ -99,41 +84,26 @@ export function CreateGroupButton(props: { onSubmitWithSuccess={onSubmit} onOpenChanged={(isOpen) => { onOpenStateChange?.(isOpen) - updateMemberUsers([]) setName('') }} > <Title className="!my-0" text="Create a group" /> <Col className="gap-1 text-gray-500"> - <div>You can add markets and members to your group after creation.</div> + <div>You can add markets to your group after creation.</div> </Col> - <div className={'text-error'}>{errorText}</div> + {errorText && <div className={'text-error'}>{errorText}</div>} - <div> - <div className="form-control w-full"> - <label className="label"> - <span className="mb-0">Add members (optional)</span> - </label> - <FilterSelectUsers - setSelectedUsers={updateMemberUsers} - selectedUsers={memberUsers} - ignoreUserIds={[user.id]} - /> - </div> - <div className="form-control w-full"> - <label className="label"> - <span className="mt-1">Group name (optional)</span> - </label> - <input - placeholder={defaultName} - className="input input-bordered resize-none" - disabled={isSubmitting} - value={name} - maxLength={MAX_GROUP_NAME_LENGTH} - onChange={(e) => setName(e.target.value || '')} - /> - </div> + <div className="form-control w-full"> + <label className="mb-2 ml-1 mt-0">Group name</label> + <input + placeholder={'Your group name'} + className="input input-bordered resize-none" + disabled={isSubmitting} + value={name} + maxLength={MAX_GROUP_NAME_LENGTH} + onChange={(e) => setName(e.target.value || '')} + /> <Spacer h={4} /> </div> From 1da4373335cec4e99dfed736502d7826e5596f1c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 14:24:29 -0500 Subject: [PATCH 33/71] Creating a group from create market adds it immediately --- web/components/groups/create-group-button.tsx | 18 ++++++++++++++++-- web/components/groups/group-selector.tsx | 4 ++-- web/hooks/use-warn-unsaved-changes.ts | 8 ++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/web/components/groups/create-group-button.tsx b/web/components/groups/create-group-button.tsx index 7c7b6f1e..e0324c4e 100644 --- a/web/components/groups/create-group-button.tsx +++ b/web/components/groups/create-group-button.tsx @@ -16,10 +16,18 @@ export function CreateGroupButton(props: { label?: string onOpenStateChange?: (isOpen: boolean) => void goToGroupOnSubmit?: boolean + addGroupIdParamOnSubmit?: boolean icon?: JSX.Element }) { - const { user, className, label, onOpenStateChange, goToGroupOnSubmit, icon } = - props + const { + user, + className, + label, + onOpenStateChange, + goToGroupOnSubmit, + addGroupIdParamOnSubmit, + icon, + } = props const [name, setName] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) @@ -53,6 +61,12 @@ export function CreateGroupButton(props: { console.log(e) setErrorText(e.message) }) + else if (addGroupIdParamOnSubmit) { + router.replace({ + pathname: router.pathname, + query: { ...router.query, groupId: result.group.id }, + }) + } setIsSubmitting(false) return true } else { diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index 54fc0764..a04a91af 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -131,7 +131,7 @@ export function GroupSelector(props: { )} <span className={clsx( - 'ml-3 mt-1 block flex flex-row justify-between', + 'ml-3 mt-1 flex flex-row justify-between', selected && 'font-semibold' )} > @@ -166,7 +166,7 @@ export function GroupSelector(props: { 'w-full justify-start rounded-none border-0 bg-white pl-2 font-normal text-gray-900 hover:bg-indigo-500 hover:text-white' } label={'Create a new Group'} - goToGroupOnSubmit={false} + addGroupIdParamOnSubmit icon={ <PlusCircleIcon className="text-primary mr-2 h-5 w-5" /> } diff --git a/web/hooks/use-warn-unsaved-changes.ts b/web/hooks/use-warn-unsaved-changes.ts index b871b8b2..53620aae 100644 --- a/web/hooks/use-warn-unsaved-changes.ts +++ b/web/hooks/use-warn-unsaved-changes.ts @@ -13,7 +13,11 @@ export const useWarnUnsavedChanges = (hasUnsavedChanges: boolean) => { return confirmationMessage } - const beforeRouteHandler = () => { + const beforeRouteHandler = (href: string) => { + const pathname = href.split('?')[0] + // Don't warn if the user is navigating to the same page. + if (pathname === location.pathname) return + if (!confirm(confirmationMessage)) { Router.events.emit('routeChangeError') throw 'Abort route change. Please ignore this error.' @@ -21,7 +25,7 @@ export const useWarnUnsavedChanges = (hasUnsavedChanges: boolean) => { } window.addEventListener('beforeunload', warnUnsavedChanges) - Router.events.on('routeChangeStart', beforeRouteHandler) + Router.events.on('routeChangeStart', (href) => beforeRouteHandler(href)) return () => { window.removeEventListener('beforeunload', warnUnsavedChanges) From 676bcc159dd557e1eb10ebb33107f4b69b30b28b Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 15:53:04 -0500 Subject: [PATCH 34/71] Fix missing key --- web/components/notification-settings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index 7c1f3546..047d15dd 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -276,6 +276,7 @@ export function NotificationSettings(props: { <Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}> {subscriptionTypes.map((subType) => ( <NotificationSettingLine + key={subType} subscriptionTypeKey={subType as notification_preference} destinations={getUsersSavedPreference( subType as notification_preference From f111d6e24fbae91739365fdebdc093c422b9d907 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 15:55:39 -0500 Subject: [PATCH 35/71] Fix console errors from svg non-camelcase attributes --- web/lib/icons/bold-icon.tsx | 6 +++--- web/lib/icons/italic-icon.tsx | 6 +++--- web/lib/icons/link-icon.tsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/lib/icons/bold-icon.tsx b/web/lib/icons/bold-icon.tsx index f4fec497..eb7c68e2 100644 --- a/web/lib/icons/bold-icon.tsx +++ b/web/lib/icons/bold-icon.tsx @@ -8,9 +8,9 @@ export default function BoldIcon(props: React.SVGProps<SVGSVGElement>) { viewBox="0 0 24 24" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" {...props} > <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path> diff --git a/web/lib/icons/italic-icon.tsx b/web/lib/icons/italic-icon.tsx index d412ed77..f0fb5883 100644 --- a/web/lib/icons/italic-icon.tsx +++ b/web/lib/icons/italic-icon.tsx @@ -8,9 +8,9 @@ export default function ItalicIcon(props: React.SVGProps<SVGSVGElement>) { viewBox="0 0 24 24" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" {...props} > <line x1="19" y1="4" x2="10" y2="4"></line> diff --git a/web/lib/icons/link-icon.tsx b/web/lib/icons/link-icon.tsx index 6323344c..eb96e737 100644 --- a/web/lib/icons/link-icon.tsx +++ b/web/lib/icons/link-icon.tsx @@ -8,9 +8,9 @@ export default function LinkIcon(props: React.SVGProps<SVGSVGElement>) { viewBox="0 0 24 24" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" {...props} > <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> From 540915eb6596f9deb8a6c4345206ea45dba43874 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 18 Sep 2022 16:05:13 -0500 Subject: [PATCH 36/71] homepage: fix betting streaks error --- web/pages/home/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 8246f2f1..83bcb15b 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -281,8 +281,8 @@ function DailyStats(props: { const [first, last] = [metrics[0], metrics[metrics.length - 1]] const privateUser = usePrivateUser() - const streaksHidden = - privateUser?.notificationPreferences.betting_streaks.length === 0 + const streaks = privateUser?.notificationPreferences?.betting_streaks ?? [] + const streaksHidden = streaks.length === 0 let profit = 0 let profitPercent = 0 From 373cfc5d10e9c3573f0942b939a4bb4e2df45758 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 16:23:07 -0500 Subject: [PATCH 37/71] Format firestore /group rules --- firestore.rules | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/firestore.rules b/firestore.rules index 08214b10..79ae9f96 100644 --- a/firestore.rules +++ b/firestore.rules @@ -171,33 +171,32 @@ service cloud.firestore { allow read; } - match /groups/{groupId} { - allow read; - allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) - && request.resource.data.diff(resource.data) - .affectedKeys() - .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId' ]); - allow delete: if request.auth.uid == resource.data.creatorId; + match /groups/{groupId} { + allow read; + allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) + && request.resource.data.diff(resource.data) + .affectedKeys() + .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId' ]); + allow delete: if request.auth.uid == resource.data.creatorId; - match /groupContracts/{contractId} { - allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId - } + match /groupContracts/{contractId} { + allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId + } - match /groupMembers/{memberId}{ - allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin); - allow delete: if request.auth.uid == resource.data.userId; - } + match /groupMembers/{memberId}{ + allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin); + allow delete: if request.auth.uid == resource.data.userId; + } - function isGroupMember() { - return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid)); - } + function isGroupMember() { + return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid)); + } - match /comments/{commentId} { - allow read; - allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember(); - } - - } + match /comments/{commentId} { + allow read; + allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember(); + } + } match /posts/{postId} { allow read; From ae6437442b5547d3ca93f7c2dbbc33b32d93ea1e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 16:36:45 -0500 Subject: [PATCH 38/71] Fix unsaved changes warning erronously appearing --- web/hooks/use-warn-unsaved-changes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/hooks/use-warn-unsaved-changes.ts b/web/hooks/use-warn-unsaved-changes.ts index 53620aae..04f6a59a 100644 --- a/web/hooks/use-warn-unsaved-changes.ts +++ b/web/hooks/use-warn-unsaved-changes.ts @@ -25,7 +25,7 @@ export const useWarnUnsavedChanges = (hasUnsavedChanges: boolean) => { } window.addEventListener('beforeunload', warnUnsavedChanges) - Router.events.on('routeChangeStart', (href) => beforeRouteHandler(href)) + Router.events.on('routeChangeStart', beforeRouteHandler) return () => { window.removeEventListener('beforeunload', warnUnsavedChanges) From 17453e56181a524f7be5165a889e808c98dbf389 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 16:57:20 -0500 Subject: [PATCH 39/71] Improve hook that was spamming in dev --- web/hooks/use-group.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 59b36b27..9bcb59cd 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -105,16 +105,19 @@ export const useMemberGroupIds = (user: User | null | undefined) => { export function useMemberGroupsSubscription(user: User | null | undefined) { const cachedGroups = useMemberGroups(user?.id) ?? [] - const groupIds = useMemberGroupIds(user) const [groups, setGroups] = useState(cachedGroups) + const userId = user?.id useEffect(() => { - if (groupIds) { - Promise.all(groupIds.map((id) => getGroup(id))).then((groups) => - setGroups(filterDefined(groups)) - ) + if (userId) { + return listenForMemberGroupIds(userId, (groupIds) => { + Promise.all(groupIds.map((id) => getGroup(id))).then((groups) => + setGroups(filterDefined(groups)) + ) + }) } - }, [groupIds]) + }, [userId]) + return groups } From 56b4889b94ed2df4a019537c7eca1d8f01c7cd2f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 17:00:35 -0500 Subject: [PATCH 40/71] Add user.homeSections to firestore rules --- firestore.rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firestore.rules b/firestore.rules index 79ae9f96..a9ef98e5 100644 --- a/firestore.rules +++ b/firestore.rules @@ -27,7 +27,7 @@ service cloud.firestore { allow read; allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']); // User referral rules allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() From 8ebf8291696d20e2e33e04649878f85539bc8c1a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 18 Sep 2022 17:16:13 -0500 Subject: [PATCH 41/71] Change groups default sort to Trending --- web/pages/group/[...slugs]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 73186e92..bb889e72 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -211,7 +211,7 @@ export default function GroupPage(props: { <ContractSearch headerClassName="md:sticky" user={user} - defaultSort={'newest'} + defaultSort={'score'} defaultFilter={suggestedFilter} additionalFilter={{ groupSlug: group.slug }} persistPrefix={`group-${group.slug}`} From e37b805b4958ca2aec7550ef051e3be71e653ae8 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 18 Sep 2022 17:49:29 -0500 Subject: [PATCH 42/71] disable liquidity bonus (for now) --- functions/src/on-create-bet.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index ce75f0fe..54f7a7f4 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -24,8 +24,6 @@ import { } from '../../common/antes' import { APIError } from '../../common/api' import { User } from '../../common/user' -import { UNIQUE_BETTOR_LIQUIDITY_AMOUNT } from '../../common/antes' -import { addHouseLiquidity } from './add-liquidity' import { DAY_MS } from '../../common/util/time' import { BettingStreakBonusTxn, UniqueBettorBonusTxn } from '../../common/txn' @@ -184,10 +182,6 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( // No need to give a bonus for the creator's bet if (!isNewUniqueBettor || bettor.id == contract.creatorId) return - if (contract.mechanism === 'cpmm-1') { - await addHouseLiquidity(contract, UNIQUE_BETTOR_LIQUIDITY_AMOUNT) - } - // Create combined txn for all new unique bettors const bonusTxnDetails = { contractId: contract.id, From 58dcbaaf6e23dee75c3f40106a37d19a3b642d85 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 18 Sep 2022 15:57:50 -0700 Subject: [PATCH 43/71] Precalculate and store current positions for users who make comments (#878) --- common/calculate.ts | 42 ++++++- common/comment.ts | 5 + .../src/on-create-comment-on-contract.ts | 28 +++++ .../scripts/backfill-comment-position-data.ts | 92 +++++++++++++++ .../contract/contract-leaderboard.tsx | 1 - web/components/contract/contract-tabs.tsx | 8 +- web/components/feed/contract-activity.tsx | 16 +-- .../feed/feed-answer-comment-group.tsx | 6 +- web/components/feed/feed-comments.tsx | 111 ++++-------------- 9 files changed, 204 insertions(+), 105 deletions(-) create mode 100644 functions/src/scripts/backfill-comment-position-data.ts diff --git a/common/calculate.ts b/common/calculate.ts index e4c9ed07..5edf1211 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,4 +1,4 @@ -import { maxBy, sortBy, sum, sumBy } from 'lodash' +import { maxBy, partition, sortBy, sum, sumBy } from 'lodash' import { Bet, LimitBet } from './bet' import { calculateCpmmSale, @@ -255,3 +255,43 @@ export function getTopAnswer( ) return top?.answer } + +export function getLargestPosition(contract: Contract, userBets: Bet[]) { + let yesFloorShares = 0, + yesShares = 0, + noShares = 0, + noFloorShares = 0 + + if (userBets.length === 0) { + return null + } + if (contract.outcomeType === 'FREE_RESPONSE') { + const answerCounts: { [outcome: string]: number } = {} + for (const bet of userBets) { + if (bet.outcome) { + if (!answerCounts[bet.outcome]) { + answerCounts[bet.outcome] = bet.amount + } else { + answerCounts[bet.outcome] += bet.amount + } + } + } + const majorityAnswer = + maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? '' + return { + prob: undefined, + shares: answerCounts[majorityAnswer] || 0, + outcome: majorityAnswer, + } + } + + const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES') + yesShares = sumBy(yesBets, (bet) => bet.shares) + noShares = sumBy(noBets, (bet) => bet.shares) + yesFloorShares = Math.floor(yesShares) + noFloorShares = Math.floor(noShares) + + const shares = yesFloorShares || noFloorShares + const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO' + return { shares, outcome } +} diff --git a/common/comment.ts b/common/comment.ts index 7ecbb6d4..cdb62fd3 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -33,6 +33,11 @@ export type OnContract = { // denormalized from bet betAmount?: number betOutcome?: string + + // denormalized based on betting history + commenterPositionProb?: number // binary only + commenterPositionShares?: number + commenterPositionOutcome?: string } export type OnGroup = { diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 65e32dca..6bb568ff 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -5,6 +5,8 @@ import { getContract, getUser, getValues } from './utils' import { ContractComment } from '../../common/comment' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' +import { getLargestPosition } from '../../common/calculate' +import { maxBy } from 'lodash' import { createCommentOrAnswerOrUpdatedContractNotification, replied_users_info, @@ -45,6 +47,32 @@ export const onCreateCommentOnContract = functions .doc(contract.id) .update({ lastCommentTime, lastUpdatedTime: Date.now() }) + const previousBetsQuery = await firestore + .collection('contracts') + .doc(contractId) + .collection('bets') + .where('createdTime', '<', comment.createdTime) + .get() + const previousBets = previousBetsQuery.docs.map((d) => d.data() as Bet) + const position = getLargestPosition( + contract, + previousBets.filter((b) => b.userId === comment.userId && !b.isAnte) + ) + if (position) { + const fields: { [k: string]: unknown } = { + commenterPositionShares: position.shares, + commenterPositionOutcome: position.outcome, + } + const previousProb = + contract.outcomeType === 'BINARY' + ? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter + : undefined + if (previousProb != null) { + fields.commenterPositionProb = previousProb + } + await change.ref.update(fields) + } + let bet: Bet | undefined let answer: Answer | undefined if (comment.answerOutcome) { diff --git a/functions/src/scripts/backfill-comment-position-data.ts b/functions/src/scripts/backfill-comment-position-data.ts new file mode 100644 index 00000000..eab54c55 --- /dev/null +++ b/functions/src/scripts/backfill-comment-position-data.ts @@ -0,0 +1,92 @@ +// Filling in historical bet positions on comments. + +// Warning: This just recalculates all of them, rather than trying to +// figure out which ones are out of date, since I'm using it to fill them +// in once in the first place. + +import { maxBy } from 'lodash' +import * as admin from 'firebase-admin' +import { filterDefined } from '../../../common/util/array' +import { Bet } from '../../../common/bet' +import { Comment } from '../../../common/comment' +import { Contract } from '../../../common/contract' +import { getLargestPosition } from '../../../common/calculate' +import { initAdmin } from './script-init' +import { DocumentSnapshot } from 'firebase-admin/firestore' +import { log, writeAsync } from '../utils' + +initAdmin() +const firestore = admin.firestore() + +async function getContractsById() { + const contracts = await firestore.collection('contracts').get() + const results = Object.fromEntries( + contracts.docs.map((doc) => [doc.id, doc.data() as Contract]) + ) + log(`Found ${contracts.size} contracts.`) + return results +} + +async function getCommentsByContractId() { + const comments = await firestore + .collectionGroup('comments') + .where('contractId', '!=', null) + .get() + const results = new Map<string, DocumentSnapshot[]>() + comments.forEach((doc) => { + const contractId = doc.get('contractId') + const contractComments = results.get(contractId) || [] + contractComments.push(doc) + results.set(contractId, contractComments) + }) + log(`Found ${comments.size} comments on ${results.size} contracts.`) + return results +} + +// not in a transaction for speed -- may need to be run more than once +async function denormalize() { + const contractsById = await getContractsById() + const commentsByContractId = await getCommentsByContractId() + for (const [contractId, comments] of commentsByContractId.entries()) { + const betsQuery = await firestore + .collection('contracts') + .doc(contractId) + .collection('bets') + .get() + log(`Loaded ${betsQuery.size} bets for contract ${contractId}.`) + const bets = betsQuery.docs.map((d) => d.data() as Bet) + const updates = comments.map((doc) => { + const comment = doc.data() as Comment + const contract = contractsById[contractId] + const previousBets = bets.filter( + (b) => b.createdTime < comment.createdTime + ) + const position = getLargestPosition( + contract, + previousBets.filter((b) => b.userId === comment.userId && !b.isAnte) + ) + if (position) { + const fields: { [k: string]: unknown } = { + commenterPositionShares: position.shares, + commenterPositionOutcome: position.outcome, + } + const previousProb = + contract.outcomeType === 'BINARY' + ? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter + : undefined + if (previousProb != null) { + fields.commenterPositionProb = previousProb + } + return { doc: doc.ref, fields } + } else { + return undefined + } + }) + log(`Updating ${updates.length} comments.`) + await writeAsync(firestore, filterDefined(updates)) + } +} + +if (require.main === module) { + denormalize().catch((e) => console.error(e)) +} diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index fec6744d..a863f1bf 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -106,7 +106,6 @@ export function ContractTopTrades(props: { contract={contract} comment={commentsById[topCommentId]} tips={tips[topCommentId]} - betsBySameUser={[betsById[topCommentId]]} /> </div> <Spacer h={16} /> diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index e1ee141e..939eb624 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -85,7 +85,9 @@ export function ContractTabs(props: { <div className={'mb-4 w-full border-b border-gray-200'} /> <ContractCommentsActivity contract={contract} - bets={generalBets} + betsByCurrentUser={ + user ? generalBets.filter((b) => b.userId === user.id) : [] + } comments={generalComments} tips={tips} user={user} @@ -95,7 +97,9 @@ export function ContractTabs(props: { ) : ( <ContractCommentsActivity contract={contract} - bets={visibleBets} + betsByCurrentUser={ + user ? visibleBets.filter((b) => b.userId === user.id) : [] + } comments={comments} tips={tips} user={user} diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index b8a003fa..b7789308 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -73,13 +73,12 @@ export function ContractBetsActivity(props: { export function ContractCommentsActivity(props: { contract: Contract - bets: Bet[] + betsByCurrentUser: Bet[] comments: ContractComment[] tips: CommentTipMap user: User | null | undefined }) { - const { bets, contract, comments, user, tips } = props - const betsByUserId = groupBy(bets, (bet) => bet.userId) + const { betsByCurrentUser, contract, comments, user, tips } = props const commentsByUserId = groupBy(comments, (c) => c.userId) const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_') const topLevelComments = sortBy( @@ -92,7 +91,7 @@ export function ContractCommentsActivity(props: { <ContractCommentInput className="mb-5" contract={contract} - betsByCurrentUser={(user && betsByUserId[user.id]) ?? []} + betsByCurrentUser={betsByCurrentUser} commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []} /> {topLevelComments.map((parent) => ( @@ -106,8 +105,7 @@ export function ContractCommentsActivity(props: { (c) => c.createdTime )} tips={tips} - bets={bets} - betsByUserId={betsByUserId} + betsByCurrentUser={betsByCurrentUser} commentsByUserId={commentsByUserId} /> ))} @@ -136,7 +134,9 @@ export function FreeResponseContractCommentsActivity(props: { }) .filter((answer) => answer != null) - const betsByUserId = groupBy(bets, (bet) => bet.userId) + const betsByCurrentUser = user + ? bets.filter((bet) => bet.userId === user.id) + : [] const commentsByUserId = groupBy(comments, (c) => c.userId) const commentsByOutcome = groupBy(comments, (c) => c.answerOutcome ?? '_') @@ -157,7 +157,7 @@ export function FreeResponseContractCommentsActivity(props: { (c) => c.createdTime )} tips={tips} - betsByUserId={betsByUserId} + betsByCurrentUser={betsByCurrentUser} commentsByUserId={commentsByUserId} /> </div> diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 0535ac33..958b6d6d 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -27,7 +27,7 @@ export function FeedAnswerCommentGroup(props: { answer: Answer answerComments: ContractComment[] tips: CommentTipMap - betsByUserId: Dictionary<Bet[]> + betsByCurrentUser: Bet[] commentsByUserId: Dictionary<ContractComment[]> }) { const { @@ -35,7 +35,7 @@ export function FeedAnswerCommentGroup(props: { contract, answerComments, tips, - betsByUserId, + betsByCurrentUser, commentsByUserId, user, } = props @@ -48,7 +48,6 @@ export function FeedAnswerCommentGroup(props: { const router = useRouter() const answerElementId = `answer-${answer.id}` - const betsByCurrentUser = (user && betsByUserId[user.id]) ?? [] const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? [] const isFreeResponseContractPage = !!commentsByCurrentUser const mostRecentCommentableBet = getMostRecentCommentableBet( @@ -166,7 +165,6 @@ export function FeedAnswerCommentGroup(props: { contract={contract} comment={comment} tips={tips[comment.id]} - betsBySameUser={betsByUserId[comment.userId] ?? []} onReplyClick={scrollAndOpenReplyInput} /> ))} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index f9cb205c..0eca8915 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -3,7 +3,7 @@ import { ContractComment } from 'common/comment' import { User } from 'common/user' import { Contract } from 'common/contract' import React, { useEffect, useState } from 'react' -import { minBy, maxBy, partition, sumBy, Dictionary } from 'lodash' +import { Dictionary } from 'lodash' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { useRouter } from 'next/router' @@ -29,8 +29,7 @@ export function FeedCommentThread(props: { threadComments: ContractComment[] tips: CommentTipMap parentComment: ContractComment - bets: Bet[] - betsByUserId: Dictionary<Bet[]> + betsByCurrentUser: Bet[] commentsByUserId: Dictionary<ContractComment[]> }) { const { @@ -38,8 +37,7 @@ export function FeedCommentThread(props: { contract, threadComments, commentsByUserId, - bets, - betsByUserId, + betsByCurrentUser, tips, parentComment, } = props @@ -64,17 +62,7 @@ export function FeedCommentThread(props: { contract={contract} comment={comment} tips={tips[comment.id]} - betsBySameUser={betsByUserId[comment.userId] ?? []} onReplyClick={scrollAndOpenReplyInput} - probAtCreatedTime={ - contract.outcomeType === 'BINARY' - ? minBy(bets, (bet) => { - return bet.createdTime < comment.createdTime - ? comment.createdTime - bet.createdTime - : comment.createdTime - })?.probAfter - : undefined - } /> ))} {showReply && ( @@ -85,7 +73,7 @@ export function FeedCommentThread(props: { /> <ContractCommentInput contract={contract} - betsByCurrentUser={(user && betsByUserId[user.id]) ?? []} + betsByCurrentUser={(user && betsByCurrentUser) ?? []} commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []} parentCommentId={parentComment.id} replyToUser={replyTo} @@ -104,22 +92,21 @@ export function FeedComment(props: { contract: Contract comment: ContractComment tips: CommentTips - betsBySameUser: Bet[] indent?: boolean - probAtCreatedTime?: number onReplyClick?: (comment: ContractComment) => void }) { + const { contract, comment, tips, indent, onReplyClick } = props const { - contract, - comment, - tips, - betsBySameUser, - indent, - probAtCreatedTime, - onReplyClick, - } = props - const { text, content, userUsername, userName, userAvatarUrl, createdTime } = - comment + text, + content, + userUsername, + userName, + userAvatarUrl, + commenterPositionProb, + commenterPositionShares, + commenterPositionOutcome, + createdTime, + } = comment const betOutcome = comment.betOutcome let bought: string | undefined let money: string | undefined @@ -136,13 +123,6 @@ export function FeedComment(props: { } }, [comment.id, router.asPath]) - // Only calculated if they don't have a matching bet - const { userPosition, outcome } = getBettorsLargestPositionBeforeTime( - contract, - comment.createdTime, - comment.betId ? [] : betsBySameUser - ) - return ( <Row id={comment.id} @@ -167,14 +147,17 @@ export function FeedComment(props: { username={userUsername} name={userName} />{' '} - {!comment.betId != null && - userPosition > 0 && + {comment.betId == null && + commenterPositionProb != null && + commenterPositionOutcome != null && + commenterPositionShares != null && + commenterPositionShares > 0 && contract.outcomeType !== 'NUMERIC' && ( <> {'is '} <CommentStatus - prob={probAtCreatedTime} - outcome={outcome} + prob={commenterPositionProb} + outcome={commenterPositionOutcome} contract={contract} /> </> @@ -310,56 +293,6 @@ export function ContractCommentInput(props: { ) } -function getBettorsLargestPositionBeforeTime( - contract: Contract, - createdTime: number, - bets: Bet[] -) { - let yesFloorShares = 0, - yesShares = 0, - noShares = 0, - noFloorShares = 0 - - const previousBets = bets.filter( - (prevBet) => prevBet.createdTime < createdTime && !prevBet.isAnte - ) - - if (contract.outcomeType === 'FREE_RESPONSE') { - const answerCounts: { [outcome: string]: number } = {} - for (const bet of previousBets) { - if (bet.outcome) { - if (!answerCounts[bet.outcome]) { - answerCounts[bet.outcome] = bet.amount - } else { - answerCounts[bet.outcome] += bet.amount - } - } - } - const majorityAnswer = - maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? '' - return { - userPosition: answerCounts[majorityAnswer] || 0, - outcome: majorityAnswer, - } - } - if (bets.length === 0) { - return { userPosition: 0, outcome: '' } - } - - const [yesBets, noBets] = partition( - previousBets ?? [], - (bet) => bet.outcome === 'YES' - ) - yesShares = sumBy(yesBets, (bet) => bet.shares) - noShares = sumBy(noBets, (bet) => bet.shares) - yesFloorShares = Math.floor(yesShares) - noFloorShares = Math.floor(noShares) - - const userPosition = yesFloorShares || noFloorShares - const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO' - return { userPosition, outcome } -} - function canCommentOnBet(bet: Bet, user?: User | null) { const { userId, createdTime, isRedemption } = bet const isSelf = user?.id === userId From a9e5020904f8356f95356b7cf9c68979b70c72d7 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 18 Sep 2022 19:16:48 -0700 Subject: [PATCH 44/71] Render free response comment threads more simply without bets (#893) --- web/components/contract/contract-tabs.tsx | 4 +++- web/components/feed/contract-activity.tsx | 26 ++++++----------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 939eb624..b5895f60 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -75,7 +75,9 @@ export function ContractTabs(props: { <> <FreeResponseContractCommentsActivity contract={contract} - bets={visibleBets} + betsByCurrentUser={ + user ? visibleBets.filter((b) => b.userId === user.id) : [] + } comments={comments} tips={tips} user={user} diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index b7789308..fc817082 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Contract, FreeResponseContract } from 'common/contract' import { ContractComment } from 'common/comment' -import { Answer } from 'common/answer' import { Bet } from 'common/bet' import { getOutcomeProbability } from 'common/calculate' import { Pagination } from 'web/components/pagination' @@ -12,7 +11,7 @@ import { FeedCommentThread, ContractCommentInput } from './feed-comments' import { User } from 'common/user' import { CommentTipMap } from 'web/hooks/use-tip-txns' import { LiquidityProvision } from 'common/liquidity-provision' -import { groupBy, sortBy, uniq } from 'lodash' +import { groupBy, sortBy } from 'lodash' import { Col } from 'web/components/layout/col' export function ContractBetsActivity(props: { @@ -115,34 +114,23 @@ export function ContractCommentsActivity(props: { export function FreeResponseContractCommentsActivity(props: { contract: FreeResponseContract - bets: Bet[] + betsByCurrentUser: Bet[] comments: ContractComment[] tips: CommentTipMap user: User | null | undefined }) { - const { bets, contract, comments, user, tips } = props + const { betsByCurrentUser, contract, comments, user, tips } = props - let outcomes = uniq(bets.map((bet) => bet.outcome)) - outcomes = sortBy( - outcomes, - (outcome) => -getOutcomeProbability(contract, outcome) + const sortedAnswers = sortBy( + contract.answers, + (answer) => -getOutcomeProbability(contract, answer.number.toString()) ) - - const answers = outcomes - .map((outcome) => { - return contract.answers.find((answer) => answer.id === outcome) as Answer - }) - .filter((answer) => answer != null) - - const betsByCurrentUser = user - ? bets.filter((bet) => bet.userId === user.id) - : [] const commentsByUserId = groupBy(comments, (c) => c.userId) const commentsByOutcome = groupBy(comments, (c) => c.answerOutcome ?? '_') return ( <> - {answers.map((answer) => ( + {sortedAnswers.map((answer) => ( <div key={answer.id} className={'relative pb-4'}> <span className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" From b93af31d2f666227e259e4e25ed35e671085a80e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 19 Sep 2022 01:28:18 -0500 Subject: [PATCH 45/71] Add D1 and W1 (new users) to stats --- common/stats.ts | 3 + functions/src/update-stats.ts | 108 +++++++++++++++++++++++----------- web/pages/stats.tsx | 81 +++++++++++++++++-------- 3 files changed, 134 insertions(+), 58 deletions(-) diff --git a/common/stats.ts b/common/stats.ts index 152a6eae..258eec7c 100644 --- a/common/stats.ts +++ b/common/stats.ts @@ -3,6 +3,9 @@ export type Stats = { dailyActiveUsers: number[] weeklyActiveUsers: number[] monthlyActiveUsers: number[] + d1: number[] + d1Weekly: number[] + w1NewUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] diff --git a/functions/src/update-stats.ts b/functions/src/update-stats.ts index 3f1b5d36..928ea967 100644 --- a/functions/src/update-stats.ts +++ b/functions/src/update-stats.ts @@ -142,40 +142,77 @@ export const updateStatsCore = async () => { const weeklyActiveUsers = dailyUserIds.map((_, i) => { const start = Math.max(0, i - 6) - const end = i - const uniques = new Set<string>() - for (let j = start; j <= end; j++) - dailyUserIds[j].forEach((userId) => uniques.add(userId)) + const end = i + 1 + const uniques = new Set<string>(dailyUserIds.slice(start, end).flat()) return uniques.size }) const monthlyActiveUsers = dailyUserIds.map((_, i) => { const start = Math.max(0, i - 29) - const end = i - const uniques = new Set<string>() - for (let j = start; j <= end; j++) - dailyUserIds[j].forEach((userId) => uniques.add(userId)) + const end = i + 1 + const uniques = new Set<string>(dailyUserIds.slice(start, end).flat()) return uniques.size }) + const d1 = dailyUserIds.map((userIds, i) => { + if (i === 0) return 0 + + const uniques = new Set(userIds) + const yesterday = dailyUserIds[i - 1] + + const retainedCount = sumBy(yesterday, (userId) => + uniques.has(userId) ? 1 : 0 + ) + return retainedCount / uniques.size + }) + + const d1Weekly = d1.map((_, i) => { + const start = Math.max(0, i - 6) + const end = i + 1 + return average(d1.slice(start, end)) + }) + + const dailyNewUserIds = dailyNewUsers.map((users) => users.map((u) => u.id)) + const w1NewUsers = dailyNewUserIds.map((_userIds, i) => { + if (i < 13) return 0 + + const twoWeeksAgo = { + start: Math.max(0, i - 13), + end: Math.max(0, i - 6), + } + const lastWeek = { + start: Math.max(0, i - 6), + end: i + 1, + } + const newTwoWeeksAgo = new Set<string>( + dailyNewUserIds.slice(twoWeeksAgo.start, twoWeeksAgo.end).flat() + ) + const activeLastWeek = new Set<string>( + dailyUserIds.slice(lastWeek.start, lastWeek.end).flat() + ) + const retainedCount = sumBy(Array.from(newTwoWeeksAgo), (userId) => + activeLastWeek.has(userId) ? 1 : 0 + ) + const retainedFrac = retainedCount / newTwoWeeksAgo.size + return Math.round(retainedFrac * 100 * 100) / 100 + }) + const weekOnWeekRetention = dailyUserIds.map((_userId, i) => { const twoWeeksAgo = { start: Math.max(0, i - 13), - end: Math.max(0, i - 7), + end: Math.max(0, i - 6), } const lastWeek = { start: Math.max(0, i - 6), - end: i, + end: i + 1, } - const activeTwoWeeksAgo = new Set<string>() - for (let j = twoWeeksAgo.start; j <= twoWeeksAgo.end; j++) { - dailyUserIds[j].forEach((userId) => activeTwoWeeksAgo.add(userId)) - } - const activeLastWeek = new Set<string>() - for (let j = lastWeek.start; j <= lastWeek.end; j++) { - dailyUserIds[j].forEach((userId) => activeLastWeek.add(userId)) - } + const activeTwoWeeksAgo = new Set<string>( + dailyUserIds.slice(twoWeeksAgo.start, twoWeeksAgo.end).flat() + ) + const activeLastWeek = new Set<string>( + dailyUserIds.slice(lastWeek.start, lastWeek.end).flat() + ) const retainedCount = sumBy(Array.from(activeTwoWeeksAgo), (userId) => activeLastWeek.has(userId) ? 1 : 0 ) @@ -185,22 +222,20 @@ export const updateStatsCore = async () => { const monthlyRetention = dailyUserIds.map((_userId, i) => { const twoMonthsAgo = { - start: Math.max(0, i - 60), - end: Math.max(0, i - 30), + start: Math.max(0, i - 59), + end: Math.max(0, i - 29), } const lastMonth = { - start: Math.max(0, i - 30), - end: i, + start: Math.max(0, i - 29), + end: i + 1, } - const activeTwoMonthsAgo = new Set<string>() - for (let j = twoMonthsAgo.start; j <= twoMonthsAgo.end; j++) { - dailyUserIds[j].forEach((userId) => activeTwoMonthsAgo.add(userId)) - } - const activeLastMonth = new Set<string>() - for (let j = lastMonth.start; j <= lastMonth.end; j++) { - dailyUserIds[j].forEach((userId) => activeLastMonth.add(userId)) - } + const activeTwoMonthsAgo = new Set<string>( + dailyUserIds.slice(twoMonthsAgo.start, twoMonthsAgo.end).flat() + ) + const activeLastMonth = new Set<string>( + dailyUserIds.slice(lastMonth.start, lastMonth.end).flat() + ) const retainedCount = sumBy(Array.from(activeTwoMonthsAgo), (userId) => activeLastMonth.has(userId) ? 1 : 0 ) @@ -254,12 +289,12 @@ export const updateStatsCore = async () => { }) const weeklyTopTenthActions = dailyTopTenthActions.map((_, i) => { const start = Math.max(0, i - 6) - const end = i + const end = i + 1 return average(dailyTopTenthActions.slice(start, end)) }) const monthlyTopTenthActions = dailyTopTenthActions.map((_, i) => { const start = Math.max(0, i - 29) - const end = i + const end = i + 1 return average(dailyTopTenthActions.slice(start, end)) }) @@ -269,16 +304,16 @@ export const updateStatsCore = async () => { }) const weeklyManaBet = dailyManaBet.map((_, i) => { const start = Math.max(0, i - 6) - const end = i + const end = i + 1 const total = sum(dailyManaBet.slice(start, end)) if (end - start < 7) return (total * 7) / (end - start) return total }) const monthlyManaBet = dailyManaBet.map((_, i) => { const start = Math.max(0, i - 29) - const end = i + const end = i + 1 const total = sum(dailyManaBet.slice(start, end)) - const range = end - start + 1 + const range = end - start if (range < 30) return (total * 30) / range return total }) @@ -288,6 +323,9 @@ export const updateStatsCore = async () => { dailyActiveUsers, weeklyActiveUsers, monthlyActiveUsers, + d1, + d1Weekly, + w1NewUsers, dailyBetCounts, dailyContractCounts, dailyCommentCounts, diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 08fb5498..edffcdd3 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -47,30 +47,11 @@ export default function Analytics() { ) } -export function CustomAnalytics(props: { - startDate: number - dailyActiveUsers: number[] - weeklyActiveUsers: number[] - monthlyActiveUsers: number[] - dailyBetCounts: number[] - dailyContractCounts: number[] - dailyCommentCounts: number[] - dailySignups: number[] - weekOnWeekRetention: number[] - monthlyRetention: number[] - weeklyActivationRate: number[] - topTenthActions: { - daily: number[] - weekly: number[] - monthly: number[] - } - manaBet: { - daily: number[] - weekly: number[] - monthly: number[] - } -}) { +export function CustomAnalytics(props: Stats) { const { + d1, + d1Weekly, + w1NewUsers, dailyActiveUsers, dailyBetCounts, dailyContractCounts, @@ -153,6 +134,60 @@ export function CustomAnalytics(props: { /> <Spacer h={8} /> + <Title text="D1" /> + <p className="text-gray-500"> + The fraction of users that took an action yesterday that took an action + today. + </p> + <Spacer h={4} /> + + <Tabs + defaultIndex={1} + tabs={[ + { + title: 'D1', + content: ( + <DailyCountChart dailyCounts={d1} startDate={startDate} small /> + ), + }, + { + title: 'D1 weekly average', + content: ( + <DailyCountChart + dailyCounts={d1Weekly} + startDate={startDate} + small + /> + ), + }, + ]} + /> + <Spacer h={8} /> + + <Title text="W1 New users" /> + <p className="text-gray-500"> + The fraction of new users two weeks ago that took an action in the past + week. + </p> + <Spacer h={4} /> + + <Tabs + defaultIndex={0} + tabs={[ + { + title: 'W1', + content: ( + <DailyCountChart + dailyCounts={w1NewUsers} + startDate={startDate} + small + /> + ), + }, + ]} + /> + <Spacer h={8} /> + <Title text="Daily activity" /> <Tabs defaultIndex={0} From bfe00595e73d07d06fa57151895bc3e960c6e07d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 19 Sep 2022 00:53:10 -0700 Subject: [PATCH 46/71] Make comments with bet outcome but no answer outcome appear (#894) --- web/components/feed/contract-activity.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index fc817082..e660bf10 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -126,7 +126,10 @@ export function FreeResponseContractCommentsActivity(props: { (answer) => -getOutcomeProbability(contract, answer.number.toString()) ) const commentsByUserId = groupBy(comments, (c) => c.userId) - const commentsByOutcome = groupBy(comments, (c) => c.answerOutcome ?? '_') + const commentsByOutcome = groupBy( + comments, + (c) => c.answerOutcome ?? c.betOutcome ?? '_' + ) return ( <> From 5d65bb5bb1ebb9e2333beec825f46ad8db5dc57e Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 19 Sep 2022 07:31:04 -0600 Subject: [PATCH 47/71] Add message about unique bonuses withdrawn on n/a --- web/components/answers/answer-resolve-panel.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 4594ea35..ddb7942c 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import { sum } from 'lodash' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { Col } from '../layout/col' @@ -9,6 +9,7 @@ import { Row } from '../layout/row' import { ChooseCancelSelector } from '../yes-no-selector' import { ResolveConfirmationButton } from '../confirmation-button' import { removeUndefinedProps } from 'common/util/object' +import { BETTOR, PAST_BETS } from 'common/user' export function AnswerResolvePanel(props: { isAdmin: boolean @@ -32,6 +33,18 @@ export function AnswerResolvePanel(props: { const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState<string | undefined>(undefined) + const [warning, setWarning] = useState<string | undefined>(undefined) + + useEffect(() => { + if (resolveOption === 'CANCEL') { + setWarning( + `All ${PAST_BETS} will be returned. Unique ${BETTOR} bonuses will be + withdrawn from your account.` + ) + } else { + setWarning(undefined) + } + }, [resolveOption]) const onResolve = async () => { if (resolveOption === 'CHOOSE' && answers.length !== 1) return @@ -126,6 +139,7 @@ export function AnswerResolvePanel(props: { </Col> {!!error && <div className="text-red-500">{error}</div>} + {!!warning && <div className="text-warning">{warning}</div>} </Col> ) } From 6aa45a2d129fed0577e7912447d29037024928ff Mon Sep 17 00:00:00 2001 From: FRC <pico2x@gmail.com> Date: Mon, 19 Sep 2022 17:29:17 +0100 Subject: [PATCH 48/71] Move group navbar to top (#895) --- web/components/nav/group-nav-bar.tsx | 2 +- web/pages/group/[...slugs]/index.tsx | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/web/components/nav/group-nav-bar.tsx b/web/components/nav/group-nav-bar.tsx index 8c20fee9..9ea3f5a4 100644 --- a/web/components/nav/group-nav-bar.tsx +++ b/web/components/nav/group-nav-bar.tsx @@ -32,7 +32,7 @@ export function GroupNavBar(props: { const user = useUser() return ( - <nav className="fixed inset-x-0 bottom-0 z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden"> + <nav className="z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden"> {mobileGroupNavigation.map((item) => ( <NavBarItem key={item.name} diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index bb889e72..3adb01c1 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -262,7 +262,11 @@ export default function GroupPage(props: { return ( <> - <TopGroupNavBar group={group} /> + <TopGroupNavBar + group={group} + currentPage={sidebarPages[sidebarIndex].key} + onClick={onSidebarClick} + /> <div> <div className={ @@ -287,19 +291,19 @@ export default function GroupPage(props: { {pageContent} </main> </div> - <GroupNavBar - currentPage={sidebarPages[sidebarIndex].key} - onClick={onSidebarClick} - /> </div> </> ) } -export function TopGroupNavBar(props: { group: Group }) { +export function TopGroupNavBar(props: { + group: Group + currentPage: string + onClick: (key: string) => void +}) { return ( - <header className="sticky top-0 z-50 w-full pb-2 md:hidden lg:col-span-12"> - <div className="flex items-center border-b border-gray-200 bg-white px-4"> + <header className="sticky top-0 z-50 w-full border-b border-gray-200 md:hidden lg:col-span-12"> + <div className="flex items-center bg-white px-4"> <div className="flex-shrink-0"> <Link href="/"> <a className="text-indigo-700 hover:text-gray-500 "> @@ -313,6 +317,7 @@ export function TopGroupNavBar(props: { group: Group }) { </h1> </div> </div> + <GroupNavBar currentPage={props.currentPage} onClick={props.onClick} /> </header> ) } From 24cf42284f76fcb3fdcf098e423c602c171821fc Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 19 Sep 2022 14:03:45 -0500 Subject: [PATCH 49/71] replace "predictor" => "trader" --- common/user.ts | 12 ++++++------ functions/src/email-templates/new-unique-bettor.html | 4 ++-- .../src/email-templates/new-unique-bettors.html | 8 ++++---- functions/src/email-templates/one-week.html | 2 +- web/components/contract/contract-details.tsx | 2 +- web/pages/notifications.tsx | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/common/user.ts b/common/user.ts index 3a5b91e1..44b0e59b 100644 --- a/common/user.ts +++ b/common/user.ts @@ -88,9 +88,9 @@ export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' // TODO: remove. Hardcoding the strings would be better. // Different views require different language. -export const BETTOR = ENV_CONFIG.bettor ?? 'bettor' // aka predictor -export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'bettors' -export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'bet' // aka predict -export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'bets' -export const PAST_BET = ENV_CONFIG.pastBet ?? 'bet' // aka prediction -export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'bets' // aka predictions +export const BETTOR = ENV_CONFIG.bettor ?? 'trader' +export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders' +export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade' +export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades' +export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade' +export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades' diff --git a/functions/src/email-templates/new-unique-bettor.html b/functions/src/email-templates/new-unique-bettor.html index 51026121..d44a7de6 100644 --- a/functions/src/email-templates/new-unique-bettor.html +++ b/functions/src/email-templates/new-unique-bettor.html @@ -218,10 +218,10 @@ <br/> <br/> We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for - creating a market that appeals to others, and we'll do so for each new predictor. + creating a market that appeals to others, and we'll do so for each new trader. <br/> <br/> - Keep up the good work and check out your newest predictor below! + Keep up the good work and check out your newest trader below! </span></p> </div> </td> diff --git a/functions/src/email-templates/new-unique-bettors.html b/functions/src/email-templates/new-unique-bettors.html index 09c44d03..d9d0643c 100644 --- a/functions/src/email-templates/new-unique-bettors.html +++ b/functions/src/email-templates/new-unique-bettors.html @@ -9,7 +9,7 @@ <head> <meta name="viewport" content="width=device-width" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> - <title>New unique predictors on your market + New unique traders on your market