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 { 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
|
||||||
|
|
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 }) {
|
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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
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 { 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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user