Show comments on profile (#137)

* WIP - got comments on the user page

* Remove number from chosen FR answer

* Distinguish wining and losing FR answers

* Show no answers text

* Simplify get answer items logic

* Show answer number

* Show answer # when resolving

* Fix import path

* Add user's collated comments onto profile

* Allow linking to comments/markets in profile

* Allow preload of users contracts in profile

* Remove unused check

* Small code improvements
This commit is contained in:
Boa 2022-05-05 16:30:30 -06:00 committed by GitHub
parent 2e214cab7a
commit bbf419953e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 220 additions and 42 deletions

View File

@ -3,7 +3,7 @@ import { Avatar } from '../avatar'
import { useUserById } from '../../hooks/use-users' import { useUserById } from '../../hooks/use-users'
import { UserLink } from '../user-page' import { UserLink } from '../user-page'
import { manaToUSD } from '../../pages/charity/[charitySlug]' import { manaToUSD } from '../../pages/charity/[charitySlug]'
import { RelativeTimestamp } from '../feed/feed-items' import { RelativeTimestamp } from '../relative-timestamp'
export function Donation(props: { txn: Txn }) { export function Donation(props: { txn: Txn }) {
const { txn } = props const { txn } = props

View File

@ -0,0 +1,65 @@
import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract'
import { contractPath } from '../lib/firebase/contracts'
import { SiteLink } from './site-link'
import { Row } from './layout/row'
import { Avatar } from './avatar'
import { RelativeTimestamp } from './relative-timestamp'
import { UserLink } from './user-page'
import { User } from '../../common/user'
import { Col } from './layout/col'
import { Linkify } from './linkify'
export function UserCommentsList(props: {
user: User
commentsByUniqueContracts: Map<Contract, Comment[]>
}) {
const { commentsByUniqueContracts } = props
return (
<Col className={'bg-white'}>
{Array.from(commentsByUniqueContracts).map(([contract, comments]) => (
<div key={contract.id} className={'border-width-1 border-b p-5'}>
<div className={'mb-2 text-sm text-indigo-700'}>
<SiteLink href={contractPath(contract)}>
{contract.question}
</SiteLink>
</div>
{comments.map((comment) => (
<div key={comment.id} className={'relative pb-6'}>
<div className="relative flex items-start space-x-3">
<ProfileComment comment={comment} />
</div>
</div>
))}
</div>
))}
</Col>
)
}
function ProfileComment(props: { comment: Comment }) {
const { comment } = props
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
// TODO: find and attach relevant bets by comment betId at some point
return (
<div>
<Row className={'gap-4'}>
<Avatar username={userUsername} avatarUrl={userAvatarUrl} />
<div className="min-w-0 flex-1">
<div>
<p className="mt-0.5 text-sm text-gray-500">
<UserLink
className="text-gray-500"
username={userUsername}
name={userName}
/>{' '}
<RelativeTimestamp time={createdTime} />
</p>
</div>
<Linkify text={text} />
</div>
</Row>
</div>
)
}

View File

@ -341,19 +341,8 @@ export function SearchableGrid(props: {
) )
} }
export function CreatorContractsList(props: { creator: User }) { export function CreatorContractsList(props: { contracts: Contract[] }) {
const { creator } = props const { contracts } = props
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
useEffect(() => {
if (creator?.id) {
// TODO: stream changes from firestore
listContracts(creator.id).then(setContracts)
}
}, [creator])
if (contracts === 'loading') return <></>
return ( return (
<SearchableGrid <SearchableGrid
contracts={contracts} contracts={contracts}

View File

@ -55,6 +55,7 @@ import { trackClick } from '../../lib/firebase/tracking'
import { firebaseLogin } from '../../lib/firebase/users' import { firebaseLogin } from '../../lib/firebase/users'
import { DAY_MS } from '../../../common/util/time' import { DAY_MS } from '../../../common/util/time'
import NewContractBadge from '../new-contract-badge' import NewContractBadge from '../new-contract-badge'
import { RelativeTimestamp } from '../relative-timestamp'
import { calculateCpmmSale } from '../../../common/calculate-cpmm' import { calculateCpmmSale } from '../../../common/calculate-cpmm'
export function FeedItems(props: { export function FeedItems(props: {
@ -357,17 +358,6 @@ export function CommentInput(props: {
) )
} }
export function RelativeTimestamp(props: { time: number }) {
const { time } = props
return (
<DateTimeTooltip time={time}>
<span className="ml-1 whitespace-nowrap text-gray-400">
{fromNow(time)}
</span>
</DateTimeTooltip>
)
}
function getBettorsPosition( function getBettorsPosition(
contract: Contract, contract: Contract,
createdTime: number, createdTime: number,

View File

@ -1,6 +1,7 @@
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { Row } from './row'
type Tab = { type Tab = {
title: string title: string
@ -10,8 +11,13 @@ type Tab = {
href?: string href?: string
} }
export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) { export function Tabs(props: {
const { tabs, defaultIndex } = props tabs: Tab[]
defaultIndex?: number
className?: string
onClick?: (tabName: string) => void
}) {
const { tabs, defaultIndex, className, onClick } = props
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
const activeTab = tabs[activeIndex] const activeTab = tabs[activeIndex]
@ -28,19 +34,21 @@ export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) {
e.preventDefault() e.preventDefault()
} }
setActiveIndex(i) setActiveIndex(i)
onClick?.(tab.title)
}} }}
className={clsx( className={clsx(
activeIndex === i activeIndex === i
? 'border-indigo-500 text-indigo-600' ? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
'cursor-pointer whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium' 'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium',
className
)} )}
aria-current={activeIndex === i ? 'page' : undefined} aria-current={activeIndex === i ? 'page' : undefined}
> >
{tab.tabIcon ? ( <Row className={'items-center justify-center gap-1'}>
<span className="mr-2">{tab.tabIcon}</span> {tab.tabIcon && <span> {tab.tabIcon}</span>}
) : null}
{tab.title} {tab.title}
</Row>
</a> </a>
</Link> </Link>
))} ))}

