2021-12-15 00:45:13 +00:00
|
|
|
import {
|
|
|
|
doc,
|
|
|
|
setDoc,
|
|
|
|
getDoc,
|
2021-12-16 01:34:36 +00:00
|
|
|
collection,
|
|
|
|
query,
|
|
|
|
where,
|
|
|
|
limit,
|
|
|
|
getDocs,
|
2022-01-18 03:20:25 +00:00
|
|
|
orderBy,
|
2022-02-18 01:16:58 +00:00
|
|
|
updateDoc,
|
2022-06-03 04:52:14 +00:00
|
|
|
deleteDoc,
|
2022-06-08 03:24:18 +00:00
|
|
|
collectionGroup,
|
|
|
|
onSnapshot,
|
2021-12-15 00:45:13 +00:00
|
|
|
} from 'firebase/firestore'
|
2021-12-08 16:42:50 +00:00
|
|
|
import { getAuth } from 'firebase/auth'
|
2022-07-21 07:38:26 +00:00
|
|
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
2022-06-29 19:21:40 +00:00
|
|
|
import { app, db } from './init'
|
2022-06-24 17:14:20 +00:00
|
|
|
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
2022-08-18 22:46:11 +00:00
|
|
|
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
2022-05-26 05:30:03 +00:00
|
|
|
import { safeLocalStorage } from '../util/local'
|
2022-06-08 03:24:18 +00:00
|
|
|
import { filterDefined } from 'common/util/array'
|
2022-07-18 16:40:44 +00:00
|
|
|
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
2022-07-01 13:47:19 +00:00
|
|
|
import { removeUndefinedProps } from 'common/util/object'
|
|
|
|
import dayjs from 'dayjs'
|
2022-07-09 21:38:23 +00:00
|
|
|
import utc from 'dayjs/plugin/utc'
|
|
|
|
dayjs.extend(utc)
|
|
|
|
|
2022-07-01 13:47:19 +00:00
|
|
|
import { track } from '@amplitude/analytics-browser'
|
2022-02-18 01:16:58 +00:00
|
|
|
|
2022-06-29 19:21:40 +00:00
|
|
|
export const users = coll<User>('users')
|
|
|
|
export const privateUsers = coll<PrivateUser>('private-users')
|
|
|
|
|
2022-01-10 21:07:57 +00:00
|
|
|
export type { User }
|
2021-12-14 17:36:23 +00:00
|
|
|
|
2022-08-12 20:41:00 +00:00
|
|
|
export type UserAndPrivateUser = { user: User; privateUser: PrivateUser }
|
|
|
|
|
2022-06-24 17:14:20 +00:00
|
|
|
export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime'
|
2022-06-22 20:29:40 +00:00
|
|
|
|
2021-12-08 16:42:50 +00:00
|
|
|
export const auth = getAuth(app)
|
|
|
|
|
|
|
|
export async function getUser(userId: string) {
|
2022-06-29 19:21:40 +00:00
|
|
|
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
|
|
|
|
return (await getDoc(doc(users, userId))).data()!
|
2021-12-08 16:42:50 +00:00
|
|
|
}
|
|
|
|
|
2022-08-09 05:43:04 +00:00
|
|
|
export async function getPrivateUser(userId: string) {
|
|
|
|
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
|
2022-08-10 06:55:41 +00:00
|
|
|
return (await getDoc(doc(privateUsers, userId))).data()!
|
2022-08-09 05:43:04 +00:00
|
|
|
}
|
|
|
|
|
2022-08-12 20:41:00 +00:00
|
|
|
export async function getUserAndPrivateUser(userId: string) {
|
|
|
|
const [user, privateUser] = (
|
|
|
|
await Promise.all([
|
|
|
|
getDoc(doc(users, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
|
|
|
getDoc(doc(privateUsers, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
|
|
|
])
|
|
|
|
).map((d) => d.data()) as [User, PrivateUser]
|
|
|
|
return { user, privateUser } as UserAndPrivateUser
|
|
|
|
}
|
|
|
|
|
2021-12-16 01:34:36 +00:00
|
|
|
export async function getUserByUsername(username: string) {
|
|
|
|
// Find a user whose username matches the given username, or null if no such user exists.
|
2022-06-29 19:21:40 +00:00
|
|
|
const q = query(users, where('username', '==', username), limit(1))
|
|
|
|
const docs = (await getDocs(q)).docs
|
|
|
|
return docs.length > 0 ? docs[0].data() : null
|
2021-12-16 01:34:36 +00:00
|
|
|
}
|
|
|
|
|
2021-12-08 16:42:50 +00:00
|
|
|
export async function setUser(userId: string, user: User) {
|
2022-06-29 19:21:40 +00:00
|
|
|
await setDoc(doc(users, userId), user)
|
2021-12-08 16:42:50 +00:00
|
|
|
}
|
|
|
|
|
2022-02-18 01:16:58 +00:00
|
|
|
export async function updateUser(userId: string, update: Partial<User>) {
|
2022-06-29 19:21:40 +00:00
|
|
|
await updateDoc(doc(users, userId), { ...update })
|
2022-02-18 01:16:58 +00:00
|
|
|
}
|
|
|
|
|
2022-05-16 03:41:07 +00:00
|
|
|
export async function updatePrivateUser(
|
|
|
|
userId: string,
|
|
|
|
update: Partial<PrivateUser>
|
|
|
|
) {
|
2022-06-29 19:21:40 +00:00
|
|
|
await updateDoc(doc(privateUsers, userId), { ...update })
|
2022-05-16 03:41:07 +00:00
|
|
|
}
|
|
|
|
|
2022-02-04 00:13:04 +00:00
|
|
|
export function listenForUser(
|
|
|
|
userId: string,
|
|
|
|
setUser: (user: User | null) => void
|
|
|
|
) {
|
2022-06-29 19:21:40 +00:00
|
|
|
const userRef = doc(users, userId)
|
2022-02-04 00:13:04 +00:00
|
|
|
return listenForValue<User>(userRef, setUser)
|
2021-12-15 00:45:13 +00:00
|
|
|
}
|
|
|
|
|
2022-02-04 03:04:56 +00:00
|
|
|
export function listenForPrivateUser(
|
|
|
|
userId: string,
|
2022-02-04 03:11:59 +00:00
|
|
|
setPrivateUser: (privateUser: PrivateUser | null) => void
|
2022-02-04 03:04:56 +00:00
|
|
|
) {
|
2022-06-29 19:21:40 +00:00
|
|
|
const userRef = doc(privateUsers, userId)
|
2022-02-04 03:11:59 +00:00
|
|
|
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
2022-02-04 03:04:56 +00:00
|
|
|
}
|
|
|
|
|
2022-07-01 13:47:19 +00:00
|
|
|
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
|
|
|
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
2022-07-18 16:40:44 +00:00
|
|
|
const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY'
|
2022-01-18 07:18:38 +00:00
|
|
|
|
2022-07-01 13:47:19 +00:00
|
|
|
export function writeReferralInfo(
|
|
|
|
defaultReferrerUsername: string,
|
2022-08-02 23:18:08 +00:00
|
|
|
otherOptions?: {
|
|
|
|
contractId?: string
|
|
|
|
overwriteReferralUsername?: string
|
|
|
|
groupId?: string
|
|
|
|
}
|
2022-07-01 13:47:19 +00:00
|
|
|
) {
|
|
|
|
const local = safeLocalStorage()
|
|
|
|
const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)
|
2022-08-02 23:18:08 +00:00
|
|
|
const { contractId, overwriteReferralUsername, groupId } = otherOptions || {}
|
2022-07-01 13:47:19 +00:00
|
|
|
// Write the first referral username we see.
|
|
|
|
if (!cachedReferralUser)
|
|
|
|
local?.setItem(
|
|
|
|
CACHED_REFERRAL_USERNAME_KEY,
|
2022-08-02 23:18:08 +00:00
|
|
|
overwriteReferralUsername || defaultReferrerUsername
|
2022-07-01 13:47:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// If an explicit referral query is passed, overwrite the cached referral username.
|
2022-08-02 23:18:08 +00:00
|
|
|
if (overwriteReferralUsername)
|
|
|
|
local?.setItem(CACHED_REFERRAL_USERNAME_KEY, overwriteReferralUsername)
|
2022-07-01 13:47:19 +00:00
|
|
|
|
|
|
|
// Always write the most recent explicit group invite query value
|
2022-07-18 16:40:44 +00:00
|
|
|
if (groupId) local?.setItem(CACHED_REFERRAL_GROUP_ID_KEY, groupId)
|
2022-07-01 13:47:19 +00:00
|
|
|
|
|
|
|
// Write the first contract id that we see.
|
|
|
|
const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
|
|
|
if (!cachedReferralContract && contractId)
|
|
|
|
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
|
|
|
}
|
|
|
|
|
2022-07-21 07:38:26 +00:00
|
|
|
export async function setCachedReferralInfoForUser(user: User | null) {
|
2022-07-01 13:47:19 +00:00
|
|
|
if (!user || user.referredByUserId) return
|
|
|
|
// if the user wasn't created in the last minute, don't bother
|
|
|
|
const now = dayjs().utc()
|
|
|
|
const userCreatedTime = dayjs(user.createdTime)
|
2022-07-18 16:40:44 +00:00
|
|
|
if (now.diff(userCreatedTime, 'minute') > 5) return
|
2022-07-01 13:47:19 +00:00
|
|
|
|
|
|
|
const local = safeLocalStorage()
|
|
|
|
const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)
|
|
|
|
const cachedReferralContractId = local?.getItem(
|
|
|
|
CACHED_REFERRAL_CONTRACT_ID_KEY
|
|
|
|
)
|
2022-07-18 16:40:44 +00:00
|
|
|
const cachedReferralGroupId = local?.getItem(CACHED_REFERRAL_GROUP_ID_KEY)
|
2022-07-01 13:47:19 +00:00
|
|
|
|
|
|
|
// get user via username
|
|
|
|
if (cachedReferralUsername)
|
|
|
|
getUserByUsername(cachedReferralUsername).then((referredByUser) => {
|
|
|
|
if (!referredByUser) return
|
|
|
|
// update user's referralId
|
|
|
|
updateUser(
|
|
|
|
user.id,
|
|
|
|
removeUndefinedProps({
|
|
|
|
referredByUserId: referredByUser.id,
|
|
|
|
referredByContractId: cachedReferralContractId
|
|
|
|
? cachedReferralContractId
|
|
|
|
: undefined,
|
2022-07-18 16:40:44 +00:00
|
|
|
referredByGroupId: cachedReferralGroupId
|
|
|
|
? cachedReferralGroupId
|
|
|
|
: undefined,
|
2022-07-01 13:47:19 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('error setting referral details', err)
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
track('Referral', {
|
|
|
|
userId: user.id,
|
|
|
|
referredByUserId: referredByUser.id,
|
|
|
|
referredByContractId: cachedReferralContractId,
|
2022-07-18 16:40:44 +00:00
|
|
|
referredByGroupId: cachedReferralGroupId,
|
2022-07-01 13:47:19 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-07-18 16:40:44 +00:00
|
|
|
if (cachedReferralGroupId) addUserToGroupViaId(cachedReferralGroupId, user.id)
|
2022-07-01 13:47:19 +00:00
|
|
|
|
2022-07-18 16:40:44 +00:00
|
|
|
local?.removeItem(CACHED_REFERRAL_GROUP_ID_KEY)
|
2022-07-01 13:47:19 +00:00
|
|
|
local?.removeItem(CACHED_REFERRAL_USERNAME_KEY)
|
|
|
|
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:42:50 +00:00
|
|
|
export async function firebaseLogin() {
|
|
|
|
const provider = new GoogleAuthProvider()
|
2022-06-23 08:07:52 +00:00
|
|
|
return signInWithPopup(auth, provider)
|
2021-12-08 16:42:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function firebaseLogout() {
|
2022-07-22 07:03:16 +00:00
|
|
|
await auth.signOut()
|
2021-12-08 16:42:50 +00:00
|
|
|
}
|
|
|
|
|
2022-03-21 01:07:45 +00:00
|
|
|
export async function listUsers(userIds: string[]) {
|
|
|
|
if (userIds.length > 10) {
|
|
|
|
throw new Error('Too many users requested at once; Firestore limits to 10')
|
|
|
|
}
|
2022-06-29 19:21:40 +00:00
|
|
|
const q = query(users, where('id', 'in', userIds))
|
|
|
|
const docs = (await getDocs(q)).docs
|
|
|
|
return docs.map((doc) => doc.data())
|
2022-03-21 01:07:45 +00:00
|
|
|
}
|
|
|
|
|
2022-01-16 03:09:15 +00:00
|
|
|
export async function listAllUsers() {
|
2022-06-29 19:21:40 +00:00
|
|
|
const docs = (await getDocs(users)).docs
|
|
|
|
return docs.map((doc) => doc.data())
|
2022-01-16 03:09:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 17:14:20 +00:00
|
|
|
export function getTopTraders(period: Period) {
|
2022-06-22 20:29:40 +00:00
|
|
|
const topTraders = query(
|
2022-06-29 19:21:40 +00:00
|
|
|
users,
|
2022-06-22 20:29:40 +00:00
|
|
|
orderBy('profitCached.' + period, 'desc'),
|
|
|
|
limit(20)
|
|
|
|
)
|
2022-01-18 03:20:25 +00:00
|
|
|
|
2022-07-19 20:39:15 +00:00
|
|
|
return getValues<User>(topTraders)
|
2022-01-18 03:20:25 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 17:14:20 +00:00
|
|
|
export function getTopCreators(period: Period) {
|
2022-06-22 20:29:40 +00:00
|
|
|
const topCreators = query(
|
2022-06-29 19:21:40 +00:00
|
|
|
users,
|
2022-06-22 20:29:40 +00:00
|
|
|
orderBy('creatorVolumeCached.' + period, 'desc'),
|
|
|
|
limit(20)
|
|
|
|
)
|
2022-07-19 20:39:15 +00:00
|
|
|
return getValues<User>(topCreators)
|
2022-06-22 20:29:40 +00:00
|
|
|
}
|
2022-01-18 03:20:25 +00:00
|
|
|
|
2022-06-22 16:05:54 +00:00
|
|
|
export async function getTopFollowed() {
|
2022-06-29 19:21:40 +00:00
|
|
|
return (await getValues<User>(topFollowedQuery)).slice(0, 20)
|
2022-06-22 16:05:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const topFollowedQuery = query(
|
2022-06-29 19:21:40 +00:00
|
|
|
users,
|
2022-06-22 16:05:54 +00:00
|
|
|
orderBy('followerCountCached', 'desc'),
|
|
|
|
limit(20)
|
|
|
|
)
|
|
|
|
|
2022-04-19 06:47:58 +00:00
|
|
|
export function getUsers() {
|
2022-06-29 19:21:40 +00:00
|
|
|
return getValues<User>(users)
|
2022-04-19 06:47:58 +00:00
|
|
|
}
|
|
|
|
|
2022-06-03 04:52:14 +00:00
|
|
|
export async function follow(userId: string, followedUserId: string) {
|
2022-06-29 19:21:40 +00:00
|
|
|
const followDoc = doc(collection(users, userId, 'follows'), followedUserId)
|
2022-06-03 04:52:14 +00:00
|
|
|
await setDoc(followDoc, {
|
|
|
|
userId: followedUserId,
|
|
|
|
timestamp: Date.now(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function unfollow(userId: string, unfollowedUserId: string) {
|
2022-06-29 19:21:40 +00:00
|
|
|
const followDoc = doc(collection(users, userId, 'follows'), unfollowedUserId)
|
2022-06-03 04:52:14 +00:00
|
|
|
await deleteDoc(followDoc)
|
|
|
|
}
|
|
|
|
|
2022-08-27 08:09:01 +00:00
|
|
|
export async function getPortfolioHistory(userId: string, since: number) {
|
2022-06-24 17:14:20 +00:00
|
|
|
return getValues<PortfolioMetrics>(
|
|
|
|
query(
|
|
|
|
collectionGroup(db, 'portfolioHistory'),
|
|
|
|
where('userId', '==', userId),
|
2022-08-27 08:09:01 +00:00
|
|
|
where('timestamp', '>=', since),
|
2022-06-24 17:14:20 +00:00
|
|
|
orderBy('timestamp', 'asc')
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-03 04:52:14 +00:00
|
|
|
export function listenForFollows(
|
|
|
|
userId: string,
|
|
|
|
setFollowIds: (followIds: string[]) => void
|
|
|
|
) {
|
2022-06-29 19:21:40 +00:00
|
|
|
const follows = collection(users, userId, 'follows')
|
2022-06-03 04:52:14 +00:00
|
|
|
return listenForValues<{ userId: string }>(follows, (docs) =>
|
|
|
|
setFollowIds(docs.map(({ userId }) => userId))
|
|
|
|
)
|
|
|
|
}
|
2022-06-08 03:24:18 +00:00
|
|
|
|
|
|
|
export function listenForFollowers(
|
|
|
|
userId: string,
|
|
|
|
setFollowerIds: (followerIds: string[]) => void
|
|
|
|
) {
|
|
|
|
const followersQuery = query(
|
|
|
|
collectionGroup(db, 'follows'),
|
|
|
|
where('userId', '==', userId)
|
|
|
|
)
|
|
|
|
return onSnapshot(
|
|
|
|
followersQuery,
|
|
|
|
{ includeMetadataChanges: true },
|
|
|
|
(snapshot) => {
|
|
|
|
if (snapshot.metadata.fromCache) return
|
|
|
|
|
|
|
|
const values = snapshot.docs.map((doc) => doc.ref.parent.parent?.id)
|
|
|
|
setFollowerIds(filterDefined(values))
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2022-07-01 13:47:19 +00:00
|
|
|
export function listenForReferrals(
|
|
|
|
userId: string,
|
|
|
|
setReferralIds: (referralIds: string[]) => void
|
|
|
|
) {
|
|
|
|
const referralsQuery = query(
|
|
|
|
collection(db, 'users'),
|
|
|
|
where('referredByUserId', '==', userId)
|
|
|
|
)
|
|
|
|
return onSnapshot(
|
|
|
|
referralsQuery,
|
|
|
|
{ includeMetadataChanges: true },
|
|
|
|
(snapshot) => {
|
|
|
|
if (snapshot.metadata.fromCache) return
|
|
|
|
|
|
|
|
const values = snapshot.docs.map((doc) => doc.ref.id)
|
|
|
|
setReferralIds(filterDefined(values))
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|