diff --git a/README.md b/README.md index 4db307d..239aa06 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # Zooboard -To develop: +To run it yourself: 1. `npm install` -2. `npm start` in one terminal, `npm run electron` in another -3. Edit JS code -4. Sometimes you may need to re-run step 2 +2. Download, install, and run [pzp-hub][pzp-hub]. +3. In the zooboard repo, `npm run start-all` + +To run with a friend: +1. Both `npm install` +2. One of you downloads, installs, and runs [pzp-hub][pzp-hub] on a machine with a public IP. +3. You both, in the zooboard repo, run `npm start` in one terminal and `npm run electron` in another. + +To use: +1. One peer goes to the hub website (default on localhost is [0.0.0.0:3000](http://0.0.0.0:3000)), gets the admin invite, and uses it in the app by clicking "Join". +2. That peer clicks "Invite" in the app and gives that invite to the other peer. +3. The other peer uses that invite by clicking "Join". +4. Draw together and be happy! +5. [Discuss bugs here](https://codeberg.org/pzp/pzp-hub/issues) or pzp in general on the #pzp hashtag on your preferred social network. + +[pzp-hub]: https://codeberg.org/pzp/pzp-hub \ No newline at end of file diff --git a/main.js b/main.js index 58f5393..0fed6b4 100644 --- a/main.js +++ b/main.js @@ -20,7 +20,7 @@ na.sodium_free = function sodium_free_monkey_patched() {} 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 path = process.env.PZP_DIR ?? Path.resolve(app.getPath('userData'), 'ppppp') const keypairPath = Path.join(path, 'keypair.json') const keypair = Keypair.loadOrCreateSync(keypairPath) @@ -135,10 +135,12 @@ async function writeElements(ev, elements) { async function createInvite() { if (globalAccountID === null) throw new Error('account not loaded') - const { url } = await p(peer.invite.createForFriend)({ + let { url } = await p(peer.invite.createForFriend)({ hubs: 1, id: globalAccountID, }) + // if the hub is on localhost, it's probably on the default port of 3000, so let's make things a bit easier for the user + if (url.indexOf('0.0.0.0') !== -1) url = url.replace("0.0.0.0", "0.0.0.0:3000") return url } @@ -228,7 +230,10 @@ async function handlePPPPPUri(ev, uri) { setTimeout(handlePPPPPUri, 100, null, uri) return } - if (!uri.startsWith('ppppp://')) return console.log('Not a ppppp:// URI', uri) + if (uri.startsWith("http:") || uri.startsWith("https://")) { + uri = decodeURIComponent(uri.split('/invite#')[1]) + } + if (!uri.startsWith('ppppp://')) return console.log('Not a ppppp invite URI', uri) const commands = peer.invite.parse(uri) for (const command of commands) { console.log('Executing command', JSON.stringify(command)) @@ -277,20 +282,20 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient('ppppp') } -const hasLock = app.requestSingleInstanceLock() +//const hasLock = app.requestSingleInstanceLock() -if (!hasLock) { - app.quit() -} else { - app.on('second-instance', (ev, argv, cwd, extraData) => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() - if (argv.length > 1) { - handlePPPPPUri(null, argv[argv.length - 1]) - } - } - }) +//if (!hasLock) { +// app.quit() +//} else { +// app.on('second-instance', (ev, argv, cwd, extraData) => { +// if (mainWindow) { +// if (mainWindow.isMinimized()) mainWindow.restore() +// mainWindow.focus() +// if (argv.length > 1) { +// handlePPPPPUri(null, argv[argv.length - 1]) +// } +// } +// }) app.whenReady().then(() => { ipcMain.handle('loadAccount', loadAccount) @@ -313,4 +318,4 @@ if (!hasLock) { if (process.platform !== 'darwin') app.quit() }) }) -} +//} diff --git a/package-lock.json b/package-lock.json index c516f05..2152f13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "concurrently": "^8.2.2", "debounce": "2.0", "ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "ppppp-conductor": "github:staltz/ppppp-conductor#8ebeb0fb12de766fac21f8292121307cf7d1bd1e", @@ -6777,6 +6778,148 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -7323,6 +7466,21 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debounce": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", @@ -17303,6 +17461,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", @@ -18083,6 +18249,11 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -19032,6 +19203,14 @@ "node": ">=8" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", diff --git a/package.json b/package.json index 49aeaa5..12f7755 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,21 @@ "main": "main.js", "homepage": "./", "scripts": { - "start": "BROWSER=none react-scripts start", + "start": "PORT=3001 BROWSER=none react-scripts start", + "start2": "PZP_DIR=/tmp/zooboard-test PORT=3002 BROWSER=none react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "electron": "ELECTRON_START_URL=http://localhost:3000 DEBUG=*,-shse,-pull-secretstream electron ." + "electron": "ELECTRON_START_URL=http://localhost:3001 DEBUG=*,-shse,-pull-secretstream electron .", + "electron2": "PZP_DIR=/tmp/zooboard-test ELECTRON_START_URL=http://localhost:3002 DEBUG=*,-shse,-pull-secretstream electron .", + "start-all": "concurrently --kill-others \"npm start\" \"./scripts/wait.sh 3001 && npm run electron\" \"npm run start2\" \"./scripts/wait.sh 3002 && npm run electron2\"" }, "dependencies": { "@excalidraw/excalidraw": "0.17.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "concurrently": "^8.2.2", "debounce": "2.0", "ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "ppppp-db": "github:staltz/ppppp-db#cf1532965ea1d16929ed2291a9b737a4ce74caac", diff --git a/scripts/wait.sh b/scripts/wait.sh new file mode 100755 index 0000000..89c67f8 --- /dev/null +++ b/scripts/wait.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# waits until a certain port is reachable +WAIT_PORT=$1 +bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' localhost $WAIT_PORT \ No newline at end of file diff --git a/src/App.js b/src/App.js index 09cf2e1..1ab0e0b 100644 --- a/src/App.js +++ b/src/App.js @@ -16,6 +16,7 @@ function App() { const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [inviteCode, setInviteCode] = useState(null) + const [inviteErr, setInviteErr] = useState(null) const [inviteModalOpen, setInviteModalOpen] = useState(false) const openInviteModal = () => setInviteModalOpen(true) @@ -45,7 +46,11 @@ function App() { setInviteCode(null) openInviteModal() window.electronAPI.createInvite().then((invite) => { + setInviteErr(null) setInviteCode(invite) + }).catch(err => { + setInviteCode(null) + setInviteErr(err) }) } @@ -102,6 +107,7 @@ function App() { isOpen={inviteModalOpen} onClose={closeInviteModal} inviteCode={inviteCode} + inviteErr={inviteErr} /> diff --git a/src/CreateInviteModal.js b/src/CreateInviteModal.js index 0b44c6b..f7db91f 100644 --- a/src/CreateInviteModal.js +++ b/src/CreateInviteModal.js @@ -1,7 +1,7 @@ import Button from './Button' import Modal from './Modal' -function CreateInviteModal({ isOpen, onClose, inviteCode }) { +function CreateInviteModal({ isOpen, onClose, inviteCode, inviteErr }) { const copyInviteToClipboard = () => { window.electronAPI.copyToClipboard(inviteCode).then(() => { onClose() @@ -20,7 +20,12 @@ function CreateInviteModal({ isOpen, onClose, inviteCode }) { return ( - {inviteCode ? ( + {inviteErr ? ( + <> +
Got an error when creating the invite:
+
{inviteErr.toString()}
+ + ) : inviteCode ? ( <> This is an invite code that you can send to a friend to collaborate on this drawing with you. diff --git a/src/JoinModal.js b/src/JoinModal.js index 467a081..a340884 100644 --- a/src/JoinModal.js +++ b/src/JoinModal.js @@ -16,10 +16,10 @@ function JoinModal({ isOpen, onClose }) { return ( - Insert here the ppppp:// invite code you received from your friend. + Insert here the ppppp:// or http(s):// invite code you received from your friend.