View File

@ -0,0 +1,14 @@
import { DateTimeTooltip } from './datetime-tooltip'
import { fromNow } from '../lib/util/time'
import React from 'react'
export function RelativeTimestamp(props: { time: number }) {
const { time } = props
return (
<DateTimeTooltip time={time}>
<span className="ml-1 whitespace-nowrap text-gray-400">
{fromNow(time)}
</span>
</DateTimeTooltip>
)
}

View File

@ -12,6 +12,15 @@ import { Row } from './layout/row'
import { LinkIcon } from '@heroicons/react/solid' import { LinkIcon } from '@heroicons/react/solid'
import { genHash } from '../../common/util/random' import { genHash } from '../../common/util/random'
import { PencilIcon } from '@heroicons/react/outline' import { PencilIcon } from '@heroicons/react/outline'
import { Tabs } from './layout/tabs'
import { UserCommentsList } from './comments-list'
import { useEffect, useState } from 'react'
import { Comment, getUsersComments } from '../lib/firebase/comments'
import { Contract } from '../../common/contract'
import { getContractFromId, listContracts } from '../lib/firebase/contracts'
import { LoadingIndicator } from './loading-indicator'
import { useRouter } from 'next/router'
import _ from 'lodash'
export function UserLink(props: { export function UserLink(props: {
name: string name: string
@ -29,10 +38,47 @@ export function UserLink(props: {
) )
} }
export function UserPage(props: { user: User; currentUser?: User }) { export function UserPage(props: {
const { user, currentUser } = props user: User
currentUser?: User
defaultTabTitle?: string
}) {
const router = useRouter()
const { user, currentUser, defaultTabTitle } = props
const isCurrentUser = user.id === currentUser?.id const isCurrentUser = user.id === currentUser?.id
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
const [usersComments, setUsersComments] = useState<Comment[]>([] as Comment[])
const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>(
'loading'
)
const [commentsByContract, setCommentsByContract] = useState<
Map<Contract, Comment[]> | 'loading'
>('loading')
useEffect(() => {
if (!user) return
getUsersComments(user.id).then(setUsersComments)
listContracts(user.id).then(setUsersContracts)
}, [user])
useEffect(() => {
const uniqueContractIds = _.uniq(
usersComments.map((comment) => comment.contractId)
)
Promise.all(
uniqueContractIds.map((contractId) => getContractFromId(contractId))
).then((contracts) => {
const commentsByContract = new Map<Contract, Comment[]>()
contracts.forEach((contract) => {
if (!contract) return
commentsByContract.set(
contract,
usersComments.filter((comment) => comment.contractId === contract.id)
)
})
setCommentsByContract(commentsByContract)
})
}, [usersComments])
return ( return (
<Page> <Page>
@ -138,8 +184,59 @@ export function UserPage(props: { user: User; currentUser?: User }) {
</Col> </Col>
<Spacer h={10} /> <Spacer h={10} />
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
<CreatorContractsList creator={user} /> <Tabs
className={'pb-2 pt-1 '}
defaultIndex={defaultTabTitle === 'Comments' ? 1 : 0}
onClick={(tabName) =>
router.push(
{
pathname: `/${user.username}`,
query: { tab: tabName },
},
undefined,
{ shallow: true }
)
}
tabs={[
{
title: 'Markets',
content: <CreatorContractsList contracts={usersContracts} />,
tabIcon: (
<div
className={clsx(
usersContracts.length > 9 ? 'px-1' : 'px-1.5',
'items-center rounded-full border-2 border-current py-0.5 text-xs'
)}
>
{usersContracts.length}
</div>
),
},
{
title: 'Comments',
content: (
<UserCommentsList
user={user}
commentsByUniqueContracts={commentsByContract}
/>
),
tabIcon: (
<div
className={clsx(
usersComments.length > 9 ? 'px-1' : 'px-1.5',
'items-center rounded-full border-2 border-current py-0.5 text-xs'
)}
>
{usersComments.length}
</div>
),
},
]}
/>
) : (
<LoadingIndicator />
)}
</Col> </Col>
</Page> </Page>
) )

View File

@ -1,11 +1,11 @@
import { import {
doc,
collection, collection,
setDoc,
query,
collectionGroup, collectionGroup,
where, doc,
orderBy, orderBy,
query,
setDoc,
where,
} from 'firebase/firestore' } from 'firebase/firestore'
import _ from 'lodash' import _ from 'lodash'
@ -13,6 +13,7 @@ import { getValues, listenForValues } from './utils'
import { db } from './init' import { db } from './init'
import { User } from '../../../common/user' import { User } from '../../../common/user'
import { Comment } from '../../../common/comment' import { Comment } from '../../../common/comment'
export type { Comment } export type { Comment }
export const MAX_COMMENT_LENGTH = 10000 export const MAX_COMMENT_LENGTH = 10000
@ -125,3 +126,13 @@ export async function getDailyComments(
return commentsByDay return commentsByDay
} }
const getUsersCommentsQuery = (userId: string) =>
query(
collectionGroup(db, 'comments'),
where('userId', '==', userId),
orderBy('createdTime', 'desc')
)
export async function getUsersComments(userId: string) {
return await getValues<Comment>(getUsersCommentsQuery(userId))
}

View File

@ -9,7 +9,7 @@ import Custom404 from '../404'
export default function UserProfile() { export default function UserProfile() {
const router = useRouter() const router = useRouter()
const [user, setUser] = useState<User | null | 'loading'>('loading') const [user, setUser] = useState<User | null | 'loading'>('loading')
const { username } = router.query as { username: string } const { username, tab } = router.query as { username: string; tab: string }
useEffect(() => { useEffect(() => {
if (username) { if (username) {
getUserByUsername(username).then(setUser) getUserByUsername(username).then(setUser)
@ -21,7 +21,11 @@ export default function UserProfile() {
if (user === 'loading') return <></> if (user === 'loading') return <></>
return user ? ( return user ? (
<UserPage user={user} currentUser={currentUser || undefined} /> <UserPage
user={user}
currentUser={currentUser || undefined}
defaultTabTitle={tab}
/>
) : ( ) : (
<Custom404 /> <Custom404 />
) )