Skip to content

Commit bd59ef6

Browse files
committed
Editor locks and colors
1 parent 76e44df commit bd59ef6

9 files changed

+137
-28
lines changed

client/CameraMain.js

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class CameraVideo extends React.Component {
6969
};
7070
});
7171
};
72+
73+
if (cv.allReadyForCamera) init();
74+
else cv.onRuntimeInitialized = init;
7275
}
7376

7477
_onMouseDown = mouseDownEvent => {
@@ -275,6 +278,8 @@ export default class CameraMain extends React.Component {
275278
currentCodeUrl: programWithData.currentCodeUrl,
276279
currentCodeHash: programWithData.currentCodeHash,
277280
debugUrl: programWithData.debugUrl,
281+
claimUrl: programWithData.claimUrl,
282+
editorInfo: programWithData.editorInfo,
278283
};
279284
})
280285
.filter(Boolean)

client/EditorMain.css

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
margin-bottom: paddingSize;
4343
}
4444

45+
:local(.editorColor) {
46+
display: inline-block;
47+
width: 16px;
48+
height: 16px;
49+
}
50+
4551
:local(.logline) {
4652
font: 12px monospace;
4753
}

client/EditorMain.js

+41-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import MonacoEditor from 'react-monaco-editor';
22
import React from 'react';
3+
import randomColor from 'randomcolor';
4+
import sortBy from 'lodash/sortBy';
35
import xhr from 'xhr';
46

