From 005e3d918ce74b63f016bc03f7f4ed67f652165d Mon Sep 17 00:00:00 2001 From: Vojtech Simetka Date: Tue, 4 Apr 2023 22:29:42 +0200 Subject: [PATCH] refactor: strenghten firebase data types (#390) --- .../src/lib/adapters/firebase/db-adapter.ts | 99 +++++++++++ .../ui/src/lib/adapters/firebase/index.ts | 161 +++++++----------- .../ui/src/lib/adapters/firebase/types.d.ts | 39 +++++ .../ui/src/lib/components/chat-screen.svelte | 2 +- packages/ui/src/lib/stores/chat.ts | 1 - packages/ui/src/lib/stores/persona.ts | 1 - packages/ui/src/lib/stores/post.ts | 14 +- packages/ui/src/lib/stores/tokens.ts | 5 +- packages/ui/src/lib/stores/transaction.ts | 9 +- .../[id]/pending/[postId]/+page.svelte | 2 +- .../persona/[id]/post/[postId]/+page.svelte | 2 +- 11 files changed, 216 insertions(+), 119 deletions(-) create mode 100644 packages/ui/src/lib/adapters/firebase/db-adapter.ts create mode 100644 packages/ui/src/lib/adapters/firebase/types.d.ts diff --git a/packages/ui/src/lib/adapters/firebase/db-adapter.ts b/packages/ui/src/lib/adapters/firebase/db-adapter.ts new file mode 100644 index 0000000..84fc5e1 --- /dev/null +++ b/packages/ui/src/lib/adapters/firebase/db-adapter.ts @@ -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, + } +} diff --git a/packages/ui/src/lib/adapters/firebase/index.ts b/packages/ui/src/lib/adapters/firebase/index.ts index f84f66c..475f914 100644 --- a/packages/ui/src/lib/adapters/firebase/index.ts +++ b/packages/ui/src/lib/adapters/firebase/index.ts @@ -12,17 +12,9 @@ import { VOTE_GO_PRICE, } from '$lib/constants' 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 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 { getFirestore, @@ -38,8 +30,19 @@ import { } from 'firebase/firestore' import { get } from 'svelte/store' import { subscribeAccountChanged, subscribeChainChanged } from '../utils' -// TODO: Add SDKs for Firebase products that you want to use -// https://firebase.google.com/docs/web/setup#available-libraries +import { + 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 const firebaseConfig = { @@ -82,6 +85,8 @@ export class Firebase implements Adapter { private subscriptions: Array<() => unknown> = [] private userSubscriptions: Array<() => unknown> = [] private votes = new Map() + private participants = new Map() + private postIdParticipant = new Map() async start() { const personasQuery = query(collection(db, 'personas')) @@ -89,10 +94,10 @@ export class Firebase implements Adapter { personas.update((state) => { const all = new Map() data.docs.forEach((e) => { - const persona = e.data() - persona.participantsCount = persona.participants?.length - persona.personaId = e.id - all.set(e.id, persona as Persona) + const dbPersona = e.data() as DBPersona + const persona = personaFromDB(dbPersona, e.id) + this.participants.set(e.id, dbPersona.participants) + all.set(e.id, persona) }) return { ...state, all, loading: false } @@ -118,7 +123,8 @@ export class Firebase implements Adapter { const subscribeTransactions = onSnapshot(transactionSnapshot, (res) => { const trns: TransactionRecord[] = [] res.docs.forEach((d) => { - trns.push(d.data() as TransactionRecord) + const transactionsDb = d.data() as DBTransaction + trns.push(transactionsDb) }) transaction.set({ transactions: trns }) }) @@ -132,16 +138,10 @@ export class Firebase implements Adapter { const newChats = new Map() const personasTemp = get(personas) res.docs.forEach((d) => { - const data = d.data() + const data = d.data() as DBChat const persona = personasTemp.all.get(data.personaId) if (!persona) return - const chat: Chat = { - persona, - post: data.post, - users: data.users, - chatId: d.id, - messages: data.messages, - } + const chat = chatFromDB(data, persona, d.id) newChats.set(d.id, chat) }) chats.update((state) => ({ ...state, chats: newChats, loading: false })) @@ -233,29 +233,27 @@ export class Firebase implements Adapter { await signer.signMessage('This "transaction" publishes persona') const address = await signer.getAddress() const personasCollection = collection(db, 'personas') - const { posts, ...persona } = draftPersona - const personaDoc = await addDoc(personasCollection, { - ...persona, - participants: [address], - postsCount: 5, - timestamp: Date.now(), - }) + const { posts } = draftPersona + const personaDoc = await addDoc(personasCollection, personaToDB(draftPersona, [address])) + const postCollection = collection(db, `personas/${personaDoc.id}/posts`) - posts.forEach((p) => - addDoc(postCollection, { + posts.forEach((p) => { + const dbPost: DBPost = { ...p, address, - }), - ) + } + addDoc(postCollection, dbPost) + }) const profileCollection = collection(db, `users/${address}/transactions`) - await addDoc(profileCollection, { + const transaction: DBTransaction = { timestamp: Date.now(), goChange: -CREATE_PERSONA_GO_PRICE, repChange: 0, personaId: personaDoc.id, type: 'publish persona', - }) + } + await addDoc(profileCollection, transaction) const { go, repTotal, repStaked } = get(tokens) const user = doc(db, `users/${address}`) @@ -306,9 +304,9 @@ export class Firebase implements Adapter { signer: Signer, ): Promise { 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(), text, images, @@ -330,13 +328,15 @@ export class Firebase implements Adapter { const postDoc = await addDoc(pendingPosts, post) const profileCollection = collection(db, `users/${address}/transactions`) - await addDoc(profileCollection, { + + const transaction: DBTransaction = { timestamp: Date.now(), goChange: -NEW_POST_GO_PRICE, repChange: -NEW_POST_REP_PRICE, personaId: groupId, type: 'publish post', - }) + } + await addDoc(profileCollection, transaction) const { go, repTotal, repStaked } = get(tokens) const user = doc(db, `users/${address}`) @@ -354,32 +354,14 @@ export class Firebase implements Adapter { const postsCollection = collection(db, `personas/${groupId}/posts`) const subscribePending = onSnapshot(pendingCollection, (res) => { - const newPending: Post[] = [] + const newPending: PostPending[] = [] res.docs.forEach((d) => { - interface PendingPost { - demote: string[] - images: string[] - promote: string[] - text: string - 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, - }) + const postDb = d.data() as DBPostPending + const { address } = get(profile) + this.votes.set(d.id, { promote: postDb.promote, demote: postDb.demote }) + this.postIdParticipant.set(d.id, postDb.address) + newPending.push(postPendingFromDB(postDb, d.id, address)) }) posts.update(({ data }) => { @@ -407,7 +389,9 @@ export class Firebase implements Adapter { if (vt.promote.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 }) @@ -420,22 +404,10 @@ export class Firebase implements Adapter { const newPostst: Post[] = [] res.docs.forEach((d) => { - interface DbPost { - images: string[] - text: string - timestamp: number - 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, - }) + const postDb = d.data() as DBPost + const { address } = get(profile) + this.postIdParticipant.set(d.id, postDb.address) + newPostst.push(postFromDB(postDb, d.id, address)) }) posts.update(({ data }) => { @@ -477,34 +449,29 @@ export class Firebase implements Adapter { setDoc(user, { address, go: go - VOTE_GO_PRICE }, { merge: true }) const profileCollection = collection(db, `users/${address}/transactions`) - await addDoc(profileCollection, { + + const transaction: DBTransaction = { timestamp: Date.now(), goChange: -VOTE_GO_PRICE, repChange: 0, type: promoteDemote, personaId: groupId, - }) + } + + await addDoc(profileCollection, transaction) } async startChat(chat: DraftChat): Promise { - 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 (!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.persona.personaId) throw new Error('PersonaId is missing') - const dbChat = { - 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 dbChat = chatToDB(chat, address, postSender) + const chatCollection = collection(db, `/chats`) const chatDoc = await addDoc(chatCollection, dbChat) diff --git a/packages/ui/src/lib/adapters/firebase/types.d.ts b/packages/ui/src/lib/adapters/firebase/types.d.ts new file mode 100644 index 0000000..7db37fb --- /dev/null +++ b/packages/ui/src/lib/adapters/firebase/types.d.ts @@ -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 diff --git a/packages/ui/src/lib/components/chat-screen.svelte b/packages/ui/src/lib/components/chat-screen.svelte index ddf41fc..5e2a031 100644 --- a/packages/ui/src/lib/components/chat-screen.svelte +++ b/packages/ui/src/lib/components/chat-screen.svelte @@ -50,7 +50,7 @@ let avatar = createAvatar(botttsNeutral, { size: 94, // This is 47pt at 2x resolution - seed: chat.chatId, + seed: (chat as Chat).chatId, }).toDataUriSync() $: if (scrollElement) observer.observe(scrollElement) diff --git a/packages/ui/src/lib/stores/chat.ts b/packages/ui/src/lib/stores/chat.ts index e6e38e2..dbaaf1c 100644 --- a/packages/ui/src/lib/stores/chat.ts +++ b/packages/ui/src/lib/stores/chat.ts @@ -14,7 +14,6 @@ export interface DraftChat { post: Post messages: Message[] closed?: boolean - chatId?: string } export interface Chat extends DraftChat { diff --git a/packages/ui/src/lib/stores/persona.ts b/packages/ui/src/lib/stores/persona.ts index b289f6b..13275b1 100644 --- a/packages/ui/src/lib/stores/persona.ts +++ b/packages/ui/src/lib/stores/persona.ts @@ -15,7 +15,6 @@ export interface Persona { postsCount: number minReputation: ReputationOptions timestamp: number - participants?: string[] // FIXME: this is only needed for firebase, might want to remove } export interface DraftPersona diff --git a/packages/ui/src/lib/stores/post.ts b/packages/ui/src/lib/stores/post.ts index ac3901f..a939b9c 100644 --- a/packages/ui/src/lib/stores/post.ts +++ b/packages/ui/src/lib/stores/post.ts @@ -7,18 +7,20 @@ export interface DraftPost { } export interface Post extends DraftPost { - yourVote?: '+' | '-' - myPost?: boolean postId: string - address?: string // FIXME: only needed for firebase, might want to remove + myPost?: boolean +} + +export interface PostPending extends Post { + yourVote?: '+' | '-' } interface PostData { - data: Map + data: Map } export interface PostStore extends Writable { - addPending: (post: Post, groupId: string) => void + addPending: (post: PostPending, groupId: string) => void addApproved: (post: Post, groupId: string) => void } @@ -27,7 +29,7 @@ function createPostStore(): PostStore { return { ...store, - addPending: (post: Post, groupId: string) => { + addPending: (post: PostPending, groupId: string) => { store.update(({ data }) => { const personaPostData = data.get(groupId) diff --git a/packages/ui/src/lib/stores/tokens.ts b/packages/ui/src/lib/stores/tokens.ts index 035488a..2c768d0 100644 --- a/packages/ui/src/lib/stores/tokens.ts +++ b/packages/ui/src/lib/stores/tokens.ts @@ -1,4 +1,3 @@ -import { DEFAULT_GO_AMOUNT } from '$lib/constants' import { writable, type Writable } from 'svelte/store' export interface TokenData { @@ -15,8 +14,8 @@ export type TokenStore = Writable function createTokenStore(): TokenStore { const epochDuration = 8 * 60 * 60 * 1000 const store = writable({ - go: DEFAULT_GO_AMOUNT, - repTotal: 55, + go: 0, + repTotal: 0, repStaked: 0, loading: false, epochDuration, diff --git a/packages/ui/src/lib/stores/transaction.ts b/packages/ui/src/lib/stores/transaction.ts index 9eee70a..31a58d5 100644 --- a/packages/ui/src/lib/stores/transaction.ts +++ b/packages/ui/src/lib/stores/transaction.ts @@ -1,13 +1,6 @@ import { writable, type Writable } from 'svelte/store' -type TransactionType = - | 'publish persona' - | 'promote' - | 'demote' - | 'publish post' - | 'vote_win' - | 'post_included' - | 'post_rejected' +export type TransactionType = 'publish persona' | 'promote' | 'demote' | 'publish post' export interface TransactionRecord { timestamp: number diff --git a/packages/ui/src/routes/persona/[id]/pending/[postId]/+page.svelte b/packages/ui/src/routes/persona/[id]/pending/[postId]/+page.svelte index e4b4b1f..904fd44 100644 --- a/packages/ui/src/routes/persona/[id]/pending/[postId]/+page.svelte +++ b/packages/ui/src/routes/persona/[id]/pending/[postId]/+page.svelte @@ -206,7 +206,7 @@ on:click={adapter.signIn} disabled={!canConnectWallet()} /> - {:else if $profile.signer !== undefined && $profile.address !== post.address} + {:else if $profile.signer !== undefined && !post.myPost}