refactor: strenghten firebase data types (#390)

This commit is contained in:
Vojtech Simetka 2023-04-04 22:29:42 +02:00 committed by GitHub
parent 4acdff71fe
commit 005e3d918c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 216 additions and 119 deletions

View File

@ -0,0 +1,99 @@
import type { Chat, DraftChat } from '$lib/stores/chat'
import type { DraftPersona, Persona } from '$lib/stores/persona'
import type { DraftPost, Post, PostPending } from '$lib/stores/post'
export function personaFromDB(persona: DBPersona, personaId: string): Persona {
return {
participantsCount: persona.participants.length,
picture: persona.picture,
minReputation: persona.minReputation,
cover: persona.cover,
name: persona.name,
pitch: persona.pitch,
description: persona.description,
postsCount: persona.postsCount,
timestamp: persona.timestamp,
personaId,
}
}
export function personaToDB(persona: DraftPersona, participants: string[]): DBPersona {
return {
cover: persona.cover,
description: persona.description,
minReputation: persona.minReputation,
name: persona.name,
postsCount: persona.posts.length,
picture: persona.picture,
pitch: persona.pitch,
timestamp: persona.timestamp,
participants,
}
}
export function postDraftToDB(post: DraftPost, address: string): DBPostPending {
return {
...post,
address,
demote: [],
promote: [],
}
}
export function postPendingFromDB(
post: DBPostPending,
postId: string,
address?: string,
): PostPending {
let yourVote: '+' | '-' | undefined = undefined
if (address && post.promote.includes(address)) yourVote = '+'
if (address && post.demote.includes(address)) yourVote = '-'
return {
postId,
yourVote,
text: post.text,
images: post.images,
timestamp: post.timestamp,
myPost: address === post.address,
}
}
export function postFromDB(post: DBPost, postId: string, address?: string): Post {
return {
postId,
text: post.text,
timestamp: post.timestamp,
images: post.images,
myPost: address === post.address,
}
}
export function chatToDB(chat: DraftChat, address: string, postAddress: string): DBChat {
return {
users: [address, postAddress],
personaId: chat.persona.personaId,
post: {
postId: chat.post.postId,
address: postAddress,
images: chat.post.images,
text: chat.post.text,
timestamp: chat.post.timestamp,
},
messages: [],
}
}
export function chatFromDB(chat: DBChat, persona: Persona, chatId: string): Chat {
return {
chatId,
users: chat.users,
persona: persona,
post: {
postId: chat.post.postId,
images: chat.post.images,
text: chat.post.text,
timestamp: chat.post.timestamp,
},
messages: chat.messages,
}
}

View File

@ -12,17 +12,9 @@ import {
VOTE_GO_PRICE, VOTE_GO_PRICE,
} from '$lib/constants' } from '$lib/constants'
import { tokens } from '$lib/stores/tokens' import { tokens } from '$lib/stores/tokens'
import { posts, type Post } from '$lib/stores/post' import { posts, type Post, type PostPending } from '$lib/stores/post'
import { transaction, type TransactionRecord } from '$lib/stores/transaction' import { transaction, type TransactionRecord } from '$lib/stores/transaction'
import type { Adapter } from '..' import type { Adapter } from '..'
// FIXME: no idea where whe should put these so that they don't leak. I can limit to some specific origin I guess
const IPFS_AUTH =
'Basic Mk5Nbk1vZUNSTWMyOTlCQjYzWm9QZzlQYTU3OjAwZTk2MmJjZTBkZmQxZWQxNGNhNmY1M2JiYjYxMTli'
const IPFS_GATEWAY = 'https://kurate.infura-ipfs.io/ipfs'
// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app' import { initializeApp } from 'firebase/app'
import { import {
getFirestore, getFirestore,
@ -38,8 +30,19 @@ import {
} from 'firebase/firestore' } from 'firebase/firestore'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { subscribeAccountChanged, subscribeChainChanged } from '../utils' import { subscribeAccountChanged, subscribeChainChanged } from '../utils'
// TODO: Add SDKs for Firebase products that you want to use import {
// https://firebase.google.com/docs/web/setup#available-libraries chatFromDB,
chatToDB,
personaFromDB,
personaToDB,
postFromDB,
postPendingFromDB,
} from './db-adapter'
// FIXME: no idea where whe should put these so that they don't leak. I can limit to some specific origin I guess
const IPFS_AUTH =
'Basic Mk5Nbk1vZUNSTWMyOTlCQjYzWm9QZzlQYTU3OjAwZTk2MmJjZTBkZmQxZWQxNGNhNmY1M2JiYjYxMTli'
const IPFS_GATEWAY = 'https://kurate.infura-ipfs.io/ipfs'
// Your web app's Firebase configuration // Your web app's Firebase configuration
const firebaseConfig = { const firebaseConfig = {
@ -82,6 +85,8 @@ export class Firebase implements Adapter {
private subscriptions: Array<() => unknown> = [] private subscriptions: Array<() => unknown> = []
private userSubscriptions: Array<() => unknown> = [] private userSubscriptions: Array<() => unknown> = []
private votes = new Map<string, { promote: string[]; demote: string[] }>() private votes = new Map<string, { promote: string[]; demote: string[] }>()
private participants = new Map<string, string[]>()
private postIdParticipant = new Map<string, string>()
async start() { async start() {
const personasQuery = query(collection(db, 'personas')) const personasQuery = query(collection(db, 'personas'))
@ -89,10 +94,10 @@ export class Firebase implements Adapter {
personas.update((state) => { personas.update((state) => {
const all = new Map<string, Persona>() const all = new Map<string, Persona>()
data.docs.forEach((e) => { data.docs.forEach((e) => {
const persona = e.data() const dbPersona = e.data() as DBPersona
persona.participantsCount = persona.participants?.length const persona = personaFromDB(dbPersona, e.id)
persona.personaId = e.id this.participants.set(e.id, dbPersona.participants)
all.set(e.id, persona as Persona) all.set(e.id, persona)
}) })
return { ...state, all, loading: false } return { ...state, all, loading: false }
@ -118,7 +123,8 @@ export class Firebase implements Adapter {
const subscribeTransactions = onSnapshot(transactionSnapshot, (res) => { const subscribeTransactions = onSnapshot(transactionSnapshot, (res) => {
const trns: TransactionRecord[] = [] const trns: TransactionRecord[] = []
res.docs.forEach((d) => { res.docs.forEach((d) => {
trns.push(d.data() as TransactionRecord) const transactionsDb = d.data() as DBTransaction
trns.push(transactionsDb)
}) })
transaction.set({ transactions: trns }) transaction.set({ transactions: trns })
}) })
@ -132,16 +138,10 @@ export class Firebase implements Adapter {
const newChats = new Map<string, Chat>() const newChats = new Map<string, Chat>()
const personasTemp = get(personas) const personasTemp = get(personas)
res.docs.forEach((d) => { res.docs.forEach((d) => {
const data = d.data() const data = d.data() as DBChat
const persona = personasTemp.all.get(data.personaId) const persona = personasTemp.all.get(data.personaId)
if (!persona) return if (!persona) return
const chat: Chat = { const chat = chatFromDB(data, persona, d.id)
persona,
post: data.post,
users: data.users,
chatId: d.id,
messages: data.messages,
}
newChats.set(d.id, chat) newChats.set(d.id, chat)
}) })
chats.update((state) => ({ ...state, chats: newChats, loading: false })) chats.update((state) => ({ ...state, chats: newChats, loading: false }))
@ -233,29 +233,27 @@ export class Firebase implements Adapter {
await signer.signMessage('This "transaction" publishes persona') await signer.signMessage('This "transaction" publishes persona')
const address = await signer.getAddress() const address = await signer.getAddress()
const personasCollection = collection(db, 'personas') const personasCollection = collection(db, 'personas')
const { posts, ...persona } = draftPersona const { posts } = draftPersona
const personaDoc = await addDoc(personasCollection, { const personaDoc = await addDoc(personasCollection, personaToDB(draftPersona, [address]))
...persona,
participants: [address],
postsCount: 5,
timestamp: Date.now(),
})
const postCollection = collection(db, `personas/${personaDoc.id}/posts`) const postCollection = collection(db, `personas/${personaDoc.id}/posts`)
posts.forEach((p) => posts.forEach((p) => {
addDoc(postCollection, { const dbPost: DBPost = {
...p, ...p,
address, address,
}), }
) addDoc(postCollection, dbPost)
})
const profileCollection = collection(db, `users/${address}/transactions`) const profileCollection = collection(db, `users/${address}/transactions`)
await addDoc(profileCollection, { const transaction: DBTransaction = {
timestamp: Date.now(), timestamp: Date.now(),
goChange: -CREATE_PERSONA_GO_PRICE, goChange: -CREATE_PERSONA_GO_PRICE,
repChange: 0, repChange: 0,
personaId: personaDoc.id, personaId: personaDoc.id,
type: 'publish persona', type: 'publish persona',
}) }
await addDoc(profileCollection, transaction)
const { go, repTotal, repStaked } = get(tokens) const { go, repTotal, repStaked } = get(tokens)
const user = doc(db, `users/${address}`) const user = doc(db, `users/${address}`)
@ -306,9 +304,9 @@ export class Firebase implements Adapter {
signer: Signer, signer: Signer,
): Promise<string> { ): Promise<string> {
const address = await signer.getAddress() const address = await signer.getAddress()
const isMemberOfGroup = get(personas).all.get(groupId)?.participants?.includes(address) const isMemberOfGroup = this.participants.get(groupId)?.includes(address)
const post = { const post: DBPostPending = {
timestamp: Date.now(), timestamp: Date.now(),
text, text,
images, images,
@ -330,13 +328,15 @@ export class Firebase implements Adapter {
const postDoc = await addDoc(pendingPosts, post) const postDoc = await addDoc(pendingPosts, post)
const profileCollection = collection(db, `users/${address}/transactions`) const profileCollection = collection(db, `users/${address}/transactions`)
await addDoc(profileCollection, {
const transaction: DBTransaction = {
timestamp: Date.now(), timestamp: Date.now(),
goChange: -NEW_POST_GO_PRICE, goChange: -NEW_POST_GO_PRICE,
repChange: -NEW_POST_REP_PRICE, repChange: -NEW_POST_REP_PRICE,
personaId: groupId, personaId: groupId,
type: 'publish post', type: 'publish post',
}) }
await addDoc(profileCollection, transaction)
const { go, repTotal, repStaked } = get(tokens) const { go, repTotal, repStaked } = get(tokens)
const user = doc(db, `users/${address}`) const user = doc(db, `users/${address}`)
@ -354,32 +354,14 @@ export class Firebase implements Adapter {
const postsCollection = collection(db, `personas/${groupId}/posts`) const postsCollection = collection(db, `personas/${groupId}/posts`)
const subscribePending = onSnapshot(pendingCollection, (res) => { const subscribePending = onSnapshot(pendingCollection, (res) => {
const newPending: Post[] = [] const newPending: PostPending[] = []
res.docs.forEach((d) => { res.docs.forEach((d) => {
interface PendingPost { const postDb = d.data() as DBPostPending
demote: string[] const { address } = get(profile)
images: string[] this.votes.set(d.id, { promote: postDb.promote, demote: postDb.demote })
promote: string[] this.postIdParticipant.set(d.id, postDb.address)
text: string newPending.push(postPendingFromDB(postDb, d.id, address))
timestamp: number
address: string
}
const { text, images, timestamp, demote, promote, address } = d.data() as PendingPost
const loggedUser = get(profile)
let yourVote: '+' | '-' | undefined = undefined
this.votes.set(d.id, { promote, demote })
if (loggedUser.address && promote.includes(loggedUser.address)) yourVote = '+'
if (loggedUser.address && demote.includes(loggedUser.address)) yourVote = '-'
newPending.push({
text,
images,
timestamp,
yourVote,
postId: d.id,
address,
myPost: loggedUser.address === address,
})
}) })
posts.update(({ data }) => { posts.update(({ data }) => {
@ -407,7 +389,9 @@ export class Firebase implements Adapter {
if (vt.promote.includes(address)) yourVote = '+' if (vt.promote.includes(address)) yourVote = '+'
if (vt.demote.includes(address)) yourVote = '-' if (vt.demote.includes(address)) yourVote = '-'
return { ...p, myPost: p.address === address, yourVote } const postSender = this.postIdParticipant.get(p.postId)
return { ...p, myPost: postSender === address, yourVote }
}) })
data.set(groupId, { ...personaPostData, pending }) data.set(groupId, { ...personaPostData, pending })
@ -420,22 +404,10 @@ export class Firebase implements Adapter {
const newPostst: Post[] = [] const newPostst: Post[] = []
res.docs.forEach((d) => { res.docs.forEach((d) => {
interface DbPost { const postDb = d.data() as DBPost
images: string[] const { address } = get(profile)
text: string this.postIdParticipant.set(d.id, postDb.address)
timestamp: number newPostst.push(postFromDB(postDb, d.id, address))
address: string
}
const { text, images, timestamp, address } = d.data() as DbPost
const loggedUser = get(profile)
newPostst.push({
text,
images,
timestamp,
postId: d.id,
address,
myPost: address === loggedUser.address,
})
}) })
posts.update(({ data }) => { posts.update(({ data }) => {
@ -477,34 +449,29 @@ export class Firebase implements Adapter {
setDoc(user, { address, go: go - VOTE_GO_PRICE }, { merge: true }) setDoc(user, { address, go: go - VOTE_GO_PRICE }, { merge: true })
const profileCollection = collection(db, `users/${address}/transactions`) const profileCollection = collection(db, `users/${address}/transactions`)
await addDoc(profileCollection, {
const transaction: DBTransaction = {
timestamp: Date.now(), timestamp: Date.now(),
goChange: -VOTE_GO_PRICE, goChange: -VOTE_GO_PRICE,
repChange: 0, repChange: 0,
type: promoteDemote, type: promoteDemote,
personaId: groupId, personaId: groupId,
}) }
await addDoc(profileCollection, transaction)
} }
async startChat(chat: DraftChat): Promise<string> { async startChat(chat: DraftChat): Promise<string> {
const address = get(profile).address const { address } = get(profile)
const postSender = this.postIdParticipant.get(chat.post.postId)
if (!address) throw new Error('You need to be logged in to start a chat') if (!address) throw new Error('You need to be logged in to start a chat')
if (!chat.post.address) throw new Error('Info about original poster is missing') if (!postSender) throw new Error('Info about original poster is missing')
if (!chat.post.postId) throw new Error('PostId is missing') if (!chat.post.postId) throw new Error('PostId is missing')
if (!chat.persona.personaId) throw new Error('PersonaId is missing') if (!chat.persona.personaId) throw new Error('PersonaId is missing')
const dbChat = { const dbChat = chatToDB(chat, address, postSender)
users: [address, chat.post.address],
post: {
postId: chat.post.postId,
address: chat.post.address,
images: chat.post.images,
timestamp: chat.post.timestamp,
text: chat.post.text,
} as Post,
personaId: chat.persona.personaId,
}
const chatCollection = collection(db, `/chats`) const chatCollection = collection(db, `/chats`)
const chatDoc = await addDoc(chatCollection, dbChat) const chatDoc = await addDoc(chatCollection, dbChat)

View File

@ -0,0 +1,39 @@
interface DBPersona {
cover: string
description: string
minReputation: ReputationOptions
name: string
participants: string[]
postsCount: number
picture: string
pitch: string
postsCount: string
timestamp: number
}
interface DBPost {
images: string[]
text: string
timestamp: number
address: string
}
interface DBPostPending extends DBPost {
demote: string[]
promote: string[]
}
interface DBChatMessage {
address: string
text: string
timestamp: number
}
interface DBChat {
messages: DBChatMessage[]
personaId: string
post: DBPost & { postId: string }
users: string[]
}
type DBTransaction = TransactionRecord

View File

@ -50,7 +50,7 @@
let avatar = createAvatar(botttsNeutral, { let avatar = createAvatar(botttsNeutral, {
size: 94, // This is 47pt at 2x resolution size: 94, // This is 47pt at 2x resolution
seed: chat.chatId, seed: (chat as Chat).chatId,
}).toDataUriSync() }).toDataUriSync()
$: if (scrollElement) observer.observe(scrollElement) $: if (scrollElement) observer.observe(scrollElement)

View File

@ -14,7 +14,6 @@ export interface DraftChat {
post: Post post: Post
messages: Message[] messages: Message[]
closed?: boolean closed?: boolean
chatId?: string
} }
export interface Chat extends DraftChat { export interface Chat extends DraftChat {

View File

@ -15,7 +15,6 @@ export interface Persona {
postsCount: number postsCount: number
minReputation: ReputationOptions minReputation: ReputationOptions
timestamp: number timestamp: number
participants?: string[] // FIXME: this is only needed for firebase, might want to remove
} }
export interface DraftPersona export interface DraftPersona

View File

@ -7,18 +7,20 @@ export interface DraftPost {
} }
export interface Post extends DraftPost { export interface Post extends DraftPost {
yourVote?: '+' | '-'
myPost?: boolean
postId: string postId: string
address?: string // FIXME: only needed for firebase, might want to remove myPost?: boolean
}
export interface PostPending extends Post {
yourVote?: '+' | '-'
} }
interface PostData { interface PostData {
data: Map<string, { approved: Post[]; pending: Post[]; loading: boolean }> data: Map<string, { approved: Post[]; pending: PostPending[]; loading: boolean }>
} }
export interface PostStore extends Writable<PostData> { export interface PostStore extends Writable<PostData> {
addPending: (post: Post, groupId: string) => void addPending: (post: PostPending, groupId: string) => void
addApproved: (post: Post, groupId: string) => void addApproved: (post: Post, groupId: string) => void
} }
@ -27,7 +29,7 @@ function createPostStore(): PostStore {
return { return {
...store, ...store,
addPending: (post: Post, groupId: string) => { addPending: (post: PostPending, groupId: string) => {
store.update(({ data }) => { store.update(({ data }) => {
const personaPostData = data.get(groupId) const personaPostData = data.get(groupId)

View File

@ -1,4 +1,3 @@
import { DEFAULT_GO_AMOUNT } from '$lib/constants'
import { writable, type Writable } from 'svelte/store' import { writable, type Writable } from 'svelte/store'
export interface TokenData { export interface TokenData {
@ -15,8 +14,8 @@ export type TokenStore = Writable<TokenData>
function createTokenStore(): TokenStore { function createTokenStore(): TokenStore {
const epochDuration = 8 * 60 * 60 * 1000 const epochDuration = 8 * 60 * 60 * 1000
const store = writable<TokenData>({ const store = writable<TokenData>({
go: DEFAULT_GO_AMOUNT, go: 0,
repTotal: 55, repTotal: 0,
repStaked: 0, repStaked: 0,
loading: false, loading: false,
epochDuration, epochDuration,

View File

@ -1,13 +1,6 @@
import { writable, type Writable } from 'svelte/store' import { writable, type Writable } from 'svelte/store'
type TransactionType = export type TransactionType = 'publish persona' | 'promote' | 'demote' | 'publish post'
| 'publish persona'
| 'promote'
| 'demote'
| 'publish post'
| 'vote_win'
| 'post_included'
| 'post_rejected'
export interface TransactionRecord { export interface TransactionRecord {
timestamp: number timestamp: number

View File

@ -206,7 +206,7 @@
on:click={adapter.signIn} on:click={adapter.signIn}
disabled={!canConnectWallet()} disabled={!canConnectWallet()}
/> />
{:else if $profile.signer !== undefined && $profile.address !== post.address} {:else if $profile.signer !== undefined && !post.myPost}
<Button variant="primary" label="Chat with poster" icon={ChatBot} on:click={startChat} /> <Button variant="primary" label="Chat with poster" icon={ChatBot} on:click={startChat} />
{/if} {/if}
</Post> </Post>

View File

@ -85,7 +85,7 @@
on:click={adapter.signIn} on:click={adapter.signIn}
disabled={!canConnectWallet()} disabled={!canConnectWallet()}
/> />
{:else if $profile.signer !== undefined && $profile.address !== post.address} {:else if $profile.signer !== undefined && !post.myPost}
<Button variant="primary" label="Chat with poster" icon={ChatBot} on:click={startChat} /> <Button variant="primary" label="Chat with poster" icon={ChatBot} on:click={startChat} />
{/if} {/if}
</div> </div>