Skip to content

Commit

Permalink
New feed (#3212)
Browse files Browse the repository at this point in the history
* site activity

* get-site-activity

* fix contracts issue

* pass params

* remove comments

* adjust limits

* styling

* feed images

* contract mention: don't open in new tab

* conditionally show description

* move description outside hovercard

* todos

* include sales

* more todos
  • Loading branch information
mantikoros authored Dec 12, 2024
1 parent c1cc69f commit f612c15
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 12 deletions.
69 changes: 69 additions & 0 deletions backend/api/src/get-site-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { convertContract } from 'common/supabase/contracts'
import { filterDefined } from 'common/util/array'
import { uniqBy } from 'lodash'
import { log } from 'shared/utils'
import { convertBet } from 'common/supabase/bets'
import { convertContractComment } from 'common/supabase/comments'

export const getSiteActivity: APIHandler<'get-site-activity'> = async (props) => {
const { limit, blockedUserIds = [], blockedGroupSlugs = [], blockedContractIds = [] } = props
const pg = createSupabaseDirectClient()
log('getSiteActivity called', { limit })

const [recentBets, recentComments, newContracts] = await Promise.all([
// todo: show
// [ ] sweepcash bets >= 5
// [ ] large limit orders
// [ ] personalization based on followed users & topics
pg.manyOrNone(
`select * from contract_bets
where abs(amount) >= 500
and user_id != all($1)
and contract_id != all($2)
order by created_time desc limit $3`,
[blockedUserIds, blockedContractIds, limit * 5]
),
pg.manyOrNone(
`select * from contract_comments
where (likes - coalesce(dislikes, 0)) >= 2
and user_id != all($1)
and contract_id != all($2)
order by created_time desc limit $3`,
[blockedUserIds, blockedContractIds, limit]
),
pg.manyOrNone(
`select * from contracts
where visibility = 'public'
and tier != 'play'
and creator_id != all($1)
and id != all($2)
and not exists (
select 1 from group_contracts gc
join groups g on g.id = gc.group_id
where gc.contract_id = contracts.id
and g.slug = any($3)
)
order by created_time desc limit $4`,
[blockedUserIds, blockedContractIds, blockedGroupSlugs, limit]
),
])

const contractIds = uniqBy([
...recentBets.map((b) => b.contract_id),
...recentComments.map((c) => c.contract_id)
], id => id)

const relatedContracts = await pg.manyOrNone(
`select * from contracts where id = any($1)`,
[contractIds]
)

return {
bets: recentBets.map(convertBet),
comments: recentComments.map(convertContractComment),
newContracts: filterDefined(newContracts.map(convertContract)),
relatedContracts: filterDefined(relatedContracts.map(convertContract))
}
}
2 changes: 2 additions & 0 deletions backend/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ import { generateAIAnswers } from './generate-ai-answers'
import { getmonthlybets2024 } from './get-monthly-bets-2024'
import { getmaxminprofit2024 } from './get-max-min-profit-2024'
import { getNextLoanAmount } from './get-next-loan-amount'
import { getSiteActivity } from './get-site-activity'

// we define the handlers in this object in order to typecheck that every API has a handler
export const handlers: { [k in APIPath]: APIHandler<k> } = {
Expand Down Expand Up @@ -297,4 +298,5 @@ export const handlers: { [k in APIPath]: APIHandler<k> } = {
'get-monthly-bets-2024': getmonthlybets2024,
'get-max-min-profit-2024': getmaxminprofit2024,
'get-next-loan-amount': getNextLoanAmount,
'get-site-activity': getSiteActivity,
}
17 changes: 17 additions & 0 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,23 @@ export const API = (_apiTypeCheck = {
userId: z.string(),
}),
},
'get-site-activity': {
method: 'GET',
visibility: 'public',
authed: false,
returns: {} as {
bets: Bet[]
comments: ContractComment[]
newContracts: Contract[]
relatedContracts: Contract[]
},
props: z.object({
limit: z.coerce.number().default(10),
blockedUserIds: z.array(z.string()).optional(),
blockedGroupSlugs: z.array(z.string()).optional(),
blockedContractIds: z.array(z.string()).optional(),
}).strict(),
},
} as const)

