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:
parent
2e214cab7a
commit
bbf419953e
|
@ -3,7 +3,7 @@ import { Avatar } from '../avatar'
|
|||
import { useUserById } from '../../hooks/use-users'
|
||||
import { UserLink } from '../user-page'
|
||||
import { manaToUSD } from '../../pages/charity/[charitySlug]'
|
||||
import { RelativeTimestamp } from '../feed/feed-items'
|
||||
import { RelativeTimestamp } from '../relative-timestamp'
|
||||
|
||||
export function Donation(props: { txn: Txn }) {
|
||||
const { txn } = props
|
||||
|
|
65
web/components/comments-list.tsx
Normal file
65
web/components/comments-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -341,19 +341,8 @@ export function SearchableGrid(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function CreatorContractsList(props: { creator: User }) {
|
||||
const { creator } = 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 <></>
|
||||
|
||||
export function CreatorContractsList(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
return (
|
||||
<SearchableGrid
|
||||
contracts={contracts}
|
||||
|
|
|
@ -55,6 +55,7 @@ import { trackClick } from '../../lib/firebase/tracking'
|
|||
import { firebaseLogin } from '../../lib/firebase/users'
|
||||
import { DAY_MS } from '../../../common/util/time'
|
||||
import NewContractBadge from '../new-contract-badge'
|
||||
import { RelativeTimestamp } from '../relative-timestamp'
|
||||
import { calculateCpmmSale } from '../../../common/calculate-cpmm'
|
||||
|
||||
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(
|
||||
contract: Contract,
|
||||
createdTime: number,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Row } from './row'
|
||||
|
||||
type Tab = {
|
||||
title: string
|
||||
|
@ -10,8 +11,13 @@ type Tab = {
|
|||
href?: string
|
||||
}
|
||||
|
||||
export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) {
|
||||
const { tabs, defaultIndex } = props
|
||||
export function Tabs(props: {
|
||||
tabs: Tab[]
|
||||
defaultIndex?: number
|
||||
className?: string
|
||||
onClick?: (tabName: string) => void
|
||||
}) {
|
||||
const { tabs, defaultIndex, className, onClick } = props
|
||||
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
|
||||
const activeTab = tabs[activeIndex]
|
||||
|
||||
|
@ -28,19 +34,21 @@ export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) {
|
|||
e.preventDefault()
|
||||
}
|
||||
setActiveIndex(i)
|
||||
onClick?.(tab.title)
|
||||
}}
|
||||
className={clsx(
|
||||
activeIndex === i
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: '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}
|
||||
>
|
||||
{tab.tabIcon ? (
|
||||
<span className="mr-2">{tab.tabIcon}</span>
|
||||
) : null}
|
||||
{tab.title}
|
||||
<Row className={'items-center justify-center gap-1'}>
|
||||
{tab.tabIcon && <span> {tab.tabIcon}</span>}
|
||||
{tab.title}
|
||||
</Row>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
|
|
14
web/components/relative-timestamp.tsx
Normal file
14
web/components/relative-timestamp.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -12,6 +12,15 @@ import { Row } from './layout/row'
|
|||
import { LinkIcon } from '@heroicons/react/solid'
|
||||
import { genHash } from '../../common/util/random'
|
||||
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: {
|
||||
name: string
|
||||
|
@ -29,10 +38,47 @@ export function UserLink(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function UserPage(props: { user: User; currentUser?: User }) {
|
||||
const { user, currentUser } = props
|
||||
export function UserPage(props: {
|
||||
user: User
|
||||
currentUser?: User
|
||||
defaultTabTitle?: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { user, currentUser, defaultTabTitle } = props
|
||||
const isCurrentUser = user.id === currentUser?.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 (
|
||||
<Page>
|
||||
|
@ -138,8 +184,59 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
|||
</Col>
|
||||
|
||||
<Spacer h={10} />
|
||||
|
||||
<CreatorContractsList creator={user} />
|
||||
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
|
||||
<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>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
doc,
|
||||
collection,
|
||||
setDoc,
|
||||
query,
|
||||
collectionGroup,
|
||||
where,
|
||||
doc,
|
||||
orderBy,
|
||||
query,
|
||||
setDoc,
|
||||
where,
|
||||
} from 'firebase/firestore'
|
||||
import _ from 'lodash'
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { getValues, listenForValues } from './utils'
|
|||
import { db } from './init'
|
||||
import { User } from '../../../common/user'
|
||||
import { Comment } from '../../../common/comment'
|
||||
|
||||
export type { Comment }
|
||||
|
||||
export const MAX_COMMENT_LENGTH = 10000
|
||||
|
@ -125,3 +126,13 @@ export async function getDailyComments(
|
|||
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import Custom404 from '../404'
|
|||
export default function UserProfile() {
|
||||
const router = useRouter()
|
||||
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(() => {
|
||||
if (username) {
|
||||
getUserByUsername(username).then(setUser)
|
||||
|
@ -21,7 +21,11 @@ export default function UserProfile() {
|
|||
if (user === 'loading') return <></>
|
||||
|
||||
return user ? (
|
||||
<UserPage user={user} currentUser={currentUser || undefined} />
|
||||
<UserPage
|
||||
user={user}
|
||||
currentUser={currentUser || undefined}
|
||||
defaultTabTitle={tab}
|
||||
/>
|
||||
) : (
|
||||
<Custom404 />
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user