manifold/functions/src/create-notification.ts

892 lines
27 KiB
TypeScript
Raw Normal View History

import * as admin from 'firebase-admin'
import {
getDestinationsForUser,
Notification,
notification_reason_types,
} from '../../common/notification'
import { User } from '../../common/user'
import { Contract } from '../../common/contract'
import { getPrivateUser, getValues } from './utils'
import { Comment } from '../../common/comment'
import { uniq } from 'lodash'
🧾 Limit orders! (#495) * Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-10 18:05:44 +00:00
import { Bet, LimitBet } from '../../common/bet'
import { Answer } from '../../common/answer'
import { getContractBetMetrics } from '../../common/calculate'
import { removeUndefinedProps } from '../../common/util/object'
import { TipTxn } from '../../common/txn'
import { Group } from '../../common/group'
Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 21:27:02 +00:00
import { Challenge } from '../../common/challenge'
import { Like } from '../../common/like'
import {
sendMarketCloseEmail,
sendMarketResolutionEmail,
sendNewAnswerEmail,
sendNewCommentEmail,
sendNewFollowedMarketEmail,
} from './emails'
import { filterDefined } from '../../common/util/array'
const firestore = admin.firestore()
type recipients_to_reason_texts = {
[userId: string]: { reason: notification_reason_types }
}
export const createNotification = async (
sourceId: string,
sourceType: 'contract' | 'liquidity' | 'follow',
sourceUpdateType: 'closed' | 'created',
sourceUser: User,
idempotencyKey: string,
sourceText: string,
miscData?: {
contract?: Contract
recipients?: string[]
slug?: string
title?: string
}
) => {
const { contract: sourceContract, recipients, slug, title } = miscData ?? {}
const shouldReceiveNotification = (
userId: string,
userToReasonTexts: recipients_to_reason_texts
) => {
return (
sourceUser.id != userId &&
!Object.keys(userToReasonTexts).includes(userId)
)
}
const sendNotificationsIfSettingsPermit = async (
userToReasonTexts: recipients_to_reason_texts
) => {
for (const userId in userToReasonTexts) {
const { reason } = userToReasonTexts[userId]
const privateUser = await getPrivateUser(userId)
if (!privateUser) continue
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
privateUser,
reason
)
if (sendToBrowser) {
const notificationRef = firestore
.collection(`/users/${userId}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId,
reason,
createdTime: Date.now(),
isSeen: false,
sourceId,
sourceType,
sourceUpdateType,
sourceContractId: sourceContract?.id,
sourceUserName: sourceUser.name,
sourceUserUsername: sourceUser.username,
sourceUserAvatarUrl: sourceUser.avatarUrl,
sourceText,
sourceContractCreatorUsername: sourceContract?.creatorUsername,
sourceContractTitle: sourceContract?.question,
sourceContractSlug: sourceContract?.slug,
sourceSlug: slug ? slug : sourceContract?.slug,
sourceTitle: title ? title : sourceContract?.question,
}
await notificationRef.set(removeUndefinedProps(notification))
}
if (!sendToEmail) continue
if (reason === 'your_contract_closed' && privateUser && sourceContract) {
// TODO: include number and names of bettors waiting for creator to resolve their market
await sendMarketCloseEmail(
reason,
sourceUser,
privateUser,
sourceContract
)
} else if (reason === 'subsidized_your_market') {
// TODO: send email to creator of market that was subsidized
} else if (reason === 'on_new_follow') {
// TODO: send email to user who was followed
}
}
}
// The following functions modify the userToReasonTexts object in place.
const userToReasonTexts: recipients_to_reason_texts = {}
if (sourceType === 'follow' && recipients?.[0]) {
if (shouldReceiveNotification(recipients[0], userToReasonTexts))
userToReasonTexts[recipients[0]] = {
reason: 'on_new_follow',
}
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
} else if (
sourceType === 'contract' &&
sourceUpdateType === 'closed' &&
sourceContract
) {
userToReasonTexts[sourceContract.creatorId] = {
reason: 'your_contract_closed',
}
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
} else if (
sourceType === 'liquidity' &&
sourceUpdateType === 'created' &&
sourceContract
) {
if (shouldReceiveNotification(sourceContract.creatorId, userToReasonTexts))
userToReasonTexts[sourceContract.creatorId] = {
reason: 'subsidized_your_market',
}
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
}
}
export type replied_users_info = {
[key: string]: {
repliedToType: 'comment' | 'answer'
repliedToAnswerText: string | undefined
repliedToId: string | undefined
bet: Bet | undefined
}
}
export const createCommentOrAnswerOrUpdatedContractNotification = async (
sourceId: string,
sourceType: 'comment' | 'answer' | 'contract',
sourceUpdateType: 'created' | 'updated' | 'resolved',
sourceUser: User,
idempotencyKey: string,
sourceText: string,
sourceContract: Contract,
miscData?: {
repliedUsersInfo: replied_users_info
taggedUserIds: string[]
},
resolutionData?: {
bets: Bet[]
userInvestments: { [userId: string]: number }
userPayouts: { [userId: string]: number }
creator: User
creatorPayout: number
contract: Contract
outcome: string
resolutionProbability?: number
resolutions?: { [outcome: string]: number }
}
) => {
const { repliedUsersInfo, taggedUserIds } = miscData ?? {}
2022-09-12 17:00:24 +00:00
const browserRecipientIdsList: string[] = []
const emailRecipientIdsList: string[] = []
const contractFollowersSnap = await firestore
.collection(`contracts/${sourceContract.id}/follows`)
.get()
const contractFollowersIds = contractFollowersSnap.docs.map(
(doc) => doc.data().id
)
const createBrowserNotification = async (
userId: string,
reason: notification_reason_types
) => {
const notificationRef = firestore
.collection(`/users/${userId}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId,
reason,
createdTime: Date.now(),
isSeen: false,
sourceId,
sourceType,
sourceUpdateType,
sourceContractId: sourceContract.id,
sourceUserName: sourceUser.name,
sourceUserUsername: sourceUser.username,
sourceUserAvatarUrl: sourceUser.avatarUrl,
sourceText,
sourceContractCreatorUsername: sourceContract.creatorUsername,
sourceContractTitle: sourceContract.question,
sourceContractSlug: sourceContract.slug,
sourceSlug: sourceContract.slug,
sourceTitle: sourceContract.question,
}
return await notificationRef.set(removeUndefinedProps(notification))
}
const stillFollowingContract = (userId: string) => {
return contractFollowersIds.includes(userId)
}
const sendNotificationsIfSettingsPermit = async (
userId: string,
reason: notification_reason_types
) => {
if (
!stillFollowingContract(sourceContract.creatorId) ||
2022-09-12 17:00:24 +00:00
sourceUser.id == userId
)
return
const privateUser = await getPrivateUser(userId)
if (!privateUser) return
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
privateUser,
reason
)
// Browser notifications
2022-09-12 17:00:24 +00:00
if (sendToBrowser && !browserRecipientIdsList.includes(userId)) {
await createBrowserNotification(userId, reason)
2022-09-12 17:00:24 +00:00
browserRecipientIdsList.push(userId)
}
// Emails notifications
if (!sendToEmail || emailRecipientIdsList.includes(userId)) return
if (sourceType === 'comment') {
const { repliedToType, repliedToAnswerText, repliedToId, bet } =
repliedUsersInfo?.[userId] ?? {}
// TODO: change subject of email title to be more specific, i.e.: replied to you on/tagged you on/comment
await sendNewCommentEmail(
reason,
privateUser,
sourceUser,
sourceContract,
sourceText,
sourceId,
bet,
repliedToAnswerText,
repliedToType === 'answer' ? repliedToId : undefined
)
emailRecipientIdsList.push(userId)
} else if (sourceType === 'answer') {
await sendNewAnswerEmail(
reason,
privateUser,
sourceUser.name,
sourceText,
sourceContract,
sourceUser.avatarUrl
)
emailRecipientIdsList.push(userId)
} else if (
sourceType === 'contract' &&
sourceUpdateType === 'resolved' &&
resolutionData
) {
await sendMarketResolutionEmail(
reason,
privateUser,
resolutionData.userInvestments[userId] ?? 0,
resolutionData.userPayouts[userId] ?? 0,
sourceUser,
resolutionData.creatorPayout,
sourceContract,
resolutionData.outcome,
resolutionData.resolutionProbability,
resolutionData.resolutions
)
2022-09-12 17:00:24 +00:00
emailRecipientIdsList.push(userId)
}
}
const notifyContractFollowers = async () => {
for (const userId of contractFollowersIds) {
await sendNotificationsIfSettingsPermit(
userId,
sourceType === 'answer'
? 'answer_on_contract_you_follow'
: sourceType === 'comment'
? 'comment_on_contract_you_follow'
: sourceUpdateType === 'updated'
? 'update_on_contract_you_follow'
: 'resolution_on_contract_you_follow'
)
}
}
const notifyContractCreator = async () => {
await sendNotificationsIfSettingsPermit(
sourceContract.creatorId,
sourceType === 'comment'
? 'comment_on_your_contract'
: 'answer_on_your_contract'
)
}
const notifyOtherAnswerersOnContract = async () => {
const answers = await getValues<Answer>(
firestore
.collection('contracts')
.doc(sourceContract.id)
.collection('answers')
)
const recipientUserIds = uniq(answers.map((answer) => answer.userId))
await Promise.all(
recipientUserIds.map((userId) =>
sendNotificationsIfSettingsPermit(
userId,
sourceType === 'answer'
? 'answer_on_contract_with_users_answer'
: sourceType === 'comment'
? 'comment_on_contract_with_users_answer'
: sourceUpdateType === 'updated'
? 'update_on_contract_with_users_answer'
: 'resolution_on_contract_with_users_answer'
)
)
)
}
const notifyOtherCommentersOnContract = async () => {
const comments = await getValues<Comment>(
firestore
.collection('contracts')
.doc(sourceContract.id)
.collection('comments')
)
const recipientUserIds = uniq(comments.map((comment) => comment.userId))
await Promise.all(
recipientUserIds.map((userId) =>
sendNotificationsIfSettingsPermit(
userId,
sourceType === 'answer'
? 'answer_on_contract_with_users_comment'
: sourceType === 'comment'
? 'comment_on_contract_with_users_comment'
: sourceUpdateType === 'updated'
? 'update_on_contract_with_users_comment'
: 'resolution_on_contract_with_users_comment'
)
)
)
}
const notifyBettorsOnContract = async () => {
const betsSnap = await firestore
.collection(`contracts/${sourceContract.id}/bets`)
.get()
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
// filter bets for only users that have an amount invested still
const recipientUserIds = uniq(bets.map((bet) => bet.userId)).filter(
(userId) => {
return (
getContractBetMetrics(
sourceContract,
bets.filter((bet) => bet.userId === userId)
).invested > 0
)
}
)
await Promise.all(
recipientUserIds.map((userId) =>
sendNotificationsIfSettingsPermit(
userId,
sourceType === 'answer'
? 'answer_on_contract_with_users_shares_in'
: sourceType === 'comment'
? 'comment_on_contract_with_users_shares_in'
: sourceUpdateType === 'updated'
? 'update_on_contract_with_users_shares_in'
: 'resolution_on_contract_with_users_shares_in'
)
)
)
}
const notifyRepliedUser = async () => {
if (sourceType === 'comment' && repliedUsersInfo)
await Promise.all(
Object.keys(repliedUsersInfo).map((userId) =>
sendNotificationsIfSettingsPermit(
userId,
repliedUsersInfo[userId].repliedToType === 'answer'
? 'reply_to_users_answer'
: 'reply_to_users_comment'
)
)
)
}
const notifyTaggedUsers = async () => {
if (sourceType === 'comment' && taggedUserIds && taggedUserIds.length > 0)
await Promise.all(
taggedUserIds.map((userId) =>
sendNotificationsIfSettingsPermit(userId, 'tagged_user')
)
)
}
const notifyLiquidityProviders = async () => {
const liquidityProviders = await firestore
.collection(`contracts/${sourceContract.id}/liquidity`)
.get()
const liquidityProvidersIds = uniq(
liquidityProviders.docs.map((doc) => doc.data().userId)
)
await Promise.all(
liquidityProvidersIds.map((userId) =>
sendNotificationsIfSettingsPermit(
userId,
sourceType === 'answer'
? 'answer_on_contract_with_users_shares_in'
: sourceType === 'comment'
? 'comment_on_contract_with_users_shares_in'
: sourceUpdateType === 'updated'
? 'update_on_contract_with_users_shares_in'
: 'resolution_on_contract_with_users_shares_in'
)
)
)
}
await notifyRepliedUser()
await notifyTaggedUsers()
await notifyContractCreator()
await notifyOtherAnswerersOnContract()
await notifyLiquidityProviders()
await notifyBettorsOnContract()
await notifyOtherCommentersOnContract()
// if they weren't notified previously, notify them now
await notifyContractFollowers()
}
export const createTipNotification = async (
fromUser: User,
toUser: User,
tip: TipTxn,
idempotencyKey: string,
commentId: string,
contract?: Contract,
group?: Group
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'tip_received'
)
if (!sendToBrowser) return
const slug = group ? group.slug + `#${commentId}` : commentId
const notificationRef = firestore
.collection(`/users/${toUser.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: toUser.id,
reason: 'tip_received',
createdTime: Date.now(),
isSeen: false,
sourceId: tip.id,
sourceType: 'tip',
sourceUpdateType: 'created',
sourceUserName: fromUser.name,
sourceUserUsername: fromUser.username,
sourceUserAvatarUrl: fromUser.avatarUrl,
sourceText: tip.amount.toString(),
sourceContractCreatorUsername: contract?.creatorUsername,
sourceContractTitle: contract?.question,
sourceContractSlug: contract?.slug,
sourceSlug: slug,
sourceTitle: group?.name,
}
return await notificationRef.set(removeUndefinedProps(notification))
// TODO: send notification to users that are watching the contract and want highly tipped comments only
// maybe TODO: send email notification to bet creator
}
🧾 Limit orders! (#495) * Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-10 18:05:44 +00:00
export const createBetFillNotification = async (
fromUser: User,
toUser: User,
bet: Bet,
userBet: LimitBet,
contract: Contract,
idempotencyKey: string
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'bet_fill'
)
if (!sendToBrowser) return
🧾 Limit orders! (#495) * Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-10 18:05:44 +00:00
const fill = userBet.fills.find((fill) => fill.matchedBetId === bet.id)
const fillAmount = fill?.amount ?? 0
const notificationRef = firestore
.collection(`/users/${toUser.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: toUser.id,
reason: 'bet_fill',
createdTime: Date.now(),
isSeen: false,
sourceId: userBet.id,
sourceType: 'bet',
sourceUpdateType: 'updated',
sourceUserName: fromUser.name,
sourceUserUsername: fromUser.username,
sourceUserAvatarUrl: fromUser.avatarUrl,
sourceText: fillAmount.toString(),
sourceContractCreatorUsername: contract.creatorUsername,
sourceContractTitle: contract.question,
sourceContractSlug: contract.slug,
sourceContractId: contract.id,
🧾 Limit orders! (#495) * Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-10 18:05:44 +00:00
}
return await notificationRef.set(removeUndefinedProps(notification))
// maybe TODO: send email notification to bet creator
}
2022-07-18 16:40:44 +00:00
export const createReferralNotification = async (
toUser: User,
referredUser: User,
idempotencyKey: string,
bonusAmount: string,
referredByContract?: Contract,
referredByGroup?: Group
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'you_referred_user'
)
if (!sendToBrowser) return
2022-07-18 16:40:44 +00:00
const notificationRef = firestore
.collection(`/users/${toUser.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: toUser.id,
reason: referredByGroup
? 'user_joined_from_your_group_invite'
: referredByContract?.creatorId === toUser.id
? 'user_joined_to_bet_on_your_market'
: 'you_referred_user',
createdTime: Date.now(),
isSeen: false,
sourceId: referredUser.id,
sourceType: 'user',
sourceUpdateType: 'updated',
sourceContractId: referredByContract?.id,
sourceUserName: referredUser.name,
sourceUserUsername: referredUser.username,
sourceUserAvatarUrl: referredUser.avatarUrl,
sourceText: bonusAmount,
// Only pass the contract referral details if they weren't referred to a group
sourceContractCreatorUsername: !referredByGroup
? referredByContract?.creatorUsername
: undefined,
sourceContractTitle: !referredByGroup
? referredByContract?.question
: undefined,
sourceContractSlug: !referredByGroup ? referredByContract?.slug : undefined,
sourceSlug: referredByGroup
? groupPath(referredByGroup.slug)
: referredByContract?.slug,
sourceTitle: referredByGroup
? referredByGroup.name
: referredByContract?.question,
}
await notificationRef.set(removeUndefinedProps(notification))
// TODO send email notification
2022-07-18 16:40:44 +00:00
}
export const createLoanIncomeNotification = async (
toUser: User,
idempotencyKey: string,
income: number
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'loan_income'
)
if (!sendToBrowser) return
const notificationRef = firestore
.collection(`/users/${toUser.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: toUser.id,
reason: 'loan_income',
createdTime: Date.now(),
isSeen: false,
sourceId: idempotencyKey,
sourceType: 'loan',
sourceUpdateType: 'updated',
sourceUserName: toUser.name,
sourceUserUsername: toUser.username,
sourceUserAvatarUrl: toUser.avatarUrl,
sourceText: income.toString(),
sourceTitle: 'Loan',
}
await notificationRef.set(removeUndefinedProps(notification))
}
2022-07-18 16:40:44 +00:00
const groupPath = (groupSlug: string) => `/group/${groupSlug}`
Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 21:27:02 +00:00
export const createChallengeAcceptedNotification = async (
challenger: User,
challengeCreator: User,
challenge: Challenge,
acceptedAmount: number,
contract: Contract
) => {
const privateUser = await getPrivateUser(challengeCreator.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'challenge_accepted'
)
if (!sendToBrowser) return
Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 21:27:02 +00:00
const notificationRef = firestore
.collection(`/users/${challengeCreator.id}/notifications`)
.doc()
const notification: Notification = {
id: notificationRef.id,
userId: challengeCreator.id,
reason: 'challenge_accepted',
createdTime: Date.now(),
isSeen: false,
sourceId: challenge.slug,
sourceType: 'challenge',
sourceUpdateType: 'updated',
sourceUserName: challenger.name,
sourceUserUsername: challenger.username,
sourceUserAvatarUrl: challenger.avatarUrl,
sourceText: acceptedAmount.toString(),
sourceContractCreatorUsername: contract.creatorUsername,
sourceContractTitle: contract.question,
sourceContractSlug: contract.slug,
sourceContractId: contract.id,
sourceSlug: `/challenges/${challengeCreator.username}/${challenge.contractSlug}/${challenge.slug}`,
}
return await notificationRef.set(removeUndefinedProps(notification))
}
export const createBettingStreakBonusNotification = async (
user: User,
txnId: string,
bet: Bet,
contract: Contract,
amount: number,
idempotencyKey: string
) => {
const privateUser = await getPrivateUser(user.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'betting_streak_incremented'
)
if (!sendToBrowser) return
const notificationRef = firestore
.collection(`/users/${user.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: user.id,
reason: 'betting_streak_incremented',
createdTime: Date.now(),
isSeen: false,
sourceId: txnId,
sourceType: 'betting_streak_bonus',
sourceUpdateType: 'created',
sourceUserName: user.name,
sourceUserUsername: user.username,
sourceUserAvatarUrl: user.avatarUrl,
sourceText: amount.toString(),
sourceSlug: `/${contract.creatorUsername}/${contract.slug}/bets/${bet.id}`,
sourceTitle: 'Betting Streak Bonus',
// Perhaps not necessary, but just in case
sourceContractSlug: contract.slug,
sourceContractId: contract.id,
sourceContractTitle: contract.question,
sourceContractCreatorUsername: contract.creatorUsername,
}
return await notificationRef.set(removeUndefinedProps(notification))
}
export const createLikeNotification = async (
fromUser: User,
toUser: User,
like: Like,
idempotencyKey: string,
contract: Contract,
tip?: TipTxn
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'liked_and_tipped_your_contract'
)
if (!sendToBrowser) return
// not handling just likes, must include tip
if (!tip) return
const notificationRef = firestore
.collection(`/users/${toUser.id}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: toUser.id,
reason: 'liked_and_tipped_your_contract',
createdTime: Date.now(),
isSeen: false,
sourceId: like.id,
sourceType: tip ? 'tip_and_like' : 'like',
sourceUpdateType: 'created',
sourceUserName: fromUser.name,
sourceUserUsername: fromUser.username,
sourceUserAvatarUrl: fromUser.avatarUrl,
sourceText: tip?.amount.toString(),
sourceContractCreatorUsername: contract.creatorUsername,
sourceContractTitle: contract.question,
sourceContractSlug: contract.slug,
sourceSlug: contract.slug,
sourceTitle: contract.question,
}
return await notificationRef.set(removeUndefinedProps(notification))
// TODO send email notification
}
export const createUniqueBettorBonusNotification = async (
contractCreatorId: string,
bettor: User,
txnId: string,
contract: Contract,
amount: number,
idempotencyKey: string
) => {
console.log('createUniqueBettorBonusNotification')
const privateUser = await getPrivateUser(contractCreatorId)
if (!privateUser) return
const { sendToBrowser } = await getDestinationsForUser(
privateUser,
'unique_bettors_on_your_contract'
)
if (!sendToBrowser) return
const notificationRef = firestore
.collection(`/users/${contractCreatorId}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: contractCreatorId,
reason: 'unique_bettors_on_your_contract',
createdTime: Date.now(),
isSeen: false,
sourceId: txnId,
sourceType: 'bonus',
sourceUpdateType: 'created',
sourceUserName: bettor.name,
sourceUserUsername: bettor.username,
sourceUserAvatarUrl: bettor.avatarUrl,
sourceText: amount.toString(),
sourceSlug: contract.slug,
sourceTitle: contract.question,
// Perhaps not necessary, but just in case
sourceContractSlug: contract.slug,
sourceContractId: contract.id,
sourceContractTitle: contract.question,
sourceContractCreatorUsername: contract.creatorUsername,
}
return await notificationRef.set(removeUndefinedProps(notification))
// TODO send email notification
}
export const createNewContractNotification = async (
contractCreator: User,
contract: Contract,
idempotencyKey: string,
text: string,
mentionedUserIds: string[]
) => {
if (contract.visibility !== 'public') return
const sendNotificationsIfSettingsAllow = async (
userId: string,
reason: notification_reason_types
) => {
const privateUser = await getPrivateUser(userId)
if (!privateUser) return
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
privateUser,
reason
)
if (sendToBrowser) {
const notificationRef = firestore
.collection(`/users/${userId}/notifications`)
.doc(idempotencyKey)
const notification: Notification = {
id: idempotencyKey,
userId: userId,
reason,
createdTime: Date.now(),
isSeen: false,
sourceId: contract.id,
sourceType: 'contract',
sourceUpdateType: 'created',
sourceUserName: contractCreator.name,
sourceUserUsername: contractCreator.username,
sourceUserAvatarUrl: contractCreator.avatarUrl,
sourceText: text,
sourceSlug: contract.slug,
sourceTitle: contract.question,
sourceContractSlug: contract.slug,
sourceContractId: contract.id,
sourceContractTitle: contract.question,
sourceContractCreatorUsername: contract.creatorUsername,
}
await notificationRef.set(removeUndefinedProps(notification))
}
if (!sendToEmail) return
if (reason === 'contract_from_followed_user')
await sendNewFollowedMarketEmail(reason, userId, privateUser, contract)
}
const followersSnapshot = await firestore
.collectionGroup('follows')
.where('userId', '==', contractCreator.id)
.get()
const followerUserIds = filterDefined(
followersSnapshot.docs.map((doc) => {
const followerUserId = doc.ref.parent.parent?.id
return followerUserId && followerUserId != contractCreator.id
? followerUserId
: undefined
})
)
// As it is coded now, the tag notification usurps the new contract notification
// It'd be easy to append the reason to the eventId if desired
for (const followerUserId of followerUserIds) {
await sendNotificationsIfSettingsAllow(
followerUserId,
'contract_from_followed_user'
)
}
for (const mentionedUserId of mentionedUserIds) {
await sendNotificationsIfSettingsAllow(mentionedUserId, 'tagged_user')
}
}