diff --git a/hooks/useLoadOnMount.ts b/hooks/useLoadOnMount.ts index d6599208d..11764506d 100644 --- a/hooks/useLoadOnMount.ts +++ b/hooks/useLoadOnMount.ts @@ -10,10 +10,10 @@ export default function useLoadOnMount(roomId?: string) { fonts.load('12px Verveine Regular', 'Fonts are loaded!').then(() => { state.send('MOUNTED', { roomId }) - if (roomId !== undefined) { - state.send('RT_LOADED_ROOM', { id: roomId }) - coopState.send('JOINED_ROOM', { id: roomId }) - } + // if (roomId !== undefined) { + // state.send('RT_LOADED_ROOM', { id: roomId }) + // coopState.send('JOINED_ROOM', { id: roomId }) + // } }) } else { setTimeout(() => state.send('MOUNTED'), 1000) diff --git a/pages/_app.tsx b/pages/_app.tsx index bc5b72301..7f97e9e92 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,8 +3,11 @@ import Head from 'next/head' import { AppProps } from 'next/app' import { globalStyles } from 'styles' import { Provider } from 'next-auth/client' +import { init } from 'utils/sentry' import 'styles/globals.css' +init() + function MyApp({ Component, pageProps }: AppProps): JSX.Element { globalStyles() diff --git a/state/commands/create-page.ts b/state/commands/create-page.ts index 540b961c9..b4dd21863 100644 --- a/state/commands/create-page.ts +++ b/state/commands/create-page.ts @@ -17,15 +17,14 @@ export default function createPage(data: Data, goToPage = true): void { data.document.pages[page.id] = page data.pageStates[page.id] = pageState - data.currentPageId = page.id - storage.savePage(data, data.document.id, page.id) - storage.saveDocumentToLocalStorage(data) - if (goToPage) { data.currentPageId = page.id } else { data.currentPageId = currentPageId } + + storage.savePage(data, data.document.id, page.id) + storage.saveDocumentToLocalStorage(data) }, undo(data) { const { page, currentPageId } = snapshot diff --git a/state/coop/client-liveblocks.ts b/state/coop/client-liveblocks.ts index 61164ac33..b0801f675 100644 --- a/state/coop/client-liveblocks.ts +++ b/state/coop/client-liveblocks.ts @@ -70,6 +70,8 @@ class CoopClient { } disconnect(): CoopClient { + if (!this.room) return + this.room.unsubscribe('connection', this.handleConnectionEvent) this.room.unsubscribe('my-presence', this.handleMyPresenceEvent) this.room.unsubscribe('others', this.handleOthersEvent) @@ -79,6 +81,8 @@ class CoopClient { } reconnect(): CoopClient { + if (!this.room) return + this.connect(this.roomId) return this } @@ -128,6 +132,8 @@ class CoopClient { } clearCursor(): CoopClient { + if (!this.room) return + this.room.updatePresence({ cursor: null }) return this } diff --git a/state/shape-utils/group.tsx b/state/shape-utils/group.tsx index 46531cf53..025b6ccd1 100644 --- a/state/shape-utils/group.tsx +++ b/state/shape-utils/group.tsx @@ -34,6 +34,7 @@ const group = registerShapeUtils({ width={size[0]} height={size[1]} data-shy={true} + fill="none" /> ) }, @@ -169,6 +170,7 @@ const group = registerShapeUtils({ const StyledGroupShape = styled('rect', { zDash: 5, zStrokeWidth: 1, + stroke: '$text', }) export default group diff --git a/state/state.ts b/state/state.ts index b3b6dab65..0a90a130d 100644 --- a/state/state.ts +++ b/state/state.ts @@ -160,7 +160,8 @@ const state = createState({ on: { MOUNTED: [ 'resetHistory', - { unless: 'hasRoomId', do: 'restoredPreviousDocument' }, + 'resetStorage', + 'restoredPreviousDocument', { to: 'ready' }, ], }, @@ -174,26 +175,9 @@ const state = createState({ }, on: { UNMOUNTED: { - do: [ - 'saveAppState', - 'saveDocumentState', - 'resetDocumentState', - 'resetHistory', - ], + do: ['saveDocumentState', 'resetDocumentState'], to: 'loading', }, - // Network-Related - RT_LOADED_ROOM: [ - 'clearRoom', - { if: 'hasRoom', do: ['resetDocumentState', 'resetHistory'] }, - ], - // RT_UNLOADED_ROOM: ['clearRoom', 'resetDocumentState'], - // RT_DISCONNECTED_ROOM: ['clearRoom', 'resetDocumentState'], - // RT_CREATED_SHAPE: 'addRtShape', - // RT_CHANGED_STATUS: 'setRtStatus', - // RT_DELETED_SHAPE: 'deleteRtShape', - // RT_EDITED_SHAPE: 'editRtShape', - // Client RESIZED_WINDOW: 'resetPageState', RESET_DOCUMENT_STATE: [ 'resetHistory', @@ -306,7 +290,7 @@ const state = createState({ }, SAVED: { unlessAny: ['isInSession', 'isReadOnly'], - do: 'forceSave', + do: ['saveDocumentState', 'saveToFileSystem'], }, LOADED_FROM_FILE: { unless: 'isInSession', @@ -346,15 +330,18 @@ const state = createState({ RESET_CAMERA: 'resetCamera', COPIED_TO_SVG: 'copyToSvg', LOADED_FROM_FILE_STSTEM: 'loadFromFileSystem', - SAVED_AS_TO_FILESYSTEM: 'saveAsToFileSystem', - SAVED_TO_FILESYSTEM: { - unless: 'isReadOnly', - then: { - if: 'isReadOnly', - do: 'saveAsToFileSystem', - else: 'saveToFileSystem', + SAVED_AS_TO_FILESYSTEM: ['saveDocumentState', 'saveAsToFileSystem'], + SAVED_TO_FILESYSTEM: [ + 'saveDocumentState', + { + unless: 'isReadOnly', + then: { + if: 'isReadOnly', + do: 'saveAsToFileSystem', + else: 'saveToFileSystem', + }, }, - }, + ], }, initial: 'selecting', states: { @@ -1281,30 +1268,38 @@ const state = createState({ clearRoom(data) { data.room = undefined }, - resetDocumentState(data) { - data.document.id = uniqueId() + resetStorage() { + storage.reset() + }, + resetDocumentState(data, payload: { roomId?: string }) { + // Save the current document and app state. + storage.savePage(data) + storage.savePageState(data) + storage.saveAppStateToLocalStorage(data) + storage.saveDocumentToLocalStorage(data) + // Cancel all current sessions, reset history, etc.. session.cancel(data) - inputs.reset() - history.reset() + storage.reset() - const newId = 'page1' - - data.currentPageId = newId + // Populate a new app state. + const newDocumentId = payload?.roomId ? payload.roomId : uniqueId() + const newPageId = 'page1' + data.document.id = newDocumentId data.pointedId = null data.hoveredId = null data.editingId = null - data.currentPageId = 'page1' - data.currentParentId = 'page1' + data.currentPageId = newPageId + data.currentParentId = newPageId data.currentCodeFileId = 'file0' data.codeControls = {} data.document.pages = { - [newId]: { - id: newId, + [newPageId]: { + id: newPageId, name: 'Page 1', type: 'page', shapes: {}, @@ -1313,8 +1308,8 @@ const state = createState({ } data.pageStates = { - [newId]: { - id: newId, + [newPageId]: { + id: newPageId, selectedIds: new Set(), camera: { point: [0, 0], @@ -1322,6 +1317,12 @@ const state = createState({ }, }, } + + // Save the new app state. + storage.savePage(data) + storage.savePageState(data) + storage.saveAppStateToLocalStorage(data) + storage.saveDocumentToLocalStorage(data) }, resetPageState(data) { const pageState = data.pageStates[data.currentPageId] @@ -1344,6 +1345,7 @@ const state = createState({ /* --------------------- Shapes --------------------- */ resetShapes(data) { const page = tld.getPage(data) + Object.values(page.shapes).forEach((shape) => { page.shapes[shape.id] = { ...shape } }) @@ -2052,8 +2054,8 @@ const state = createState({ /* ---------------------- Data ---------------------- */ - restoredPreviousDocument(data) { - storage.firstLoad(data) + restoredPreviousDocument(data, payload: { roomId?: string }) { + storage.firstLoad(data, payload?.roomId) }, saveToFileSystem(data) { @@ -2077,6 +2079,9 @@ const state = createState({ }, saveDocumentState(data) { + storage.savePage(data) + storage.savePageState(data) + storage.saveAppStateToLocalStorage(data) storage.saveDocumentToLocalStorage(data) }, @@ -2084,14 +2089,6 @@ const state = createState({ storage.saveToFileSystem(data) }, - savePage(data) { - storage.savePage(data) - }, - - loadPage(data) { - storage.loadPage(data) - }, - saveCode(data, payload: { code: string }) { data.document.code[data.currentCodeFileId].code = payload.code storage.saveDocumentToLocalStorage(data) diff --git a/state/storage.ts b/state/storage.ts index fec2faef2..66846c6a1 100644 --- a/state/storage.ts +++ b/state/storage.ts @@ -13,14 +13,9 @@ function storageId(fileId: string, label: string, id?: string) { class Storage { previousSaveHandle?: any // FileSystemHandle - constructor() { - // this.loadPreviousHandle() // Still needs debugging - } - - firstLoad(data: Data) { - const lastOpenedFileId = localStorage.getItem( - `${CURRENT_VERSION}_lastOpened` - ) + firstLoad(data: Data, roomId?: string) { + const lastOpenedFileId = + roomId || localStorage.getItem(`${CURRENT_VERSION}_lastOpened`) // 1. Load Document from Local Storage // Using the "last opened file id" in local storage. @@ -30,9 +25,9 @@ class Storage { storageId(lastOpenedFileId, 'document-state', lastOpenedFileId) ) - if (savedState === null) { + if (!savedState) { // If no state with that document was found, create a fresh random id. - data.document.id = uniqueId() + data.document.id = roomId ? roomId : uniqueId() } else { // If we did find a state and document, load it into state. const restoredDocument: Data = JSON.parse(decompress(savedState)) @@ -42,11 +37,7 @@ class Storage { } } - try { - this.load(data) - } catch (error) { - console.error(error) - } + this.load(data) } saveAppStateToLocalStorage = (data: Data) => { @@ -63,6 +54,8 @@ class Storage { storageId(data.document.id, 'document', data.document.id), compress(JSON.stringify(document)) ) + + localStorage.setItem(`${CURRENT_VERSION}_lastOpened`, data.document.id) } getCompleteDocument = (data: Data) => { @@ -138,7 +131,9 @@ class Storage { if (savedPageState !== null) { // If we've found a page state in local storage, set it into state. data.pageStates[pageId] = JSON.parse(decompress(savedPageState)) - data.pageStates[pageId].selectedIds = new Set([]) + data.pageStates[pageId].selectedIds = new Set( + data.pageStates[pageId].selectedIds + ) } else { // Or else create a new one. data.pageStates[pageId] = { @@ -154,15 +149,31 @@ class Storage { // 3. Restore the last page state // Using the "last page state" in local storage. - const savedPageState = localStorage.getItem( - storageId(data.document.id, 'lastPageState', data.document.id) - ) - if (savedPageState !== null) { + try { + const savedPageState = localStorage.getItem( + storageId(data.document.id, 'lastPageState', data.document.id) + ) const pageState = JSON.parse(decompress(savedPageState)) + + if (!data.document.pages[pageState.id]) { + throw new Error('Page state id not in document') + } + pageState.selectedIds = new Set([]) data.pageStates[pageState.id] = pageState data.currentPageId = pageState.id + } catch (e) { + console.error('Could not restore page state:', e.message) + + data.pageStates[data.currentPageId] = { + id: data.currentPageId, + selectedIds: new Set([]), + camera: { + point: [0, 0], + zoom: 1, + }, + } } // 4. Save the current app state / document @@ -203,6 +214,9 @@ class Storage { } }) + // Load the current page + this.loadPage(data, data.document.id, data.currentPageId) + // Update camera for the new page state document.documentElement.style.setProperty( '--camera-zoom', @@ -247,13 +261,13 @@ class Storage { data.currentPageId = pageId - // Get saved page from local storage - const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId)) - - if (savedPage !== null) { - // If we have a page, move it into state + try { + // If we have a page in local storage, move it into state + const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId)) data.document.pages[pageId] = JSON.parse(decompress(savedPage)) - } else { + } catch (e) { + console.warn('Could not load a page with the id', pageId) + // If we don't have a page, create a new page data.document.pages[pageId] = { id: pageId, @@ -309,23 +323,20 @@ class Storage { /* ------------------- File System ------------------ */ + reset = () => { + this.previousSaveHandle = undefined + } + saveToFileSystem = (data: Data) => { - this.saveAppStateToLocalStorage(data) - this.saveDocumentToLocalStorage(data) - this.saveDataToFileSystem(data, data.document.id, false) + this.saveDataToFileSystem(data, false) } saveAsToFileSystem = (data: Data) => { - this.saveAppStateToLocalStorage(data) - this.saveDocumentToLocalStorage(data) - this.saveDataToFileSystem(data, uniqueId(), true) + this.saveDataToFileSystem(data, true) } - saveDataToFileSystem = async ( - data: Data, - fileId: string, - saveAs: boolean - ) => { + saveDataToFileSystem = async (data: Data, saveAs: boolean) => { + const isSavingAs = saveAs || !this.previousSaveHandle const document = this.getCompleteDocument(data) // Then save to file system @@ -351,12 +362,14 @@ class Storage { blob, { fileName: `${ - saveAs ? documentName : this.previousSaveHandle?.name || 'My Document' + isSavingAs + ? documentName + : this.previousSaveHandle?.name || 'My Document' }.tldr`, description: 'tldraw file', extensions: ['.tldr'], }, - saveAs ? undefined : this.previousSaveHandle, + isSavingAs ? undefined : this.previousSaveHandle, true ) .then((handle) => { diff --git a/utils/sentry.ts b/utils/sentry.ts new file mode 100644 index 000000000..31c4a8bf5 --- /dev/null +++ b/utils/sentry.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node' +import { RewriteFrames } from '@sentry/integrations' + +export function init(): void { + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return + + const integrations = [] + + if ( + process.env.NEXT_IS_SERVER === 'true' && + process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR + ) { + // For Node.js, rewrite Error.stack to use relative paths, so that source + // maps starting with ~/_next map to files in Error.stack with path + // app:///_next + integrations.push( + new RewriteFrames({ + iteratee: (frame) => { + frame.filename = frame.filename.replace( + process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR, + 'app:///' + ) + frame.filename = frame.filename.replace('.next', '_next') + return frame + }, + }) + ) + } + + Sentry.init({ + enabled: process.env.NODE_ENV === 'production', + integrations, + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + release: process.env.NEXT_PUBLIC_COMMIT_SHA, + }) +}