diff --git a/package-lock.json b/package-lock.json
index f47a0eb..45310fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
+ "express-ws": "^4.0.0",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
"yargs": "^16.2.0"
@@ -86,6 +87,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
},
+ "node_modules/async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -629,6 +635,20 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-ws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz",
+ "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==",
+ "dependencies": {
+ "ws": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.5.0"
+ },
+ "peerDependencies": {
+ "express": "^4.0.0 || ^5.0.0-alpha.1"
+ }
+ },
"node_modules/express/node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1522,6 +1542,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "node_modules/ws": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
+ "dependencies": {
+ "async-limiter": "~1.0.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
@@ -1658,6 +1686,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
},
+ "async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2080,6 +2113,14 @@
}
}
},
+ "express-ws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz",
+ "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==",
+ "requires": {
+ "ws": "^5.2.0"
+ }
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -2760,6 +2801,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "ws": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ },
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
diff --git a/package.json b/package.json
index 7d07d18..ffc367f 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
+ "express-ws": "^4.0.0",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
"yargs": "^16.2.0"
diff --git a/server.js b/server.js
index 76380b4..6b393f4 100644
--- a/server.js
+++ b/server.js
@@ -86,12 +86,12 @@ const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE ke
// express config
const app = express();
-app.use(express.static('public'));
+const expressWs = require('express-ws')(app);
+app.use(express.static('public'));
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'}));
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
-
app.set('json spaces', 2);
app.use(cookieParser());
@@ -126,6 +126,20 @@ function decryptCookie(ciphertext){
return JSON.parse(deciphered);
}
+// broadcast isn't user specific right now...
+app.ws('/ws/:uid', (ws, req) => {
+ ws.on("message", (msg) => {
+ //console.log("received messsage: " + msg);
+ });
+ console.log("socket opened for user " + req.params.uid);
+});
+
+function broadcast(uid, msg){
+ for ( let socket of expressWs.getWss('/ws/' + uid).clients ){
+ socket.send(JSON.stringify(msg));
+ }
+}
+
// handle auth
app.use ( async (req, res, next) => {
@@ -283,7 +297,7 @@ const SERVER_ERROR = {status: "error", error: "server error"};
app.get("/api/whoami", (req, res) => {
- res.send({name: req.user.name, version: VERSION});
+ res.send({name: req.user.name, version: VERSION, id: req.user.id});
});
// list boards
@@ -335,7 +349,8 @@ app.post('/api/boards', (req, res) => {
board.titlePinId = 0;
res.send(board);
console.log(`Created board#${id} ${req.body.name}`);
-
+
+ broadcast(req.user.id, {b:id});
} catch (err){
console.log("Error creating board: " + err.message);
if ( err.message.includes('UNIQUE constraint failed:') ){
@@ -383,6 +398,7 @@ app.delete("/api/boards/:boardId", async (req, res) => {
if ( result.changes == 1 ){
res.send(OK);
+ broadcast(req.user.id, {deleteBoard: req.params.boardId});
} else {
res.status(404).send(NOT_FOUND);
}
@@ -411,6 +427,18 @@ app.get("/api/pins/:pinId", (req, res) => {
app.post("/api/pins", async (req, res) => {
try {
+ let boardId = req.body.boardId;
+
+ if ( boardId == "new" ){
+ try {
+ let result = db.prepare("INSERT INTO boards (name, userId, hidden, createDate) VALUES (@name, @userId, @hidden, @createDate)").run({name: req.body.newBoardName, userId: req.user.id, hidden: 0, createDate: new Date().toISOString()});
+ boardId = result.lastInsertRowid;
+ } catch (e){
+ console.log("error creating new board: ", err);
+ res.status(500).send(SERVER_ERROR);
+ }
+ }
+
// download the image first to make sure we can get it
let image = await downloadImage(req.body.imageUrl);
@@ -439,7 +467,7 @@ app.post("/api/pins", async (req, res) => {
@userId,
@createDate)
`).run({
- boardId: req.body.boardId,
+ boardId: boardId,
imageUrl: req.body.imageUrl,
siteUrl: req.body.siteUrl,
description: req.body.description,
@@ -460,12 +488,15 @@ app.post("/api/pins", async (req, res) => {
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
res.send(pin);
+ broadcast(req.user.id, {updateBoard:boardId});
+
} catch (err) {
console.log(`Error creating pin: ${err.message}`, err);
res.status(500).send(SERVER_ERROR);
}
});
+// create pin
app.post("/api/pins/:pinId", (req,res) => {
try {
@@ -487,6 +518,7 @@ app.post("/api/pins/:pinId", (req,res) => {
if ( result.changes == 1 ){
console.log(`updated pin#${req.params.pinId}`)
res.send(OK);
+ broadcast(req.user.id, {updateBoard:req.body.boardId});
} else {
res.status(404).send(NOT_FOUND);
}
@@ -497,9 +529,12 @@ app.post("/api/pins/:pinId", (req,res) => {
});
+// delete pin
app.delete("/api/pins/:pinId", async (req, res) => {
try {
+ let pin = db.prepare('SELECT * FROM pins WHERE userId = @userId and id = @pinId').get({userId: req.user.id, pinId:req.params.pinId});
+
let result = db.prepare('DELETE FROM pins WHERE userId = @userId and id = @pinId').run({userId: req.user.id, pinId:req.params.pinId});
if ( result.changes == 1 ){
@@ -512,6 +547,9 @@ app.delete("/api/pins/:pinId", async (req, res) => {
console.log(`deleted pin#${req.params.pinId}`);
res.send(OK);
+
+ broadcast(req.user.id, {updateBoard:pin.boardId});
+
} else {
res.status(404).send(NOT_FOUND);
}
@@ -592,6 +630,10 @@ app.post("/up/", async (req, res) => {
return;
});
+app.get("/otl/:l", (req, res) => {
+
+});
+
// start listening
diff --git a/static/app.js b/static/app.js
index 20a7ebc..ec2e2b3 100644
--- a/static/app.js
+++ b/static/app.js
@@ -25,13 +25,56 @@ app.addSetter("load.boards", async (data) => {
store.do("loader.hide");
});
-app.addSetter('load.board', async (data) => {
+// handle update events
+window.addEventListener("broadcast", async (e) => {
+
+ let data = store.data;
+
+ if ( e.detail.updateBoard ){
+ console.log("updating board");
+ let boardId = e.detail.updateBoard;
+
+ let boardExists = false;
+ for ( let i = 0; i < data.boards.length; ++i ){
+ if ( data.boards[i].id == boardId ){
+ boardExists = true;
+ }
+ }
+
+ // if it's a new board
+ if ( !boardExists ){
+ store.do("load.boards");
+ }
+
+ // if we are currently viewing this board, reload the pins
+ if ( data.board && boardId == data.board.id ){
+ store.do("load.board", true);
+ }
+ } else if ( e.detail.deleteBoard ) {
+ console.log("deleting board");
+ let boardId = e.detail.deleteBoard;
+
+ // reload the boards
+ store.do("load.boards");
+
+ // we're currently looking at this board... alert and error
+ if ( data.board && boardId == data.board.id ){
+ window.alert("this board has been deleted on another device");
+ window.location.hash = "#";
+ }
+ }
+
+});
+
+app.addSetter('load.board', async (data, force) => {
store.do("loader.show");
- if ( !data.board || data.board.id != data.hash.board ){
- let res = await fetch("/api/boards/" + data.hash.board);
- data.board = await res.json();
- }
+ if ( data.hash.board ){
+ if ( force || !data.board || data.board.id != data.hash.board ){
+ let res = await fetch("/api/boards/" + data.hash.board);
+ data.board = await res.json();
+ }
+ }
store.do("loader.hide");
});
@@ -42,6 +85,9 @@ app.addSetter('load.user', async (data) => {
let res = await fetch("/api/whoami");
data.user = await res.json();
+ window.uid = data.user.id;
+ window.socketConnect();
+
store.do("loader.hide");
});
@@ -225,4 +271,12 @@ store.do('load.user');
store.do('load.boards');
store.do('hash.update');
-appComponent.render();
\ No newline at end of file
+appComponent.render();
+
+
+// refresh on focus
+window.addEventListener("focus", () => {
+ store.do("load.boards");
+ store.do("load.board");
+ window.dispatchEvent(new CustomEvent("socket-connect"));
+});
\ No newline at end of file
diff --git a/static/client.css b/static/client.css
index 1552554..4fdb2ae 100644
--- a/static/client.css
+++ b/static/client.css
@@ -286,6 +286,21 @@
margin-top: -2px;
}
+/* https://thenounproject.com/search/?q=connected&i=88200 */
+#socketConnected {
+ background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMWExYTFhIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTkwLjMsNDguMUM5MC41LDYxLjksNzkuNCw3My41LDY1LjYsNzRjLTMuNywwLjEtNy4yLTAuNi0xMC40LTEuOWMtMS42LTAuNy0yLjMtMi42LTEuNS00LjJsMC0wLjFjMC43LTEuMywyLjMtMS45LDMuNi0xLjMgIGMzLDEuMyw2LjQsMS44LDEwLDEuM2M4LjktMS4yLDE1LjktOC41LDE2LjgtMTcuNGMxLjItMTIuMi05LTIyLjUtMjEuMy0yMS4zQzU0LDI5LjksNDYuNiwzNyw0NS41LDQ2Yy0wLjMsMi41LTAuMiw0LjksMC40LDcuMSAgYzAuMywxLjEtMC4xLDIuMi0wLjksMi45YzAsMCwwLDAsMCwwYy0xLjcsMS41LTQuNCwwLjctNC45LTEuNWMtMC44LTMuMy0xLTYuOS0wLjMtMTAuNmMyLTEwLjksMTEtMTkuNCwyMi0yMC43ICBDNzcsMjEuNCw5MC4xLDMzLjIsOTAuMyw0OC4xeiBNMzYuOSw3NC44YzExLTEuMywyMC05LjgsMjItMjAuN2MwLjctMy43LDAuNS03LjMtMC4zLTEwLjdjLTAuNS0yLjItMy4yLTMuMS00LjktMS41bDAsMCAgYy0wLjgsMC43LTEuMSwxLjgtMC45LDIuOWMwLjYsMi4zLDAuNyw0LjcsMC40LDcuMmMtMS4yLDguOS04LjUsMTYtMTcuNSwxNi45QzIzLjQsNzAsMTMuMiw1OS44LDE0LjQsNDcuNiAgYzAuOS04LjksNy45LTE2LjIsMTYuOC0xNy40YzMuNi0wLjUsNywwLjEsMTAsMS4zYzEuNCwwLjYsMi45LDAsMy42LTEuM2wwLTAuMWMwLjgtMS42LDAuMi0zLjUtMS41LTQuMmMtMy43LTEuNS03LjktMi4yLTEyLjMtMS43ICBDMTksMjUuNCw5LjQsMzUuMyw4LjQsNDcuNEM3LjEsNjMuNCwyMC43LDc2LjcsMzYuOSw3NC44eiI+PC9wYXRoPjwvc3ZnPg==");
+ background-size: 24px 24px;
+ background-repeat: no-repeat;
+ background-position: 6px 8px;
+ opacity: 0.1;
+ margin-left: 10px;
+}
+
+body.socketConnected #socketConnected {
+ opacity: 1;
+}
+
+
.modal-card {
max-width: 95%;
}
diff --git a/static/components/addpin.js b/static/components/addpin.js
index fd16e92..dd83ed9 100644
--- a/static/components/addpin.js
+++ b/static/components/addpin.js
@@ -48,26 +48,9 @@ app.addSetter('addPinModal.save', async (data) => {
let boardId = data.addPinModal.boardId;
- let newBoard = null;
-
- if ( boardId == "new" ){
- let res = await fetch('api/boards', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- "name": data.addPinModal.newBoardName
- })
- });
-
- if ( res.status == 200 ){
- newBoard = await res.json();
- boardId = newBoard.id;
- data.boards.push(newBoard);
- }
- }
-
let postData = {
boardId: boardId,
+ newBoardName: data.addPinModal.newBoardName,
imageUrl: data.addPinModal.imageUrl,
siteUrl: data.addPinModal.siteUrl,
description: data.addPinModal.description
@@ -80,20 +63,21 @@ app.addSetter('addPinModal.save', async (data) => {
},
body: JSON.stringify(postData)
});
-
+
if ( res.status == 200 ){
let body = await res.json();
if ( data.board && data.board.id == boardId ){
data.board.pins.push(body);
}
-
- if ( newBoard ){
- newBoard.titlePinId = body.id;
- }
window.localStorage.addPinLastBoardId = boardId;
store.do("addPinModal.close");
+
+ // if we don't have a listening socket, we need to trigger our own update
+ if ( boardId == "new" && !window.socketConnected ){
+ store.do("load.boards");
+ }
}
store.do("loader.hide");
diff --git a/static/components/navbar.js b/static/components/navbar.js
index d83df3f..63b8536 100644
--- a/static/components/navbar.js
+++ b/static/components/navbar.js
@@ -76,7 +76,9 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
${boardName}
+
+