Allow to follow/unfollow markets, backfill as well
This commit is contained in:
parent
bea94d58c5
commit
89644502a5
|
@ -57,6 +57,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
uniqueBettorIds?: string[]
|
uniqueBettorIds?: string[]
|
||||||
uniqueBettorCount?: number
|
uniqueBettorCount?: number
|
||||||
popularityScore?: number
|
popularityScore?: number
|
||||||
|
followerCount?: number
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
export type BinaryContract = Contract & Binary
|
export type BinaryContract = Contract & Binary
|
||||||
|
|
|
@ -70,3 +70,4 @@ export type notification_reason_types =
|
||||||
| 'challenge_accepted'
|
| 'challenge_accepted'
|
||||||
| 'betting_streak_incremented'
|
| 'betting_streak_incremented'
|
||||||
| 'loan_income'
|
| 'loan_income'
|
||||||
|
| 'you_follow_contract'
|
||||||
|
|
|
@ -44,6 +44,11 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /contracts/{contractId}/follows/{userId} {
|
||||||
|
allow read;
|
||||||
|
allow create, delete: if userId == request.auth.uid;
|
||||||
|
}
|
||||||
|
|
||||||
match /contracts/{contractId}/challenges/{challengeId}{
|
match /contracts/{contractId}/challenges/{challengeId}{
|
||||||
allow read;
|
allow read;
|
||||||
allow create: if request.auth.uid == request.resource.data.creatorId;
|
allow create: if request.auth.uid == request.resource.data.creatorId;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"deploy": "firebase deploy --only functions",
|
"deploy": "firebase deploy --only functions",
|
||||||
"logs": "firebase functions:log",
|
"logs": "firebase functions:log",
|
||||||
"dev": "nodemon src/serve.ts",
|
"dev": "nodemon src/serve.ts",
|
||||||
"firestore": "firebase emulators:start --only firestore --import=./firestore_export",
|
"firestore": "firebase emulators:start --only firestore,pubsub --import=./firestore_export",
|
||||||
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
|
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
|
||||||
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
||||||
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from '../../common/notification'
|
} from '../../common/notification'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { getValues } from './utils'
|
import { getValues, log } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { Bet, LimitBet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
|
@ -33,19 +33,12 @@ export const createNotification = async (
|
||||||
sourceText: string,
|
sourceText: string,
|
||||||
miscData?: {
|
miscData?: {
|
||||||
contract?: Contract
|
contract?: Contract
|
||||||
relatedSourceType?: notification_source_types
|
|
||||||
recipients?: string[]
|
recipients?: string[]
|
||||||
slug?: string
|
slug?: string
|
||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { contract: sourceContract, recipients, slug, title } = miscData ?? {}
|
||||||
contract: sourceContract,
|
|
||||||
relatedSourceType,
|
|
||||||
recipients,
|
|
||||||
slug,
|
|
||||||
title,
|
|
||||||
} = miscData ?? {}
|
|
||||||
|
|
||||||
const shouldGetNotification = (
|
const shouldGetNotification = (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -90,24 +83,6 @@ export const createNotification = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyLiquidityProviders = async (
|
|
||||||
userToReasonTexts: user_to_reason_texts,
|
|
||||||
contract: Contract
|
|
||||||
) => {
|
|
||||||
const liquidityProviders = await firestore
|
|
||||||
.collection(`contracts/${contract.id}/liquidity`)
|
|
||||||
.get()
|
|
||||||
const liquidityProvidersIds = uniq(
|
|
||||||
liquidityProviders.docs.map((doc) => doc.data().userId)
|
|
||||||
)
|
|
||||||
liquidityProvidersIds.forEach((userId) => {
|
|
||||||
if (!shouldGetNotification(userId, userToReasonTexts)) return
|
|
||||||
userToReasonTexts[userId] = {
|
|
||||||
reason: 'on_contract_with_users_shares_in',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyUsersFollowers = async (
|
const notifyUsersFollowers = async (
|
||||||
userToReasonTexts: user_to_reason_texts
|
userToReasonTexts: user_to_reason_texts
|
||||||
) => {
|
) => {
|
||||||
|
@ -129,23 +104,6 @@ export const createNotification = async (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyRepliedUser = (
|
|
||||||
userToReasonTexts: user_to_reason_texts,
|
|
||||||
relatedUserId: string,
|
|
||||||
relatedSourceType: notification_source_types
|
|
||||||
) => {
|
|
||||||
if (!shouldGetNotification(relatedUserId, userToReasonTexts)) return
|
|
||||||
if (relatedSourceType === 'comment') {
|
|
||||||
userToReasonTexts[relatedUserId] = {
|
|
||||||
reason: 'reply_to_users_comment',
|
|
||||||
}
|
|
||||||
} else if (relatedSourceType === 'answer') {
|
|
||||||
userToReasonTexts[relatedUserId] = {
|
|
||||||
reason: 'reply_to_users_answer',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyFollowedUser = (
|
const notifyFollowedUser = (
|
||||||
userToReasonTexts: user_to_reason_texts,
|
userToReasonTexts: user_to_reason_texts,
|
||||||
followedUserId: string
|
followedUserId: string
|
||||||
|
@ -182,71 +140,6 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyOtherAnswerersOnContract = async (
|
|
||||||
userToReasonTexts: user_to_reason_texts,
|
|
||||||
sourceContract: Contract
|
|
||||||
) => {
|
|
||||||
const answers = await getValues<Answer>(
|
|
||||||
firestore
|
|
||||||
.collection('contracts')
|
|
||||||
.doc(sourceContract.id)
|
|
||||||
.collection('answers')
|
|
||||||
)
|
|
||||||
const recipientUserIds = uniq(answers.map((answer) => answer.userId))
|
|
||||||
recipientUserIds.forEach((userId) => {
|
|
||||||
if (shouldGetNotification(userId, userToReasonTexts))
|
|
||||||
userToReasonTexts[userId] = {
|
|
||||||
reason: 'on_contract_with_users_answer',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyOtherCommentersOnContract = async (
|
|
||||||
userToReasonTexts: user_to_reason_texts,
|
|
||||||
sourceContract: Contract
|
|
||||||
) => {
|
|
||||||
const comments = await getValues<Comment>(
|
|
||||||
firestore
|
|
||||||
.collection('contracts')
|
|
||||||
.doc(sourceContract.id)
|
|
||||||
.collection('comments')
|
|
||||||
)
|
|
||||||
const recipientUserIds = uniq(comments.map((comment) => comment.userId))
|
|
||||||
recipientUserIds.forEach((userId) => {
|
|
||||||
if (shouldGetNotification(userId, userToReasonTexts))
|
|
||||||
userToReasonTexts[userId] = {
|
|
||||||
reason: 'on_contract_with_users_comment',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyBettorsOnContract = async (
|
|
||||||
userToReasonTexts: user_to_reason_texts,
|
|
||||||
sourceContract: Contract
|
|
||||||
) => {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
recipientUserIds.forEach((userId) => {
|
|
||||||
if (shouldGetNotification(userId, userToReasonTexts))
|
|
||||||
userToReasonTexts[userId] = {
|
|
||||||
reason: 'on_contract_with_users_shares_in',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyUserAddedToGroup = (
|
const notifyUserAddedToGroup = (
|
||||||
userToReasonTexts: user_to_reason_texts,
|
userToReasonTexts: user_to_reason_texts,
|
||||||
relatedUserId: string
|
relatedUserId: string
|
||||||
|
@ -266,58 +159,289 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsersToNotify = async () => {
|
const userToReasonTexts: user_to_reason_texts = {}
|
||||||
const userToReasonTexts: user_to_reason_texts = {}
|
// The following functions modify the userToReasonTexts object in place.
|
||||||
// The following functions modify the userToReasonTexts object in place.
|
|
||||||
if (sourceType === 'follow' && recipients?.[0]) {
|
|
||||||
notifyFollowedUser(userToReasonTexts, recipients[0])
|
|
||||||
} else if (
|
|
||||||
sourceType === 'group' &&
|
|
||||||
sourceUpdateType === 'created' &&
|
|
||||||
recipients
|
|
||||||
) {
|
|
||||||
recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following functions need sourceContract to be defined.
|
if (sourceType === 'follow' && recipients?.[0]) {
|
||||||
if (!sourceContract) return userToReasonTexts
|
notifyFollowedUser(userToReasonTexts, recipients[0])
|
||||||
|
} else if (
|
||||||
if (
|
sourceType === 'group' &&
|
||||||
sourceType === 'comment' ||
|
sourceUpdateType === 'created' &&
|
||||||
sourceType === 'answer' ||
|
recipients
|
||||||
(sourceType === 'contract' &&
|
) {
|
||||||
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r))
|
||||||
) {
|
} else if (
|
||||||
if (sourceType === 'comment') {
|
sourceType === 'contract' &&
|
||||||
if (recipients?.[0] && relatedSourceType)
|
sourceUpdateType === 'created' &&
|
||||||
notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType)
|
sourceContract
|
||||||
if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
) {
|
||||||
}
|
await notifyUsersFollowers(userToReasonTexts)
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
||||||
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
} else if (
|
||||||
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
sourceType === 'contract' &&
|
||||||
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
sourceUpdateType === 'closed' &&
|
||||||
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
sourceContract
|
||||||
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
) {
|
||||||
await notifyUsersFollowers(userToReasonTexts)
|
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||||
notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
force: true,
|
||||||
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
})
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
} else if (
|
||||||
force: true,
|
sourceType === 'liquidity' &&
|
||||||
})
|
sourceUpdateType === 'created' &&
|
||||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
sourceContract
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
) {
|
||||||
} else if (sourceType === 'bonus' && sourceUpdateType === 'created') {
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||||
// Note: the daily bonus won't have a contract attached to it
|
} else if (
|
||||||
await notifyContractCreatorOfUniqueBettorsBonus(
|
sourceType === 'bonus' &&
|
||||||
userToReasonTexts,
|
sourceUpdateType === 'created' &&
|
||||||
sourceContract.creatorId
|
sourceContract
|
||||||
)
|
) {
|
||||||
}
|
// Note: the daily bonus won't have a contract attached to it
|
||||||
return userToReasonTexts
|
await notifyContractCreatorOfUniqueBettorsBonus(
|
||||||
|
userToReasonTexts,
|
||||||
|
sourceContract.creatorId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userToReasonTexts = await getUsersToNotify()
|
await createUsersNotifications(userToReasonTexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCommentOrAnswerOrUpdatedContractNotification = async (
|
||||||
|
sourceId: string,
|
||||||
|
sourceType: notification_source_types,
|
||||||
|
sourceUpdateType: notification_source_update_types,
|
||||||
|
sourceUser: User,
|
||||||
|
idempotencyKey: string,
|
||||||
|
sourceText: string,
|
||||||
|
sourceContract: Contract,
|
||||||
|
miscData?: {
|
||||||
|
relatedSourceType?: notification_source_types
|
||||||
|
repliedUserId?: string
|
||||||
|
taggedUserIds?: string[]
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { relatedSourceType, repliedUserId, taggedUserIds } = miscData ?? {}
|
||||||
|
|
||||||
|
const createUsersNotifications = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(userToReasonTexts).map(async (userId) => {
|
||||||
|
const notificationRef = firestore
|
||||||
|
.collection(`/users/${userId}/notifications`)
|
||||||
|
.doc(idempotencyKey)
|
||||||
|
const notification: Notification = {
|
||||||
|
id: idempotencyKey,
|
||||||
|
userId,
|
||||||
|
reason: userToReasonTexts[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,
|
||||||
|
}
|
||||||
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get contract follower documents and check here if they're a follower
|
||||||
|
const contractFollowersSnap = await firestore
|
||||||
|
.collection(`contracts/${sourceContract.id}/follows`)
|
||||||
|
.get()
|
||||||
|
const contractFollowersIds = contractFollowersSnap.docs.map(
|
||||||
|
(doc) => doc.data().id
|
||||||
|
)
|
||||||
|
log('contractFollowerIds', contractFollowersIds)
|
||||||
|
|
||||||
|
const stillFollowingContract = (userId: string) => {
|
||||||
|
return contractFollowersIds.includes(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldGetNotification = (
|
||||||
|
userId: string,
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
sourceUser.id != userId &&
|
||||||
|
!Object.keys(userToReasonTexts).includes(userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyContractFollowers = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
for (const userId of contractFollowersIds) {
|
||||||
|
if (shouldGetNotification(userId, userToReasonTexts))
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'you_follow_contract',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyContractCreator = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(sourceContract.creatorId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(sourceContract.creatorId)
|
||||||
|
)
|
||||||
|
userToReasonTexts[sourceContract.creatorId] = {
|
||||||
|
reason: 'on_users_contract',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyOtherAnswerersOnContract = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
const answers = await getValues<Answer>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.doc(sourceContract.id)
|
||||||
|
.collection('answers')
|
||||||
|
)
|
||||||
|
const recipientUserIds = uniq(answers.map((answer) => answer.userId))
|
||||||
|
recipientUserIds.forEach((userId) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(userId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(userId)
|
||||||
|
)
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_contract_with_users_answer',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyOtherCommentersOnContract = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
const comments = await getValues<Comment>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.doc(sourceContract.id)
|
||||||
|
.collection('comments')
|
||||||
|
)
|
||||||
|
const recipientUserIds = uniq(comments.map((comment) => comment.userId))
|
||||||
|
recipientUserIds.forEach((userId) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(userId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(userId)
|
||||||
|
)
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_contract_with_users_comment',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyBettorsOnContract = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
recipientUserIds.forEach((userId) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(userId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(userId)
|
||||||
|
)
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_contract_with_users_shares_in',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyRepliedUser = (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
relatedUserId: string,
|
||||||
|
relatedSourceType: notification_source_types
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(relatedUserId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(relatedUserId)
|
||||||
|
) {
|
||||||
|
if (relatedSourceType === 'comment') {
|
||||||
|
userToReasonTexts[relatedUserId] = {
|
||||||
|
reason: 'reply_to_users_comment',
|
||||||
|
}
|
||||||
|
} else if (relatedSourceType === 'answer') {
|
||||||
|
userToReasonTexts[relatedUserId] = {
|
||||||
|
reason: 'reply_to_users_answer',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyTaggedUsers = (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
userIds: (string | undefined)[]
|
||||||
|
) => {
|
||||||
|
userIds.forEach((id) => {
|
||||||
|
console.log('tagged user: ', id)
|
||||||
|
// Allowing non-following users to get tagged
|
||||||
|
if (id && shouldGetNotification(id, userToReasonTexts))
|
||||||
|
userToReasonTexts[id] = {
|
||||||
|
reason: 'tagged_user',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyLiquidityProviders = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
) => {
|
||||||
|
const liquidityProviders = await firestore
|
||||||
|
.collection(`contracts/${sourceContract.id}/liquidity`)
|
||||||
|
.get()
|
||||||
|
const liquidityProvidersIds = uniq(
|
||||||
|
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||||
|
)
|
||||||
|
liquidityProvidersIds.forEach((userId) => {
|
||||||
|
if (
|
||||||
|
shouldGetNotification(userId, userToReasonTexts) &&
|
||||||
|
stillFollowingContract(userId)
|
||||||
|
) {
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_contract_with_users_shares_in',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const userToReasonTexts: user_to_reason_texts = {}
|
||||||
|
|
||||||
|
if (sourceType === 'comment') {
|
||||||
|
if (repliedUserId && relatedSourceType)
|
||||||
|
notifyRepliedUser(userToReasonTexts, repliedUserId, relatedSourceType)
|
||||||
|
if (sourceText) notifyTaggedUsers(userToReasonTexts, taggedUserIds ?? [])
|
||||||
|
}
|
||||||
|
await notifyContractCreator(userToReasonTexts)
|
||||||
|
await notifyOtherAnswerersOnContract(userToReasonTexts)
|
||||||
|
await notifyLiquidityProviders(userToReasonTexts)
|
||||||
|
await notifyBettorsOnContract(userToReasonTexts)
|
||||||
|
await notifyOtherCommentersOnContract(userToReasonTexts)
|
||||||
|
// if they weren't added previously, add them now
|
||||||
|
await notifyContractFollowers(userToReasonTexts)
|
||||||
|
|
||||||
await createUsersNotifications(userToReasonTexts)
|
await createUsersNotifications(userToReasonTexts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
functions/src/follow-market.ts
Normal file
30
functions/src/follow-market.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Contract } from '../../common/lib/contract'
|
||||||
|
import { User } from '../../common/lib/user'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { FieldValue } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const addUserToContractFollowers = async (
|
||||||
|
contract: Contract,
|
||||||
|
user: User
|
||||||
|
) => {
|
||||||
|
const followerDoc = await firestore
|
||||||
|
.collection(`contracts/${contract.id}/follows`)
|
||||||
|
.doc(user.id)
|
||||||
|
.get()
|
||||||
|
if (followerDoc.exists) return
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts/${contract.id}/follows`)
|
||||||
|
.doc(user.id)
|
||||||
|
.set({
|
||||||
|
id: user.id,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
})
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.doc(contract.id)
|
||||||
|
.update({
|
||||||
|
followerCount: FieldValue.increment(1),
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { getContract, getUser } from './utils'
|
import { getContract, getUser } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
|
||||||
export const onCreateAnswer = functions.firestore
|
export const onCreateAnswer = functions.firestore
|
||||||
.document('contracts/{contractId}/answers/{answerNumber}')
|
.document('contracts/{contractId}/answers/{answerNumber}')
|
||||||
|
@ -20,14 +21,14 @@ export const onCreateAnswer = functions.firestore
|
||||||
|
|
||||||
const answerCreator = await getUser(answer.userId)
|
const answerCreator = await getUser(answer.userId)
|
||||||
if (!answerCreator) throw new Error('Could not find answer creator')
|
if (!answerCreator) throw new Error('Could not find answer creator')
|
||||||
|
await addUserToContractFollowers(contract, answerCreator)
|
||||||
await createNotification(
|
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||||
answer.id,
|
answer.id,
|
||||||
'answer',
|
'answer',
|
||||||
'created',
|
'created',
|
||||||
answerCreator,
|
answerCreator,
|
||||||
eventId,
|
eventId,
|
||||||
answer.text,
|
answer.text,
|
||||||
{ contract }
|
contract
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { APIError } from '../../common/api'
|
import { APIError } from '../../common/api'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||||
|
@ -54,14 +55,13 @@ export const onCreateBet = functions.firestore
|
||||||
log(`Could not find contract ${contractId}`)
|
log(`Could not find contract ${contractId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
|
||||||
|
|
||||||
const bettor = await getUser(bet.userId)
|
const bettor = await getUser(bet.userId)
|
||||||
if (!bettor) return
|
if (!bettor) return
|
||||||
|
|
||||||
|
await addUserToContractFollowers(contract, bettor)
|
||||||
|
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||||
await notifyFills(bet, contract, eventId, bettor)
|
await notifyFills(bet, contract, eventId, bettor)
|
||||||
await updateBettingStreak(bettor, bet, contract, eventId)
|
await updateBettingStreak(bettor, bet, contract, eventId)
|
||||||
|
|
||||||
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,9 @@ import { ContractComment } from '../../common/comment'
|
||||||
import { sendNewCommentEmail } from './emails'
|
import { sendNewCommentEmail } from './emails'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { createNotification } from './create-notification'
|
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||||
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ export const onCreateCommentOnContract = functions
|
||||||
const commentCreator = await getUser(comment.userId)
|
const commentCreator = await getUser(comment.userId)
|
||||||
if (!commentCreator) throw new Error('Could not find comment creator')
|
if (!commentCreator) throw new Error('Could not find comment creator')
|
||||||
|
|
||||||
|
await addUserToContractFollowers(contract, commentCreator)
|
||||||
|
|
||||||
await firestore
|
await firestore
|
||||||
.collection('contracts')
|
.collection('contracts')
|
||||||
.doc(contract.id)
|
.doc(contract.id)
|
||||||
|
@ -77,18 +80,19 @@ export const onCreateCommentOnContract = functions
|
||||||
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
||||||
: answer?.userId
|
: answer?.userId
|
||||||
|
|
||||||
const recipients = uniq(
|
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||||
compact([...parseMentions(comment.content), repliedUserId])
|
|
||||||
)
|
|
||||||
|
|
||||||
await createNotification(
|
|
||||||
comment.id,
|
comment.id,
|
||||||
'comment',
|
'comment',
|
||||||
'created',
|
'created',
|
||||||
commentCreator,
|
commentCreator,
|
||||||
eventId,
|
eventId,
|
||||||
richTextToString(comment.content),
|
richTextToString(comment.content),
|
||||||
{ contract, relatedSourceType, recipients }
|
contract,
|
||||||
|
{
|
||||||
|
relatedSourceType,
|
||||||
|
repliedUserId,
|
||||||
|
taggedUserIds: compact(parseMentions(comment.content)),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const recipientUserIds = uniq([
|
const recipientUserIds = uniq([
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createNotification } 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'
|
||||||
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
|
||||||
export const onCreateContract = functions
|
export const onCreateContract = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
@ -18,6 +19,7 @@ export const onCreateContract = functions
|
||||||
|
|
||||||
const desc = contract.description as JSONContent
|
const desc = contract.description as JSONContent
|
||||||
const mentioned = parseMentions(desc)
|
const mentioned = parseMentions(desc)
|
||||||
|
await addUserToContractFollowers(contract, contractCreator)
|
||||||
|
|
||||||
await createNotification(
|
await createNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as functions from 'firebase-functions'
|
||||||
import { getContract, getUser } from './utils'
|
import { getContract, getUser } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createNotification } from './create-notification'
|
||||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||||
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
|
||||||
export const onCreateLiquidityProvision = functions.firestore
|
export const onCreateLiquidityProvision = functions.firestore
|
||||||
.document('contracts/{contractId}/liquidity/{liquidityId}')
|
.document('contracts/{contractId}/liquidity/{liquidityId}')
|
||||||
|
@ -18,6 +19,7 @@ export const onCreateLiquidityProvision = functions.firestore
|
||||||
|
|
||||||
const liquidityProvider = await getUser(liquidity.userId)
|
const liquidityProvider = await getUser(liquidity.userId)
|
||||||
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||||
|
await addUserToContractFollowers(contract, liquidityProvider)
|
||||||
|
|
||||||
await createNotification(
|
await createNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
|
||||||
export const onUpdateContract = functions.firestore
|
export const onUpdateContract = functions.firestore
|
||||||
|
@ -29,14 +29,14 @@ export const onUpdateContract = functions.firestore
|
||||||
resolutionText = `${contract.resolutionValue}`
|
resolutionText = `${contract.resolutionValue}`
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNotification(
|
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
'contract',
|
'contract',
|
||||||
'resolved',
|
'resolved',
|
||||||
contractUpdater,
|
contractUpdater,
|
||||||
eventId,
|
eventId,
|
||||||
resolutionText,
|
resolutionText,
|
||||||
{ contract }
|
contract
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
previousValue.closeTime !== contract.closeTime ||
|
previousValue.closeTime !== contract.closeTime ||
|
||||||
|
@ -52,14 +52,14 @@ export const onUpdateContract = functions.firestore
|
||||||
sourceText = contract.question
|
sourceText = contract.question
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNotification(
|
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
'contract',
|
'contract',
|
||||||
'updated',
|
'updated',
|
||||||
contractUpdater,
|
contractUpdater,
|
||||||
eventId,
|
eventId,
|
||||||
sourceText,
|
sourceText,
|
||||||
{ contract }
|
contract
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
74
functions/src/scripts/backfill-contract-followers.ts
Normal file
74
functions/src/scripts/backfill-contract-followers.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
initAdmin()
|
||||||
|
|
||||||
|
import { getValues } from '../utils'
|
||||||
|
import { Contract } from 'common/lib/contract'
|
||||||
|
import { Comment } from 'common/lib/comment'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import { Bet } from 'common/lib/bet'
|
||||||
|
import {
|
||||||
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
} from 'common/lib/antes'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
async function backfillContractFollowers() {
|
||||||
|
console.log('Backfilling contract followers')
|
||||||
|
const contracts = await getValues<Contract>(
|
||||||
|
firestore.collection('contracts').where('isResolved', '==', false)
|
||||||
|
)
|
||||||
|
let count = 0
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const comments = await getValues<Comment>(
|
||||||
|
firestore.collection('contracts').doc(contract.id).collection('comments')
|
||||||
|
)
|
||||||
|
const commenterIds = uniq(comments.map((comment) => comment.userId))
|
||||||
|
const betsSnap = await firestore
|
||||||
|
.collection(`contracts/${contract.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 bettorIds = uniq(bets.map((bet) => bet.userId))
|
||||||
|
const liquidityProviders = await firestore
|
||||||
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
|
.get()
|
||||||
|
const liquidityProvidersIds = uniq(
|
||||||
|
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||||
|
// exclude free market liquidity provider
|
||||||
|
).filter(
|
||||||
|
(id) =>
|
||||||
|
id !== HOUSE_LIQUIDITY_PROVIDER_ID ||
|
||||||
|
id !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
)
|
||||||
|
const followerIds = uniq([
|
||||||
|
...commenterIds,
|
||||||
|
...bettorIds,
|
||||||
|
...liquidityProvidersIds,
|
||||||
|
contract.creatorId,
|
||||||
|
])
|
||||||
|
for (const followerId of followerIds) {
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts/${contract.id}/follows`)
|
||||||
|
.doc(followerId)
|
||||||
|
.set({ id: followerId, createdTime: Date.now() })
|
||||||
|
}
|
||||||
|
const followerCount = followerIds.length
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.doc(contract.id)
|
||||||
|
.update({ followerCount: followerCount })
|
||||||
|
count += 1
|
||||||
|
if (count % 100 === 0) {
|
||||||
|
console.log(`${count} contracts processed`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
backfillContractFollowers()
|
||||||
|
.then(() => process.exit())
|
||||||
|
.catch(console.log)
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { ShareIcon } from '@heroicons/react/outline'
|
import { EyeIcon, EyeOffIcon, ShareIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Contract } from 'web/lib/firebase/contracts'
|
import { Contract, contracts } from 'web/lib/firebase/contracts'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { CreateChallengeModal } from '../challenges/create-challenge-modal'
|
import { CreateChallengeModal } from '../challenges/create-challenge-modal'
|
||||||
|
@ -10,6 +10,9 @@ import { User } from 'common/user'
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
import { ShareModal } from './share-modal'
|
import { ShareModal } from './share-modal'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
|
import { collection, deleteDoc, doc } from 'firebase/firestore'
|
||||||
|
import { users } from 'web/lib/firebase/users'
|
||||||
|
import { useContractFollows } from 'web/hooks/use-follows'
|
||||||
|
|
||||||
export function ShareRow(props: {
|
export function ShareRow(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -23,6 +26,7 @@ export function ShareRow(props: {
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [isShareOpen, setShareOpen] = useState(false)
|
const [isShareOpen, setShareOpen] = useState(false)
|
||||||
|
const followers = useContractFollows(contract.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="mt-2">
|
<Row className="mt-2">
|
||||||
|
@ -62,6 +66,38 @@ export function ShareRow(props: {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{user && (
|
||||||
|
<Button
|
||||||
|
size={'lg'}
|
||||||
|
color={'gray-white'}
|
||||||
|
onClick={async () => {
|
||||||
|
// remove user doc from contract follows collection
|
||||||
|
const followDoc = doc(
|
||||||
|
collection(contracts, contract.id, 'follows'),
|
||||||
|
user.id
|
||||||
|
)
|
||||||
|
await deleteDoc(followDoc)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{followers?.includes(user.id) ? (
|
||||||
|
<Row>
|
||||||
|
<EyeOffIcon
|
||||||
|
className={clsx('mr-2 h-[24px] w-5')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Unfollow
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
<EyeIcon
|
||||||
|
className={clsx('mr-2 h-[24px] w-5')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Follow
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { listenForFollowers, listenForFollows } from 'web/lib/firebase/users'
|
import { listenForFollowers, listenForFollows } from 'web/lib/firebase/users'
|
||||||
|
import { contracts, listenForContractFollows } from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
export const useFollows = (userId: string | null | undefined) => {
|
export const useFollows = (userId: string | null | undefined) => {
|
||||||
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||||
|
@ -29,3 +30,13 @@ export const useFollowers = (userId: string | undefined) => {
|
||||||
|
|
||||||
return followerIds
|
return followerIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useContractFollows = (contractId: string) => {
|
||||||
|
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return listenForContractFollows(contractId, setFollowIds)
|
||||||
|
}, [contractId])
|
||||||
|
|
||||||
|
return followIds
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { getBinaryProb } from 'common/contract-details'
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
import { users } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export const contracts = coll<Contract>('contracts')
|
export const contracts = coll<Contract>('contracts')
|
||||||
|
|
||||||
|
@ -212,6 +213,16 @@ export function listenForContract(
|
||||||
return listenForValue<Contract>(contractRef, setContract)
|
return listenForValue<Contract>(contractRef, setContract)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForContractFollows(
|
||||||
|
contractId: string,
|
||||||
|
setFollowIds: (followIds: string[]) => void
|
||||||
|
) {
|
||||||
|
const follows = collection(contracts, contractId, 'follows')
|
||||||
|
return listenForValues<{ id: string }>(follows, (docs) =>
|
||||||
|
setFollowIds(docs.map(({ id }) => id))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function chooseRandomSubset(contracts: Contract[], count: number) {
|
function chooseRandomSubset(contracts: Contract[], count: number) {
|
||||||
const fiveMinutes = 5 * 60 * 1000
|
const fiveMinutes = 5 * 60 * 1000
|
||||||
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user