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 (
+
+
+ {text}
+ {children}
+
+ )
+}
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 && (
+
+ )}
+ {item.name}
+
+ )
+
+ 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 && (
-
- )}
- {item.name}
-
- )
-
- 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 (
-
-
- {text}
- {children}
-
- )
-}
-
-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 (
-
- {' '}
- {item.label}
-
+
+ {' '}
+ {item.label}
+
+
+ {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() {
-
+
+
+
+
@@ -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: {
@@ -322,6 +319,7 @@ export function TrendingGroupsSection(props: {
{chosenGroups.map((g) => (
{
@@ -343,6 +341,11 @@ export function TrendingGroupsSection(props: {
}
}}
>
+
+
{g.name}
))}
From d8e9e7812a404327dc99525bc50ec85022b24a94 Mon Sep 17 00:00:00 2001
From: James Grugett
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 None
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 (
From 216616960841cbf6cba27cc36f14b1a851038107 Mon Sep 17 00:00:00 2001
From: James Grugett
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
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
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 (
From eb021f30f5fd65e9a83254d275c0c0d3ca192b46 Mon Sep 17 00:00:00 2001
From: James Grugett
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(
- 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(
+ 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
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
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
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([])
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('')
}}
>
- You can add markets and members to your group after creation.
+ You can add markets to your group after creation.
- {errorText}
+ {errorText && {errorText}
}
-
-
-
-
-
-
-
- setName(e.target.value || '')}
- />
-
+
+
+ setName(e.target.value || '')}
+ />
From 1da4373335cec4e99dfed736502d7826e5596f1c Mon Sep 17 00:00:00 2001
From: James Grugett
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: {
)}
@@ -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={
}
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
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: {
{subscriptionTypes.map((subType) => (
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) {
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}
>
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) {
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}
>
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) {
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}
>
From 540915eb6596f9deb8a6c4345206ea45dba43874 Mon Sep 17 00:00:00 2001
From: mantikoros
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
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
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
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
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
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: {
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
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()
+ 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]]}
/>
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: {
b.userId === user.id) : []
+ }
comments={generalComments}
tips={tips}
user={user}
@@ -95,7 +97,9 @@ export function ContractTabs(props: {
) : (
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: {
{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}
/>
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
+ betsByCurrentUser: Bet[]
commentsByUserId: Dictionary
}) {
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
+ betsByCurrentUser: Bet[]
commentsByUserId: Dictionary
}) {
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: {
/>
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 (
{' '}
- {!comment.betId != null &&
- userPosition > 0 &&
+ {comment.betId == null &&
+ commenterPositionProb != null &&
+ commenterPositionOutcome != null &&
+ commenterPositionShares != null &&
+ commenterPositionShares > 0 &&
contract.outcomeType !== 'NUMERIC' && (
<>
{'is '}
>
@@ -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
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: {
<>
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) => (
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()
- for (let j = start; j <= end; j++)
- dailyUserIds[j].forEach((userId) => uniques.add(userId))
+ const end = i + 1
+ const uniques = new Set(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()
- for (let j = start; j <= end; j++)
- dailyUserIds[j].forEach((userId) => uniques.add(userId))
+ const end = i + 1
+ const uniques = new Set(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(
+ dailyNewUserIds.slice(twoWeeksAgo.start, twoWeeksAgo.end).flat()
+ )
+ const activeLastWeek = new Set(
+ 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()
- for (let j = twoWeeksAgo.start; j <= twoWeeksAgo.end; j++) {
- dailyUserIds[j].forEach((userId) => activeTwoWeeksAgo.add(userId))
- }
- const activeLastWeek = new Set()
- for (let j = lastWeek.start; j <= lastWeek.end; j++) {
- dailyUserIds[j].forEach((userId) => activeLastWeek.add(userId))
- }
+ const activeTwoWeeksAgo = new Set(
+ dailyUserIds.slice(twoWeeksAgo.start, twoWeeksAgo.end).flat()
+ )
+ const activeLastWeek = new Set(
+ 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()
- for (let j = twoMonthsAgo.start; j <= twoMonthsAgo.end; j++) {
- dailyUserIds[j].forEach((userId) => activeTwoMonthsAgo.add(userId))
- }
- const activeLastMonth = new Set()
- for (let j = lastMonth.start; j <= lastMonth.end; j++) {
- dailyUserIds[j].forEach((userId) => activeLastMonth.add(userId))
- }
+ const activeTwoMonthsAgo = new Set(
+ dailyUserIds.slice(twoMonthsAgo.start, twoMonthsAgo.end).flat()
+ )
+ const activeLastMonth = new Set(
+ 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: {
/>
+
+
+ The fraction of users that took an action yesterday that took an action
+ today.
+
+
+
+
+ ),
+ },
+ {
+ title: 'D1 weekly average',
+ content: (
+
+ ),
+ },
+ ]}
+ />
+
+
+
+
+ The fraction of new users two weeks ago that took an action in the past
+ week.
+
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
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
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(undefined)
+ const [warning, setWarning] = useState(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: {
{!!error && {error}
}
+ {!!warning && {warning}
}
)
}
From 6aa45a2d129fed0577e7912447d29037024928ff Mon Sep 17 00:00:00 2001
From: FRC
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 (
-