Skip to content

Commit

Permalink
Merge pull request #3 from velascoandres/feat/whiteboard-gzip
Browse files Browse the repository at this point in the history
Feat/whiteboard gzip
  • Loading branch information
velascoandres authored Mar 30, 2024
2 parents d63a858 + c03f714 commit 8406c86
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 74 deletions.
28 changes: 27 additions & 1 deletion src/app/_whiteboards/components/whiteboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
type ExcalidrawInitialDataState
} from '@excalidraw/excalidraw/types/types'

import { decompressContent } from '@/lib/compress-whiteboard'


export interface Content {
scene: {
Expand All @@ -31,6 +33,13 @@ interface Props {
onChange?: WhiteboardChangeEventHandler
}

interface RawContentProps extends Omit<Props, 'initialContent'> {
id: number
viewModeEnabled?: boolean
initialRawCompressed?: string
onChange?: WhiteboardChangeEventHandler
}

const Excalidraw = dynamic(
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
{
Expand Down Expand Up @@ -96,4 +105,21 @@ export const Whiteboard = ({
</Excalidraw>
)
}



export const WhiterboardFromCompressed = ({ initialRawCompressed,
...rest
}: RawContentProps) => {

const [initialContent, setInitialContent] = useState<Content>()

useEffect(() => {
if (!initialRawCompressed){
return
}
void decompressContent(initialRawCompressed)
.then((content) => setInitialContent(content as unknown as Content))
}, [initialRawCompressed])

return <Whiteboard {...rest} initialContent={initialContent} />
}
47 changes: 30 additions & 17 deletions src/app/_whiteboards/hooks/use-whiteboard.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client'

import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import compare from 'just-compare'

import { type ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { type AppState, type BinaryFiles } from '@excalidraw/excalidraw/types/types'

import { useDebounceCallback } from '@/app/_shared/hooks/use-debounce-callback'
import * as exportUtils from '@/lib/export-whiteboard'
import { compressContent } from '@/lib/compress-whiteboard'
import * as exportUtils from '@/lib/export-whiteboard'
import { api } from '@/trpc/react'

import { type Content } from '../components/whiteboard'
Expand All @@ -19,24 +20,24 @@ export const useWhiteboard = (id: number) => {

const { data: whiteboard, isLoading } = api.whiteboard.findUserWhiteboardById.useQuery({
id,
}, {
}, {
enabled: Boolean(id),
cacheTime: Infinity,
queryKey: ['whiteboard.findUserWhiteboardById', { id }],
})

const [currentWhiteboard, setWhiteboard] = useState<typeof whiteboard>(whiteboard)
const tmpContent = useRef<Record<string, unknown> | null>(null)


const { mutate: updateContent } = api.whiteboard.updateUserWhiteboardContent.useMutation({
onSuccess: () => {
void utils.whiteboard.findUserWhiteboardById.invalidate()
},
onMutate: async ({ content }) => {
onMutate: async () => {

setWhiteboard({
...currentWhiteboard,
content,
content: tmpContent.current,
} as typeof whiteboard)

return { currentWhiteboard }
Expand All @@ -58,34 +59,43 @@ export const useWhiteboard = (id: number) => {

const filterdElements = elements
.filter(({ isDeleted }) => !isDeleted)
.map((element) =>({
.map((element) => ({
...element,
customData: element.customData ?? null
}))

const payload = {
scene: {
elements: filterdElements,
scene: {
elements: filterdElements,
appState: {
viewBackgroundColor: appState.viewBackgroundColor,
currentItemFontFamily: appState.currentItemFontFamily,
currentItemFontSize: appState.currentItemFontSize,
theme: appState.theme,
exportWithDarkMode: appState.exportWithDarkMode,
gridSize: appState.gridSize,
},
scrollToContent: true,
},
scrollToContent: true,
files: filesToUpdate,
},
}

const areSame = compare(payload, currentWhiteboard.content)


if (areSame){
if (areSame) {
return
}


const content = {
scene: {
...payload.scene,
},
}

tmpContent.current = content


const updatedWhitheboard = {
id: currentWhiteboard.id,
content: {
Expand All @@ -95,9 +105,12 @@ export const useWhiteboard = (id: number) => {
}
}

updateContent(updatedWhitheboard)

void updatePreview(updatedWhitheboard as Whiteboard)
void compressContent(content)
.then((compressedRawContent) => updateContent({
id: currentWhiteboard.id,
compressedRawContent
}))
.then(() => updatePreview(updatedWhitheboard as Whiteboard))
}

const onChangeHandler = (elements: ExcalidrawElement[], appState: AppState, files?: BinaryFiles) => {
Expand Down
4 changes: 2 additions & 2 deletions src/app/view-whiteboard/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { type Metadata,type ResolvingMetadata } from 'next'
import { redirect } from 'next/navigation'

import Loading from '@/app/(protected)/loading'
import findPublicWhiteboardById from '@/server/api/whiteboard/usecases/find-public-whiteboard'
import findWhiteboardInfo from '@/server/api/whiteboard/usecases/find-whiteboard-info'
import { db } from '@/server/db'


export async function generateMetadata({ params }: {params: {id: string}}, parent: ResolvingMetadata) {
const id = Number(params.id)

const whiteboard = await findPublicWhiteboardById(db, { id, omitContent: true })
const whiteboard = await findWhiteboardInfo(db, { id, isPublic: true })


if (!whiteboard){
Expand Down
19 changes: 9 additions & 10 deletions src/app/view-whiteboard/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,42 @@
import React from 'react'
import { redirect } from 'next/navigation'

import { type Content,Whiteboard } from '@/app/_whiteboards/components/whiteboard'
import { WhiterboardFromCompressed } from '@/app/_whiteboards/components/whiteboard'
import { WhiteboardHeader } from '@/app/_whiteboards/components/whiteboard-header'
import { type PublicWhiteboard } from '@/app/_whiteboards/interfaces/whiteboard'
import findPublicWhiteboardById from '@/server/api/whiteboard/usecases/find-public-whiteboard'
import findWhiteboardContent from '@/server/api/whiteboard/usecases/find-whiteboard-content'
import { db } from '@/server/db'


const getWhiteboard = async (id: number) => {
const whiteboard = await findPublicWhiteboardById(db, { id })
const whiteboard = await findWhiteboardContent(db, { id, isPublic: true })

if (!whiteboard){
redirect('/not-found')
}

return whiteboard as unknown as PublicWhiteboard & {content: undefined | Content}
return whiteboard
}



const WhitebardViewPage = async ({ params }: {params: {id: string}}) => {
const whiteboardId = Number(params.id)

const whiteboard: PublicWhiteboard = await getWhiteboard(whiteboardId)
const whiteboard = await getWhiteboard(whiteboardId)

return (
<main className="h-screen w-screen">
<Whiteboard
<WhiterboardFromCompressed
viewModeEnabled
id={whiteboard?.id}
initialContent={whiteboard?.content as Content}
initialRawCompressed={whiteboard.compressedRawContent}
/>
<WhiteboardHeader
title={whiteboard.name}
description={whiteboard.description ?? ''}
creator={{
name: whiteboard.createdBy.name,
avatarUrl: whiteboard.createdBy.image
name: whiteboard.createdBy.name ?? '',
avatarUrl: whiteboard.createdBy.image ?? ''
}}
/>
</main>
Expand Down
9 changes: 8 additions & 1 deletion src/dtos/whiteboard-dtos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from 'zod'

import { SearchByIdDto } from './shared-dtos'

export const CreateWhiteboardDto = z.object({
name: z.string().min(1, 'A name is required').max(30, 'The name must be a maximum of 30 characters.'),
description: z.string().max(180, 'The description must be a maximum of 180 characters.').optional(),
Expand All @@ -13,5 +15,10 @@ export const UpdateWhiteboardDto = z.object({

export const UpdateWhiteboardContentDto = z.object({
id: z.number(),
content: z.unknown(),
compressedRawContent: z.string(),
})


export const SearchWhitheboard = z.object({
isPublic: z.boolean().optional(),
}).merge(SearchByIdDto)
12 changes: 12 additions & 0 deletions src/lib/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const b64encode = (arrayBuffer: ArrayBuffer) => {
const buffer = Buffer.from(arrayBuffer)
const base64String = buffer.toString('base64')

return base64String
}

export const b64decode = (base64: string) => {
const buffer = Buffer.from(base64, 'base64')

return new Uint8Array(buffer)
}
21 changes: 21 additions & 0 deletions src/lib/compress-whiteboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { b64encode } from './base64'
import {
b64toStream,
compressStream,
decompressStream,
JSONtoStream,
responseToBuffer
} from './compress'

export const compressContent = async (content: Record<string, unknown>) => {
const compressedStream = await compressStream(JSONtoStream(content))
const compressedBuffer = await responseToBuffer(compressedStream)

return b64encode(compressedBuffer)
}

export const decompressContent = (base64: string): Promise<Record<string, unknown>> => {
const contentResponse = decompressStream(b64toStream(base64))

return contentResponse.json() as Promise<Record<string, unknown>>
}
50 changes: 50 additions & 0 deletions src/lib/compress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { b64decode,b64encode } from './base64'

export const JSONtoStream = (data: object) => {
return new Blob([JSON.stringify(data)], {
type: 'text/plain'
}).stream()
}

export const b64toStream = (b64: string) => {
return new Blob([b64decode(b64)], {
type: 'text/plain'
}).stream()
}

export const responseToJSON = async (response: Response): Promise<Record<string, unknown>> => {
const blob = await response.blob()

const textResponse = await blob.text()

return JSON.parse(textResponse) as Record<string, unknown>
}

export const responseToB64 = async (response: Response) => {
const blob = await response.blob()
const buffer = await blob.arrayBuffer()

return b64encode(buffer)
}

export const responseToBuffer = async (response: Response) => {
const blob = await response.blob()

return blob.arrayBuffer()
}

export const compressStream = async (stream: ReadableStream<Uint8Array>) => {
const compressedReadableStream = stream.pipeThrough(
new CompressionStream('gzip')
)

return new Response(compressedReadableStream)
}

export const decompressStream = (stream: ReadableStream<Uint8Array>) => {
const compressedReadableStream = stream.pipeThrough(
new DecompressionStream('gzip')
)

return new Response(compressedReadableStream)
}
42 changes: 0 additions & 42 deletions src/server/api/whiteboard/usecases/find-public-whiteboard.ts

This file was deleted.

Loading

0 comments on commit 8406c86

Please sign in to comment.