57
import { codeToName, getApiUrl } from './utils';
@@ -55,11 +57,18 @@ export default class EditorMain extends React.Component {
5557

5658
const program = this._selectedProgram(this.state.selectedProgramNumber);
5759
if (program) {
58-
xhr.get(program.debugUrl, { json: true }, (error, response) => {
60+
const { editorId } = this.props.editorConfig;
61+
xhr.post(program.claimUrl, { json: { editorId } }, (error, response) => {
5962
if (error) {
6063
console.error(error); // eslint-disable-line no-console
64+
} else if (response.statusCode === 400) {
65+
this.setState({
66+
selectedProgramNumber: '',
67+
code: '',
68+
debugInfo: {},
69+
});
6170
} else {
62-
this.setState({ debugInfo: response.body });
71+
this.setState({ debugInfo: response.body.debugInfo });
6372
}
6473
done();
6574
});
@@ -128,6 +137,10 @@ export default class EditorMain extends React.Component {
128137
);
129138
};
130139

140+
_editorColor = () => {
141+
return randomColor({ seed: this.props.editorConfig.editorId });
142+
};
143+
131144
render() {
132145
const selectedProgram = this._selectedProgram(this.state.selectedProgramNumber);
133146
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
@@ -151,6 +164,11 @@ export default class EditorMain extends React.Component {
151164
</div>
152165
)}
153166
<div className={styles.sidebar}>
167+
<div className={styles.sidebarSection}>
168+
editor color{' '}
169+
<div className={styles.editorColor} style={{ background: this._editorColor() }} />
170+
</div>
171+
154172
<div className={styles.sidebarSection}>
155173
<select
156174
value={this.state.selectedProgramNumber}
@@ -170,12 +188,23 @@ export default class EditorMain extends React.Component {
170188
}}
171189
>
172190
<option value={''}>- select program -</option>
173-
{this.state.spaceData.programs.map(program => (
174-
<option key={program.number} value={program.number}>
175-
#{program.number} {codeToName(program.currentCode)}
176-
{program.printed ? '' : ' (queued to print)'}
177-
</option>
178-
))}
191+
{sortBy(this.state.spaceData.programs, 'number').map(program => {
192+
const beingEditedBySomeoneElse =
193+
program.editorInfo.claimed &&
194+
program.editorInfo.editorId !== this.props.editorConfig.editorId;
195+
196+
return (
197+
<option
198+
key={program.number}
199+
value={program.number}
200+
disabled={beingEditedBySomeoneElse}
201+
>
202+
#{program.number} {codeToName(program.currentCode)}
203+
{program.printed ? '' : ' (queued to print)'}
204+
{beingEditedBySomeoneElse ? ' (being edited)' : ''}
205+
</option>
206+
);
207+
})}
179208
</select>
180209
</div>
181210

@@ -191,8 +220,8 @@ export default class EditorMain extends React.Component {
191220
errors.length > 0 && (
192221
<div className={styles.sidebarSection}>
193222
errors:{' '}
194-
{errors.map(error => (
195-
<div className={styles.logline}>
223+
{errors.map((error, index) => (
224+
<div key={index} className={styles.logline}>
196225
<strong>
197226
error[{error.filename}:{error.lineNumber}:{error.columnNumber}]:
198227
</strong>{' '}
@@ -206,8 +235,8 @@ export default class EditorMain extends React.Component {
206235
logs.length > 0 && (
207236
<div className={styles.sidebarSection}>
208237
console:{' '}
209-
{logs.map(logLine => (
210-
<div className={styles.logline}>
238+
{logs.map((logLine, index) => (
239+
<div key={index} className={styles.logline}>
211240
<strong>
212241
{logLine.name}[program:{logLine.lineNumber}:{logLine.columnNumber}]:
213242
</strong>{' '}

client/ProjectorMain.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import randomColor from 'randomcolor';
23
import xhr from 'xhr';
34

45
import { forwardProjectionMatrixForPoints, mult } from './utils';
@@ -140,6 +141,9 @@ class Program extends React.Component {
140141
height: canvasHeight,
141142
transform: matrixToCssTransform(matrix),
142143
transformOrigin: '0 0 0',
144+
boxShadow: program.editorInfo.claimed
145+
? `0 0 0 1px ${randomColor({ seed: program.editorInfo.editorId })} inset`
146+
: '',
143147
}}
144148
/>
145149
)}

client/entry-editor.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
3+
import uuidv4 from 'uuid/v4';
34

45
import EditorMain from './EditorMain';
56

67
const element = document.createElement('div');
7-
88
document.body.appendChild(element);
99

10-
ReactDOM.render(<EditorMain spaceName={window.location.search.slice(1)} />, element);
10+
const defaultConfig = {
11+
editorId: uuidv4(),
12+
};
13+
14+
localStorage.dynazarEditorConfig = JSON.stringify({
15+
...defaultConfig,
16+
...JSON.parse(localStorage.dynazarEditorConfig || '{}'),
17+
});
18+
19+
ReactDOM.render(
20+
<EditorMain
21+
editorConfig={JSON.parse(localStorage.dynazarEditorConfig)}
22+
spaceName={window.location.search.slice(1)}
23+
/>,
24+
element
25+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
exports.up = function(knex) {
2+
return knex.schema.table('programs', function(table) {
3+
table
4+
.text('editorInfo')
5+
.notNullable()
6+
.defaultTo('');
7+
});
8+
};
9+
10+
exports.down = function(knex) {
11+
return knex.schema.table('programs', function(table) {
12+
table.dropColumn('editorInfo');
13+
});
14+
};

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"node-matrices": "^1.0.0",
4444
"pdfkit": "^0.8.3",
4545
"pg": "^7.4.0",
46+
"randomcolor": "^0.5.3",
4647
"react": "^16.2.0",
4748
"react-dom": "^16.2.0",
4849
"react-monaco-editor": "^0.13.0",

server/api.js

+44-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
const crypto = require('crypto');
21
const express = require('express');
2+
const crypto = require('crypto');
3+
4+
const editorHandleDuration = 1500;
35

46
const router = express.Router();
57
router.use(express.json());
@@ -22,19 +24,28 @@ router.get('/program.:spaceName.:number.js', (req, res) => {
2224
function getSpaceData(req, callback) {
2325
const { spaceName } = req.params;
2426
knex('programs')
25-
.select('number', 'originalCode', 'currentCode', 'printed')
27+
.select('number', 'originalCode', 'currentCode', 'printed', 'editorInfo')
2628
.where({ spaceName })
2729
.then(programData => {
2830
callback({
29-
programs: programData.map(program => ({
30-
...program,
31-
currentCodeUrl: `program.${spaceName}.${program.number}.js`,
32-
currentCodeHash: crypto
33-
.createHmac('sha256', '')
34-
.update(program.currentCode)
35-
.digest('hex'),
36-
debugUrl: `/api/spaces/${spaceName}/programs/${program.number}/debugInfo`,
37-
})),
31+
programs: programData.map(program => {
32+
const editorInfo = JSON.parse(program.editorInfo || '{}');
33+
34+
return {
35+
...program,
36+
currentCodeUrl: `program.${spaceName}.${program.number}.js`,
37+
currentCodeHash: crypto
38+
.createHmac('sha256', '')
39+
.update(program.currentCode)
40+
.digest('hex'),
41+
debugUrl: `/api/spaces/${spaceName}/programs/${program.number}/debugInfo`,
42+
claimUrl: `/api/spaces/${spaceName}/programs/${program.number}/claim`,
43+
editorInfo: {
44+
...editorInfo,
45+
claimed: !!(editorInfo.time && editorInfo.time + editorHandleDuration > Date.now()),
46+
},
47+
};
48+
}),
3849
spaceName,
3950
});
4051
});
@@ -112,16 +123,35 @@ router.put('/api/spaces/:spaceName/programs/:number/debugInfo', (req, res) => {
112123
});
113124
});
114125

115-
router.get('/api/spaces/:spaceName/programs/:number/debugInfo', (req, res) => {
126+
router.post('/api/spaces/:spaceName/programs/:number/claim', (req, res) => {
116127
const { spaceName, number } = req.params;
117128

118129
knex
119-
.select('debugInfo')
130+
.select(['debugInfo', 'editorInfo'])
120131
.from('programs')
121132
.where({ spaceName, number })
122133
.then(selectResult => {
123134
if (selectResult.length === 0) return res.status(404);
124-
res.json(JSON.parse(selectResult[0].debugInfo || '{}'));
135+
const editorInfo = JSON.parse(selectResult[0].editorInfo || '{}');
136+
if (
137+
editorInfo.time &&
138+
editorInfo.time + editorHandleDuration > Date.now() &&
139+
editorInfo.editorId !== req.body.editorId
140+
) {
141+
res.status(400);
142+
res.json({});
143+
return;
144+
} else {
145+
knex('programs')
146+
.update({ editorInfo: JSON.stringify({ ...req.body, time: Date.now() }) })
147+
.where({ spaceName, number })
148+
.then(() => {
149+
res.json({
150+
debugInfo: JSON.parse(selectResult[0].debugInfo || '{}'),
151+
editorInfo,
152+
});
153+
});
154+
}
125155
});
126156
});
127157

0 commit comments

Comments
 (0)