export type APIPath = keyof typeof API
Expand Down
23 changes: 13 additions & 10 deletions web/components/contract/contract-mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { TRADED_TERM } from 'common/envs/constants'
import { formatWithToken } from 'common/util/format'
import Link from 'next/link'
import { useIsClient } from 'web/hooks/use-is-client'
import { getIsNative } from 'web/lib/native/is-native'
import { fromNow } from 'web/lib/util/time'
import { ContractStatusLabel } from './contracts-table'
import { getTextColor } from './text-color'
Expand All @@ -23,19 +22,23 @@ export function ContractMention(props: {
href={contractPath(contract)}
className={clsx('group inline whitespace-nowrap rounded-sm', className)}
title={isClient ? tooltipLabel(contract) : undefined}
target={getIsNative() ? '_self' : '_blank'}
// target={getIsNative() ? '_self' : '_blank'}
>
<span className="break-anywhere text-ink-900 group-hover:text-primary-500 group-focus:text-primary-500 mr-0.5 whitespace-normal font-medium transition-colors">
{contract.question}
</span>
<span
className={clsx(
probTextColor,
'ring-primary-100 group-hover:ring-primary-200 inline-flex rounded-full px-2 align-bottom font-semibold ring-1 ring-inset transition-colors'
)}
>
<ContractStatusLabel contract={contract} />
</span>

{contract.outcomeType === 'BINARY' && (
<span
className={clsx(
probTextColor,
'ring-primary-100 group-hover:ring-primary-200 inline-flex rounded-full px-2 align-bottom font-semibold ring-1 ring-inset transition-colors'
)}
>
<ContractStatusLabel contract={contract} />
</span>
)}

{!contract.resolution && probChange && (
<span className="text-ink-500 ml-0.5 text-xs">{probChange}</span>
)}
Expand Down
3 changes: 1 addition & 2 deletions web/components/feed/feed-bets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const FeedBet = memo(function FeedBet(props: {
className?: string
onReply?: (bet: Bet) => void
}) {
const { contract, bet, avatarSize, className, onReply } = props
const { contract, bet, avatarSize, className } = props
const { createdTime, userId } = bet
const user = useDisplayUserById(userId)
const showUser = dayjs(createdTime).isAfter('2022-06-01')
Expand All @@ -66,7 +66,6 @@ export const FeedBet = memo(function FeedBet(props: {
className="flex-1"
/>
</Row>
<BetActions onReply={onReply} bet={bet} contract={contract} />
</Row>
</Col>
)
Expand Down
199 changes: 199 additions & 0 deletions web/components/site-activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import clsx from 'clsx'
import { ContractComment } from 'common/comment'
import { Contract } from 'common/contract'
import { groupBy, keyBy, orderBy } from 'lodash'
import { memo } from 'react'
import { usePrivateUser } from 'web/hooks/use-user'
import { ContractMention } from './contract/contract-mention'
import { FeedBet } from './feed/feed-bets'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { RelativeTimestamp } from './relative-timestamp'
import { Avatar } from './widgets/avatar'
import { Content } from './widgets/editor'
import { LoadingIndicator } from './widgets/loading-indicator'
import { UserLink } from './widgets/user-link'
import { UserHovercard } from './user/user-hovercard'
import { useAPIGetter } from 'web/hooks/use-api-getter'

export function SiteActivity(props: {
className?: string
blockedUserIds?: string[]
}) {
const { className } = props
const privateUser = usePrivateUser()

const blockedGroupSlugs = privateUser?.blockedGroupSlugs ?? []
const blockedContractIds = privateUser?.blockedContractIds ?? []
const blockedUserIds = (privateUser?.blockedUserIds ?? []).concat(
props.blockedUserIds ?? []
)

const { data, loading } = useAPIGetter('get-site-activity', {
limit: 10,
blockedUserIds,
blockedGroupSlugs,
blockedContractIds,
})

if (loading || !data) return <LoadingIndicator />

const { bets, comments, newContracts, relatedContracts } = data
const contracts = [...newContracts, ...relatedContracts]
const contractsById = keyBy(contracts, 'id')

const items = orderBy(
[...bets, ...comments, ...newContracts],
'createdTime',
'desc'
)

const groups = orderBy(
Object.entries(
groupBy(items, (item) =>
'contractId' in item ? item.contractId : item.id
)
).map(([parentId, items]) => ({
parentId,
items,
})),
({ items }) => Math.max(...items.map((item) => item.createdTime)),
'desc'
)

return (
<Col className={clsx('gap-4', className)}>
<Col className="gap-4">
{groups.map(({ parentId, items }) => {
const contract = contractsById[parentId] as Contract

return (
<Col
key={parentId}
className="bg-canvas-0 border-canvas-50 hover:border-primary-300 gap-2 rounded-lg border px-4 py-3 transition-colors"
>
<Row className="gap-2">
<Col className="flex-1 gap-2">
<ContractMention contract={contract} />
<div className="space-y-2">
{items.map((item) =>
'amount' in item ? (
<FeedBet
className="!pt-0"
key={item.id}
contract={contract}
bet={item}
avatarSize="xs"
/>
) : 'question' in item ? (
<MarketCreatedLog
key={item.id}
contract={item}
showDescription={items.length === 1}
/>
) : 'channelId' in item ? null : (
<CommentLog key={item.id} comment={item} />
)
)}
</div>
</Col>
{contract.coverImageUrl && (
<img
src={contract.coverImageUrl}
alt=""
className="h-32 w-32 rounded-md object-cover"
/>
)}
</Row>
</Col>
)
})}
</Col>
</Col>
)
}

const MarketCreatedLog = memo(
(props: { contract: Contract; showDescription?: boolean }) => {
const {
creatorId,
creatorAvatarUrl,
creatorUsername,
creatorName,
createdTime,
} = props.contract
const { showDescription = false } = props

return (
<Col className="gap-2">
<UserHovercard userId={creatorId} className="flex-col">
<Row className="text-ink-600 items-center gap-2 text-sm">
<Avatar
avatarUrl={creatorAvatarUrl}
username={creatorUsername}
size="xs"
/>
<UserLink
user={{
id: creatorId,
name: creatorName,
username: creatorUsername,
}}
/>
<Row className="text-ink-400">
created
<RelativeTimestamp time={createdTime} />
</Row>
</Row>
</UserHovercard>

{showDescription && props.contract.description && (
// TODO: truncate if too long
<Content
size="sm"
content={props.contract.description}
className="mt-2 text-left"
/>
)}
</Col>
)
}
)
// todo: add liking/disliking
const CommentLog = memo(function FeedComment(props: {
comment: ContractComment
}) {
const { comment } = props
const {
userName,
text,
content,
userId,
userUsername,
userAvatarUrl,
createdTime,
} = comment

return (
<Col>
<Row
id={comment.id}
className="text-ink-500 mb-1 items-center gap-2 text-sm"
>
<UserHovercard userId={userId}>
<Avatar size="xs" username={userUsername} avatarUrl={userAvatarUrl} />
</UserHovercard>
<span>
<UserHovercard userId={userId}>
<UserLink
user={{ id: userId, name: userName, username: userUsername }}
/>
</UserHovercard>{' '}
commented
</span>
<RelativeTimestamp time={createdTime} />
</Row>
<Content size="sm" className="grow" content={content || text} />
</Col>
)
})
31 changes: 31 additions & 0 deletions web/pages/activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Col } from 'web/components/layout/col'
import { Page } from 'web/components/layout/page'
import { SEO } from 'web/components/SEO'
import { Row } from 'web/components/layout/row'
import { SiteActivity } from 'web/components/site-activity'
import { TRADE_TERM } from 'common/envs/constants'

export default function ActivityPage() {
return (
<Page trackPageView={'activity page'}>
<SEO
title="Activity"
description={`Watch all site activity live, including ${TRADE_TERM}s, comments, and new questions.`}
url="/activity"
/>

<Col className="w-full max-w-3xl gap-4 self-center sm:pb-4">
<Row
className={
'w-full items-center justify-between pt-1 sm:justify-start sm:gap-4'
}
>
<span className="text-primary-700 line-clamp-1 shrink px-1 text-2xl">
Activity
</span>
</Row>
<SiteActivity className="w-full" />
</Col>
</Page>
)
}

0 comments on commit f612c15

Please sign in to comment.