diff --git a/main.js b/main.js index 04507aa..72f9189 100644 --- a/main.js +++ b/main.js @@ -4,12 +4,28 @@ const URL = require('node:url') const p = require('node:util').promisify const Keypair = require('ppppp-keypair') +// WARNING monkey patch! -------------------------------------- +const na = require('sodium-native') +na.sodium_malloc = function sodium_malloc_monkey_patched(n) { + return Buffer.alloc(n) +} +na.sodium_free = function sodium_free_monkey_patched() {} +// Electron > 20.3.8 breaks a napi method that `sodium_malloc` +// depends on to create external buffers. (see v8 memory cage) +// +// This crashes electron when called by various libraries, so +// we monkey-patch this particular function. +// ------------------------------------------------------------ + process.env.ZOOBOARD_DATA ??= Path.join(app.getPath('appData'), 'zooboard') app.setPath('userData', process.env.ZOOBOARD_DATA) const path = Path.resolve(app.getPath('userData'), 'ppppp') const keypairPath = Path.join(path, 'keypair.json') const keypair = Keypair.loadOrCreateSync(keypairPath) +let mainWindow +let globalAccountID = null +let globalAccountName = null const peer = require('secret-stack/bare')() .use(require('secret-stack/plugins/net')) @@ -50,23 +66,10 @@ const peer = require('secret-stack/bare')() }, }) -// WARNING monkey patch! -------------------------------------- -const na = require('sodium-native') -na.sodium_malloc = function sodium_malloc_monkey_patched(n) { - return Buffer.alloc(n) -} -na.sodium_free = function sodium_free_monkey_patched() {} -// Electron > 20.3.8 breaks a napi method that `sodium_malloc` -// depends on to create external buffers. (see v8 memory cage) -// -// This crashes electron when called by various libraries, so -// we monkey-patch this particular function. -// ------------------------------------------------------------ - function createWindow() { - const mainWindow = new BrowserWindow({ + mainWindow = new BrowserWindow({ width: 1200, - height: 800, + height: 1200, title: 'Zooboard', webPreferences: { preload: Path.join(__dirname, 'preload.js'), @@ -82,27 +85,67 @@ function createWindow() { }) mainWindow.loadURL(startUrl) - // mainWindow.webContents.openDevTools() + mainWindow.webContents.openDevTools({ mode: 'bottom', activate: true }) } async function loadAccount() { + if (globalAccountID !== null) { + return { id: globalAccountID, name: globalAccountName } + } await peer.db.loaded() const id = await p(peer.db.account.findOrCreate)({ subdomain: 'account' }) + globalAccountID = id await p(peer.set.load)(id) await p(peer.dict.load)(id) const profile = peer.dict.read(id, 'profile') - return { id, name: profile?.name ?? '' } + const name = profile?.name ?? '' + globalAccountName = name + return { id, name } } -async function setProfileName(ev, name,b,c,d) { - console.log('ev',ev,'name',name,b,c,d); +async function setProfileName(ev, name) { await p(peer.dict.update)('profile', { name }) return name } +async function writeElements(ev, actions) { + if (globalAccountID === null) throw new Error('account not loaded') + for (const action of actions) { + await p(peer.db.feed.publish)({ + account: globalAccountID, + domain: 'zooboardElements', + data: action, + }) + } +} + +function subscribeToReadElements() { + const elementsByID = new Map() + for (const msg of peer.db.msgs()) { + if (msg.data && msg.metadata.domain === 'zooboardElements') { + const { id, isDeleted } = msg.data + if (isDeleted) { + elementsByID.delete(id) + } else { + elementsByID.set(id, msg.data) + } + } + } + const initialElements = [...elementsByID.values()] + mainWindow.webContents.send('readElements', initialElements) + + peer.db.onRecordAdded(({ msg }) => { + if (msg.data && msg.metadata.domain === 'zooboardElements') { + mainWindow.webContents.send('readElements', [msg.data]) + } + }) +} + app.whenReady().then(() => { ipcMain.handle('loadAccount', loadAccount) ipcMain.handle('setProfileName', setProfileName) + ipcMain.handle('writeElements', writeElements) + ipcMain.handle('subscribeToReadElements', subscribeToReadElements) createWindow() app.on('activate', function () { diff --git a/preload.js b/preload.js index 9a8e2a3..3b5131b 100644 --- a/preload.js +++ b/preload.js @@ -3,4 +3,9 @@ const { contextBridge, ipcRenderer } = require('electron/renderer') contextBridge.exposeInMainWorld('electronAPI', { loadAccount: () => ipcRenderer.invoke('loadAccount'), setProfileName: (name) => ipcRenderer.invoke('setProfileName', name), + writeElements: (actions) => ipcRenderer.invoke('writeElements', actions), + onReadElements: (callback) => { + ipcRenderer.invoke('subscribeToReadElements') + ipcRenderer.on('readElements', (_event, value) => callback(value)) + } }) diff --git a/src/App.js b/src/App.js index 3b53dca..c46057a 100644 --- a/src/App.js +++ b/src/App.js @@ -52,6 +52,38 @@ function MyAccount() { } function App() { + const elemsPersisted = new Map() + const [excalidrawAPI, setExcalidrawAPI] = useState(null) + + function loadExcalidraw(api) { + if (excalidrawAPI) return + setExcalidrawAPI(api) + window.electronAPI.onReadElements((elems) => { + for (const elem of elems) { + if (elem.isDeleted) { + elemsPersisted.delete(elem.id) + } else { + elemsPersisted.set(elem.id, elem) + } + } + api.updateScene({ elements: [...elemsPersisted.values()] }) + }) + } + + const updateElements = debounce((elems) => { + const actions = [] + for (const elem of elems) { + const oldVersion = elemsPersisted.get(elem.id)?.version ?? 0 + if (elem.version > oldVersion) { + actions.push(elem) + elemsPersisted.set(elem.id, { ...elem }) + } + } + if (actions.length > 0) { + window.electronAPI.writeElements(actions) + } + }, 100) + return (
@@ -73,6 +105,8 @@ function App() { image: false, }, }} + onChange={updateElements} + excalidrawAPI={loadExcalidraw} />