Add notifications for badge awards
This commit is contained in:
parent
4d94f24892
commit
71fac10e44
|
@ -4,13 +4,12 @@ export type Badge = {
|
||||||
type: BadgeTypes
|
type: BadgeTypes
|
||||||
createdTime: number
|
createdTime: number
|
||||||
data: { [key: string]: any }
|
data: { [key: string]: any }
|
||||||
name: string
|
name: 'Proven Correct' | 'Streaker' | 'Market Maker'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
|
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
|
||||||
|
|
||||||
export type ProvenCorrectBadgeData = {
|
export type ProvenCorrectBadgeData = {
|
||||||
name: 'Proven Correct'
|
|
||||||
type: 'PROVEN_CORRECT'
|
type: 'PROVEN_CORRECT'
|
||||||
data: {
|
data: {
|
||||||
contractSlug: string
|
contractSlug: string
|
||||||
|
@ -22,7 +21,6 @@ export type ProvenCorrectBadgeData = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MarketCreatorBadgeData = {
|
export type MarketCreatorBadgeData = {
|
||||||
name: 'Market Maker'
|
|
||||||
type: 'MARKET_CREATOR'
|
type: 'MARKET_CREATOR'
|
||||||
data: {
|
data: {
|
||||||
totalContractsCreated: number
|
totalContractsCreated: number
|
||||||
|
@ -30,7 +28,6 @@ export type MarketCreatorBadgeData = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StreakerBadgeData = {
|
export type StreakerBadgeData = {
|
||||||
name: 'Streaker'
|
|
||||||
type: 'STREAKER'
|
type: 'STREAKER'
|
||||||
data: {
|
data: {
|
||||||
totalBettingStreak: number
|
totalBettingStreak: number
|
||||||
|
|
|
@ -79,6 +79,7 @@ export function scoreCommentorsAndBettors(
|
||||||
comments,
|
comments,
|
||||||
(c) => c.betId && -profitById[c.betId]
|
(c) => c.betId && -profitById[c.betId]
|
||||||
)[0]?.id
|
)[0]?.id
|
||||||
|
const topCommentBetId = commentsById[topCommentId]?.betId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topCommentId,
|
topCommentId,
|
||||||
|
@ -87,5 +88,6 @@ export function scoreCommentorsAndBettors(
|
||||||
profitById,
|
profitById,
|
||||||
commentsById,
|
commentsById,
|
||||||
betsById,
|
betsById,
|
||||||
|
topCommentBetId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,12 @@ import {
|
||||||
Notification,
|
Notification,
|
||||||
notification_reason_types,
|
notification_reason_types,
|
||||||
} from '../../common/notification'
|
} from '../../common/notification'
|
||||||
import { User } from '../../common/user'
|
import {
|
||||||
|
MANIFOLD_AVATAR_URL,
|
||||||
|
MANIFOLD_USER_NAME,
|
||||||
|
MANIFOLD_USER_USERNAME,
|
||||||
|
User,
|
||||||
|
} from '../../common/user'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { getPrivateUser, getValues } from './utils'
|
import { getPrivateUser, getValues } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
|
@ -30,6 +35,7 @@ import {
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
||||||
import { ContractFollow } from '../../common/follow'
|
import { ContractFollow } from '../../common/follow'
|
||||||
|
import { Badge } from 'common/badge'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
type recipients_to_reason_texts = {
|
type recipients_to_reason_texts = {
|
||||||
|
@ -1087,6 +1093,43 @@ export const createBountyNotification = async (
|
||||||
sourceTitle: contract.question,
|
sourceTitle: contract.question,
|
||||||
}
|
}
|
||||||
return await notificationRef.set(removeUndefinedProps(notification))
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
}
|
||||||
// maybe TODO: send email notification to comment creator
|
|
||||||
|
export const createBadgeAwardedNotification = async (
|
||||||
|
user: User,
|
||||||
|
badge: Badge
|
||||||
|
) => {
|
||||||
|
const privateUser = await getPrivateUser(user.id)
|
||||||
|
if (!privateUser) return
|
||||||
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
|
privateUser,
|
||||||
|
'badges_awarded'
|
||||||
|
)
|
||||||
|
if (!sendToBrowser) return
|
||||||
|
|
||||||
|
const notificationRef = firestore
|
||||||
|
.collection(`/users/${user.id}/notifications`)
|
||||||
|
.doc()
|
||||||
|
const notification: Notification = {
|
||||||
|
id: notificationRef.id,
|
||||||
|
userId: user.id,
|
||||||
|
reason: 'badges_awarded',
|
||||||
|
createdTime: Date.now(),
|
||||||
|
isSeen: false,
|
||||||
|
sourceId: badge.type,
|
||||||
|
sourceType: 'badge',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceUserName: MANIFOLD_USER_NAME,
|
||||||
|
sourceUserUsername: MANIFOLD_USER_USERNAME,
|
||||||
|
sourceUserAvatarUrl: MANIFOLD_AVATAR_URL,
|
||||||
|
sourceText: `You earned a new ${badge.name} badge!`,
|
||||||
|
sourceSlug: `/${user.username}?show=badges&badge=${badge.type}`,
|
||||||
|
sourceTitle: badge.name,
|
||||||
|
data: {
|
||||||
|
badge,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
|
||||||
|
// TODO send email notification
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
revalidateStaticProps,
|
revalidateStaticProps,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
|
createBadgeAwardedNotification,
|
||||||
createBetFillNotification,
|
createBetFillNotification,
|
||||||
createBettingStreakBonusNotification,
|
createBettingStreakBonusNotification,
|
||||||
createUniqueBettorBonusNotification,
|
createUniqueBettorBonusNotification,
|
||||||
|
@ -317,6 +318,7 @@ async function handleBettingStreakBadgeAward(
|
||||||
if (newBettingStreak in streakerBadgeRarityThresholds) {
|
if (newBettingStreak in streakerBadgeRarityThresholds) {
|
||||||
const badge = {
|
const badge = {
|
||||||
type: 'STREAKER',
|
type: 'STREAKER',
|
||||||
|
name: 'Streaker',
|
||||||
data: {
|
data: {
|
||||||
totalBettingStreak: newBettingStreak,
|
totalBettingStreak: newBettingStreak,
|
||||||
},
|
},
|
||||||
|
@ -335,5 +337,6 @@ async function handleBettingStreakBadgeAward(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
await createBadgeAwardedNotification(user, badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
|
|
||||||
import { getUser, getValues } from './utils'
|
import { getUser, getValues } from './utils'
|
||||||
import { createNewContractNotification } from './create-notification'
|
import {
|
||||||
|
createBadgeAwardedNotification,
|
||||||
|
createNewContractNotification,
|
||||||
|
} from './create-notification'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
@ -49,6 +52,7 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) {
|
||||||
if (contracts.length in marketMakerBadgeRarityThresholds) {
|
if (contracts.length in marketMakerBadgeRarityThresholds) {
|
||||||
const badge = {
|
const badge = {
|
||||||
type: 'MARKET_CREATOR',
|
type: 'MARKET_CREATOR',
|
||||||
|
name: 'Market Maker',
|
||||||
data: {
|
data: {
|
||||||
totalContractsCreated: contracts.length,
|
totalContractsCreated: contracts.length,
|
||||||
},
|
},
|
||||||
|
@ -72,5 +76,6 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
await createBadgeAwardedNotification(contractCreator, badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { getUser, getValues } from './utils'
|
import { getUser, getValues } from './utils'
|
||||||
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
import {
|
||||||
|
createBadgeAwardedNotification,
|
||||||
|
createCommentOrAnswerOrUpdatedContractNotification,
|
||||||
|
} from './create-notification'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
@ -50,18 +53,19 @@ async function handleResolvedContract(contract: Contract) {
|
||||||
firestore.collection(`contracts/${contract.id}/comments`)
|
firestore.collection(`contracts/${contract.id}/comments`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const { topCommentId, profitById, commentsById, betsById } =
|
const { topCommentId, profitById, commentsById, betsById, topCommentBetId } =
|
||||||
scoreCommentorsAndBettors(contract, bets, comments)
|
scoreCommentorsAndBettors(contract, bets, comments)
|
||||||
if (topCommentId && profitById[topCommentId] > 0) {
|
if (topCommentBetId && profitById[topCommentBetId] > 0) {
|
||||||
// award proven correct badge to user
|
// award proven correct badge to user
|
||||||
const comment = commentsById[topCommentId]
|
const comment = commentsById[topCommentId]
|
||||||
const bet = betsById[topCommentId]
|
const bet = betsById[topCommentBetId]
|
||||||
|
|
||||||
const user = await getUser(comment.userId)
|
const user = await getUser(comment.userId)
|
||||||
if (!user) return
|
if (!user) return
|
||||||
const newProvenCorrectBadge = {
|
const newProvenCorrectBadge = {
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
type: 'PROVEN_CORRECT',
|
type: 'PROVEN_CORRECT',
|
||||||
|
name: 'Proven Correct',
|
||||||
data: {
|
data: {
|
||||||
contractSlug: contract.slug,
|
contractSlug: contract.slug,
|
||||||
contractCreatorUsername: contract.creatorUsername,
|
contractCreatorUsername: contract.creatorUsername,
|
||||||
|
@ -87,6 +91,7 @@ async function handleResolvedContract(contract: Contract) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
await createBadgeAwardedNotification(user, newProvenCorrectBadge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ async function main() {
|
||||||
.update({
|
.update({
|
||||||
notificationPreferences: {
|
notificationPreferences: {
|
||||||
...privateUser.notificationPreferences,
|
...privateUser.notificationPreferences,
|
||||||
badges_awarded: [],
|
badges_awarded: ['browser'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { resolvedPayout } from 'common/calculate'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
import { groupBy, mapValues, sumBy, sortBy } from 'lodash'
|
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
|
||||||
import { FeedBet } from '../feed/feed-bets'
|
import { FeedBet } from '../feed/feed-bets'
|
||||||
import { FeedComment } from '../feed/feed-comments'
|
import { FeedComment } from '../feed/feed-comments'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
|
@ -57,29 +56,34 @@ export function ContractTopTrades(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: ContractComment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, comments, tips } = props
|
const { contract, bets, comments } = props
|
||||||
const { topBetId, topBettor, profitById, betsById } =
|
const {
|
||||||
scoreCommentorsAndBettors(contract, bets, comments)
|
topBetId,
|
||||||
|
topBettor,
|
||||||
// And also the comment with the highest profit
|
profitById,
|
||||||
const topComment = sortBy(comments, (c) => c.betId && -profitById[c.betId])[0]
|
betsById,
|
||||||
|
topCommentId,
|
||||||
|
commentsById,
|
||||||
|
topCommentBetId,
|
||||||
|
} = scoreCommentorsAndBettors(contract, bets, comments)
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 max-w-sm">
|
<div className="mt-12 max-w-sm">
|
||||||
{topComment && profitById[topComment.id] > 0 && (
|
{topCommentBetId && profitById[topCommentBetId] > 0 && (
|
||||||
<>
|
<>
|
||||||
<Title text="💬 Proven correct" className="!mt-0" />
|
<Title text="💬 Proven correct" className="!mt-0" />
|
||||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||||
<FeedComment contract={contract} comment={topComment} />
|
<FeedComment
|
||||||
|
contract={contract}
|
||||||
|
comment={commentsById[topCommentId]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={16} />
|
<Spacer h={16} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* If they're the same, only show the comment; otherwise show both */}
|
{/* If they're the same, only show the comment; otherwise show both */}
|
||||||
{topBettor && topBetId !== topComment?.betId && profitById[topBetId] > 0 && (
|
{topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
|
||||||
<>
|
<>
|
||||||
<Title text="💸 Best bet" className="!mt-0" />
|
<Title text="💸 Best bet" className="!mt-0" />
|
||||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||||
|
|
|
@ -131,6 +131,7 @@ export function NotificationSettings(props: {
|
||||||
'betting_streaks',
|
'betting_streaks',
|
||||||
'referral_bonuses',
|
'referral_bonuses',
|
||||||
'unique_bettors_on_your_contract',
|
'unique_bettors_on_your_contract',
|
||||||
|
'badges_awarded',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const otherBalances: SectionData = {
|
const otherBalances: SectionData = {
|
||||||
|
|
|
@ -151,7 +151,7 @@ function StreakerBadgeItem(props: { badge: StreakerBadge; rarity: rarities }) {
|
||||||
const { badge, rarity } = props
|
const { badge, rarity } = props
|
||||||
const { totalBettingStreak } = badge.data
|
const { totalBettingStreak } = badge.data
|
||||||
return (
|
return (
|
||||||
<Col className={'text-center'}>
|
<Col className={'cursor-default text-center'}>
|
||||||
<Medal rarity={rarity} />
|
<Medal rarity={rarity} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Make predictions ${totalBettingStreak} day
|
text={`Make predictions ${totalBettingStreak} day
|
||||||
|
@ -179,7 +179,7 @@ function MarketCreatorBadgeItem(props: {
|
||||||
const { badge, rarity } = props
|
const { badge, rarity } = props
|
||||||
const { totalContractsCreated } = badge.data
|
const { totalContractsCreated } = badge.data
|
||||||
return (
|
return (
|
||||||
<Col className={'text-center'}>
|
<Col className={'cursor-default text-center'}>
|
||||||
<Medal rarity={rarity} />
|
<Medal rarity={rarity} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Make ${totalContractsCreated} market${
|
text={`Make ${totalContractsCreated} market${
|
||||||
|
|
|
@ -270,7 +270,11 @@ export function ContractPageContent(
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||||
<ContractLeaderboard contract={contract} bets={bets} />
|
<ContractLeaderboard contract={contract} bets={bets} />
|
||||||
<ContractTopTrades contract={contract} bets={bets} />
|
<ContractTopTrades
|
||||||
|
contract={contract}
|
||||||
|
bets={bets}
|
||||||
|
comments={comments}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -13,11 +13,7 @@ import { Page } from 'web/components/page'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { doc, updateDoc } from 'firebase/firestore'
|
import { doc, updateDoc } from 'firebase/firestore'
|
||||||
import { db } from 'web/lib/firebase/init'
|
import { db } from 'web/lib/firebase/init'
|
||||||
import {
|
import { MANIFOLD_AVATAR_URL, PAST_BETS, PrivateUser } from 'common/user'
|
||||||
MANIFOLD_AVATAR_URL,
|
|
||||||
MANIFOLD_USER_USERNAME,
|
|
||||||
PrivateUser,
|
|
||||||
} from 'common/user'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
|
@ -736,6 +732,24 @@ function NotificationItem(props: {
|
||||||
justSummary={justSummary}
|
justSummary={justSummary}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (sourceType === 'badge') {
|
||||||
|
return (
|
||||||
|
<BadgeNotification
|
||||||
|
notification={notification}
|
||||||
|
isChildOfGroup={isChildOfGroup}
|
||||||
|
highlighted={highlighted}
|
||||||
|
justSummary={justSummary}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
||||||
|
return (
|
||||||
|
<MarketClosedNotification
|
||||||
|
notification={notification}
|
||||||
|
isChildOfGroup={isChildOfGroup}
|
||||||
|
highlighted={highlighted}
|
||||||
|
justSummary={justSummary}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// TODO Add new notification components here
|
// TODO Add new notification components here
|
||||||
|
|
||||||
|
@ -809,9 +823,16 @@ function NotificationFrame(props: {
|
||||||
subtitle: string
|
subtitle: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
isChildOfGroup?: boolean
|
isChildOfGroup?: boolean
|
||||||
|
showUserName?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { notification, isChildOfGroup, highlighted, subtitle, children } =
|
const {
|
||||||
props
|
notification,
|
||||||
|
isChildOfGroup,
|
||||||
|
highlighted,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
showUserName,
|
||||||
|
} = props
|
||||||
const {
|
const {
|
||||||
sourceType,
|
sourceType,
|
||||||
sourceUserName,
|
sourceUserName,
|
||||||
|
@ -822,7 +843,7 @@ function NotificationFrame(props: {
|
||||||
sourceUserUsername,
|
sourceUserUsername,
|
||||||
sourceText,
|
sourceText,
|
||||||
} = notification
|
} = notification
|
||||||
const questionNeedsResolution = sourceUpdateType == 'closed'
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const isMobile = (width ?? 0) < 600
|
const isMobile = (width ?? 0) < 600
|
||||||
return (
|
return (
|
||||||
|
@ -852,18 +873,10 @@ function NotificationFrame(props: {
|
||||||
/>
|
/>
|
||||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={
|
avatarUrl={sourceUserAvatarUrl}
|
||||||
questionNeedsResolution
|
|
||||||
? MANIFOLD_AVATAR_URL
|
|
||||||
: sourceUserAvatarUrl
|
|
||||||
}
|
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
className={'z-10 mr-2'}
|
className={'z-10 mr-2'}
|
||||||
username={
|
username={sourceUserUsername}
|
||||||
questionNeedsResolution
|
|
||||||
? MANIFOLD_USER_USERNAME
|
|
||||||
: sourceUserUsername
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className={'flex w-full flex-row pl-1 sm:pl-0'}>
|
<div className={'flex w-full flex-row pl-1 sm:pl-0'}>
|
||||||
<div
|
<div
|
||||||
|
@ -872,12 +885,14 @@ function NotificationFrame(props: {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
{showUserName && (
|
||||||
<UserLink
|
<UserLink
|
||||||
name={sourceUserName || ''}
|
name={sourceUserName || ''}
|
||||||
username={sourceUserUsername || ''}
|
username={sourceUserUsername || ''}
|
||||||
className={'relative mr-1 flex-shrink-0'}
|
className={'relative mr-1 flex-shrink-0'}
|
||||||
short={isMobile}
|
short={isMobile}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{subtitle}
|
{subtitle}
|
||||||
{isChildOfGroup ? (
|
{isChildOfGroup ? (
|
||||||
<RelativeTimestamp time={notification.createdTime} />
|
<RelativeTimestamp time={notification.createdTime} />
|
||||||
|
@ -965,6 +980,64 @@ function BetFillNotification(props: {
|
||||||
</NotificationFrame>
|
</NotificationFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function MarketClosedNotification(props: {
|
||||||
|
notification: Notification
|
||||||
|
highlighted: boolean
|
||||||
|
justSummary: boolean
|
||||||
|
isChildOfGroup?: boolean
|
||||||
|
}) {
|
||||||
|
const { notification, isChildOfGroup, highlighted, justSummary } = props
|
||||||
|
notification.sourceUserAvatarUrl = MANIFOLD_AVATAR_URL
|
||||||
|
return (
|
||||||
|
<NotificationFrame
|
||||||
|
notification={notification}
|
||||||
|
isChildOfGroup={isChildOfGroup}
|
||||||
|
highlighted={highlighted}
|
||||||
|
subtitle={'Please resolve'}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<span>
|
||||||
|
{`Your market has closed. Please resolve it to pay out ${PAST_BETS}.`}
|
||||||
|
</span>
|
||||||
|
</Row>
|
||||||
|
</NotificationFrame>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BadgeNotification(props: {
|
||||||
|
notification: Notification
|
||||||
|
highlighted: boolean
|
||||||
|
justSummary: boolean
|
||||||
|
isChildOfGroup?: boolean
|
||||||
|
}) {
|
||||||
|
const { notification, isChildOfGroup, highlighted, justSummary } = props
|
||||||
|
const { sourceText } = notification
|
||||||
|
const subtitle = 'You earned a new badge!'
|
||||||
|
notification.sourceUserAvatarUrl = '/award.svg'
|
||||||
|
if (justSummary) {
|
||||||
|
return (
|
||||||
|
<NotificationSummaryFrame notification={notification} subtitle={subtitle}>
|
||||||
|
<Row className={'line-clamp-1'}>
|
||||||
|
<span>{sourceText} 🎉</span>
|
||||||
|
</Row>
|
||||||
|
</NotificationSummaryFrame>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationFrame
|
||||||
|
notification={notification}
|
||||||
|
isChildOfGroup={isChildOfGroup}
|
||||||
|
highlighted={highlighted}
|
||||||
|
subtitle={subtitle}
|
||||||
|
showUserName={false}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<span>{sourceText} 🎉</span>
|
||||||
|
</Row>
|
||||||
|
</NotificationFrame>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ContractResolvedNotification(props: {
|
function ContractResolvedNotification(props: {
|
||||||
notification: Notification
|
notification: Notification
|
||||||
|
@ -1136,6 +1209,11 @@ function getSourceUrl(notification: Notification) {
|
||||||
sourceId ?? '',
|
sourceId ?? '',
|
||||||
sourceType
|
sourceType
|
||||||
)}`
|
)}`
|
||||||
|
else if (sourceSlug)
|
||||||
|
return `/${sourceSlug}#${getSourceIdForLinkComponent(
|
||||||
|
sourceId ?? '',
|
||||||
|
sourceType
|
||||||
|
)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceIdForLinkComponent(
|
function getSourceIdForLinkComponent(
|
||||||
|
@ -1235,7 +1313,6 @@ function getReasonForShowingNotification(
|
||||||
reasonText = justSummary ? 'asked the question' : 'asked'
|
reasonText = justSummary ? 'asked the question' : 'asked'
|
||||||
else if (sourceUpdateType === 'resolved')
|
else if (sourceUpdateType === 'resolved')
|
||||||
reasonText = justSummary ? `resolved the question` : `resolved`
|
reasonText = justSummary ? `resolved the question` : `resolved`
|
||||||
else if (sourceUpdateType === 'closed') reasonText = `Please resolve`
|
|
||||||
else reasonText = justSummary ? 'updated the question' : `updated`
|
else reasonText = justSummary ? 'updated the question' : `updated`
|
||||||
break
|
break
|
||||||
case 'answer':
|
case 'answer':
|
||||||
|
|
28
web/public/award.svg
Normal file
28
web/public/award.svg
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="color">
|
||||||
|
<polyline fill="#92d3f5" stroke="#92d3f5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="54.9988,4.0221 43,16.0208 36,16.0208 30.9584,10.9792 37.9207,4.0169 54.9988,4.0169"/>
|
||||||
|
<polyline fill="#ea5a47" stroke="#ea5a47" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="23.9831,4.0039 36,16.0208 29,16.0208 16.9675,3.9883 23.9831,3.9883"/>
|
||||||
|
<polyline fill="#fcea2b" stroke="none" points="28,22.4271 28,17 44,17 44,22.4271"/>
|
||||||
|
<circle cx="36" cy="45.0208" r="23" fill="#fcea2b" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<polygon fill="#f1b31c" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="35.9861,28 30.8575,38.4014 19.3815,40.0733 27.6891,48.1652 25.7329,59.5961 35.9958,54.1957 46.2628,59.5885 44.2981,48.159 52.5996,40.061 41.1225,38.3976"/>
|
||||||
|
</g>
|
||||||
|
<g id="hair"/>
|
||||||
|
<g id="skin"/>
|
||||||
|
<g id="skin-shadow"/>
|
||||||
|
<g id="line">
|
||||||
|
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="29" x2="29" y1="19" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="43" x2="43" y1="19" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="29" x2="43" y1="16.0208" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="25.9896" x2="16.9675" y1="13.0104" y2="3.9883" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="31.9896" x2="23.9831" y1="12.0104" y2="4.0039" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="34" x2="37.9207" y1="8" y2="4.0169" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="46" x2="54.9988" y1="13" y2="4.0221" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="16.9675" x2="23.9831" y1="3.9883" y2="3.9883" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<line x1="37.9207" x2="54.9988" y1="4.0169" y2="4.0169" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
|
||||||
|
<polygon fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="35.9861,28 30.8575,38.4014 19.3815,40.0733 27.6891,48.1652 25.7329,59.5961 35.9958,54.1957 46.2628,59.5885 44.2981,48.159 52.5996,40.061 41.1225,38.3976"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in New Issue
Block a user