Merge pull request #52 from FloChehab/feat/info_config
feat: config as file, UI tweaks and refacto
Thanks! Really create stuff! 🥳
This commit is contained in:
commit
3e9c3184f7
@ -1,6 +1,6 @@
|
||||
# This workflow will do a clean install of node dependencies and check the code style
|
||||
|
||||
name: Linting code CI
|
||||
name: Linting and testing code CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -19,3 +19,4 @@ jobs:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run style
|
||||
- run: npm run test
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/config.run.yml
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM node:11 as base
|
||||
FROM node:12 as base
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /opt/app
|
||||
@ -19,7 +19,7 @@ RUN npm run build
|
||||
# Final image
|
||||
#####################
|
||||
|
||||
FROM node:11-alpine
|
||||
FROM node:12-alpine
|
||||
ENV NODE_ENV=prod
|
||||
|
||||
MAINTAINER cracker0dks
|
||||
@ -28,7 +28,7 @@ MAINTAINER cracker0dks
|
||||
RUN mkdir -p /opt/app
|
||||
WORKDIR /opt/app
|
||||
|
||||
COPY ./package.json ./package-lock.json ./
|
||||
COPY ./package.json ./package-lock.json config.default.yml ./
|
||||
RUN npm ci --only=prod
|
||||
|
||||
COPY scripts ./scripts
|
||||
|
45
README.md
45
README.md
@ -8,6 +8,10 @@ This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easi
|
||||
|
||||
[HERE](https://cloud13.de/testwhiteboard/) (Reset every night)
|
||||
|
||||
## Updating
|
||||
|
||||
Information related to updating this app can be found [here](./doc/updating_guide.md).
|
||||
|
||||
## Some Features
|
||||
|
||||
- Shows remote user cursors while drawing
|
||||
@ -34,7 +38,7 @@ You can run this app with and without docker
|
||||
|
||||
### Without Docker
|
||||
|
||||
1. install the latest NodeJs
|
||||
1. install the latest NodeJs (version >= 12)
|
||||
2. Clone the app
|
||||
3. Run `npm ci` inside the folder
|
||||
4. Run `npm run start:prod`
|
||||
@ -108,13 +112,26 @@ Call your site with GET parameters to change the WhiteboardID or the Username
|
||||
- title => Change the name of the Browser Tab
|
||||
- randomid => if set to true, a random whiteboardId will be generated if not given aswell
|
||||
|
||||
## Security - AccessToken (Optional)
|
||||
## Configuration
|
||||
|
||||
To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start.
|
||||
Many settings of this project can be set using a simple `yaml` file, to change some behaviors or tweak performances.
|
||||
|
||||
<b>Server (Without docker):</b> `node scripts/server.js --accesstoken="mySecToken"`
|
||||
### Config. file
|
||||
|
||||
<b>Server (With docker):</b> `docker run -d -p 8080:8080 rofl256/whiteboard --accesstoken="mySecToken"`
|
||||
To run the project with custom settings:
|
||||
|
||||
1. Create a `config.run.yml` file based on the content of [`config.default.yml`](./config.default.yml),
|
||||
2. Change the settings,
|
||||
3. Run the project with your custom configuration (it will be merged into the default one):
|
||||
|
||||
- locally: `node scripts/server.js --config=./config.run.yml`
|
||||
- docker: `docker run -d -p 8080:8080 -v $(pwd)/config.run.yml:/config.run.yml:ro rofl256/whiteboard --config=/config.run.yml`
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Security - AccessToken (Optional)
|
||||
|
||||
To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start (see [here](./config.default.yml)).
|
||||
|
||||
Then set the same token on the client side as well:
|
||||
|
||||
@ -122,15 +139,11 @@ Then set the same token on the client side as well:
|
||||
|
||||
Done!
|
||||
|
||||
## WebDAV (Optional)
|
||||
#### WebDAV (Optional)
|
||||
|
||||
This function allows your users to save the whiteboard directly to a webdav server (Nextcloud) as image without downloading it.
|
||||
|
||||
To enable it:
|
||||
|
||||
<b>Server (Without docker):</b> `node scripts/server.js --webdav=true`
|
||||
|
||||
<b>Server (With docker):</b> `docker run -d -p 8080:8080 rofl256/whiteboard --webdav=true`
|
||||
To enable set `enableWebdav` to `true` in the [configuration](./config.default.yml).
|
||||
|
||||
Then set the same parameter on the client side as well:
|
||||
|
||||
@ -142,17 +155,15 @@ Note: For the most owncloud/nextcloud setups you have to set the WebDav-Server U
|
||||
|
||||
Done!
|
||||
|
||||
### And many more (performance, etc.)
|
||||
|
||||
Many more settings can be tweaked. All of them are described in the [default config file](./config.default.yml).
|
||||
|
||||
## Things you may want to know
|
||||
|
||||
- Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard)
|
||||
- You should be able to customize the layout without ever touching the whiteboard.js (take a look at index.html & main.js)
|
||||
|
||||
## All server start parameters (also docker)
|
||||
|
||||
- accesstoken => take a look at "Security - AccessToken" for a full explanation
|
||||
- disablesmallestscreen => set this to "true" if you don't want show the "smallest screen" indicator (A dotted gray line) to the users
|
||||
- webdav => Enable the function to save to a webdav-server (Must also be enabled on the client; Take a look at the webdav section)
|
||||
|
||||
## ToDo
|
||||
|
||||
- Make undo function more reliable on texts
|
||||
|
45
config.default.yml
Normal file
45
config.default.yml
Normal file
@ -0,0 +1,45 @@
|
||||
# Backend configuration
|
||||
backend:
|
||||
# Access token required for interacting with the server -- string (empty string for no restrictions)
|
||||
accessToken: ""
|
||||
|
||||
# Enable the function to save to a webdav-server (check README for more info) -- boolean
|
||||
enableWebdav: false
|
||||
|
||||
# Backend performance tweaks
|
||||
performance:
|
||||
# Whiteboard information broadcasting frequency (in Hz i.e. /s) -- number
|
||||
# => diminishing this will result in more latency
|
||||
whiteboardInfoBroadcastFreq: 1
|
||||
|
||||
# Frontend configuration
|
||||
frontend:
|
||||
# When a whiteboard is loaded on a client
|
||||
onWhiteboardLoad:
|
||||
# should an (editable) whiteboard be started in read-only mode by default -- boolean
|
||||
setReadOnly: false
|
||||
|
||||
# should the whiteboard info be displayed by default -- boolean
|
||||
displayInfo: false
|
||||
|
||||
# Show the smallest screen indicator ? (with dotted lines) -- boolean
|
||||
showSmallestScreenIndicator: true
|
||||
|
||||
# Frontend performance tweaks
|
||||
performance:
|
||||
# Refresh frequency of the debug / info div (in Hz i.e. /s) -- number
|
||||
refreshInfoFreq: 5
|
||||
|
||||
# Throttling of pointer events (except drawing related) -- array of object (one must have fromUserCount == 0)
|
||||
# Throttling of events can be defined for different user count levels
|
||||
# Throttling consist of skipping certain events (i.e. not broadcasting them to others)
|
||||
pointerEventsThrottling:
|
||||
- # User count from which the specific throttling is applied -- number
|
||||
fromUserCount: 0
|
||||
# Min screen distance (in pixels) below which throttling is applied
|
||||
minDistDelta: 1
|
||||
# Maximum frequency above which throttling is applied
|
||||
maxFreq: 30
|
||||
- fromUserCount: 10
|
||||
minDistDelta: 5
|
||||
maxFreq: 10
|
13
doc/updating_guide.md
Normal file
13
doc/updating_guide.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Updating guide
|
||||
|
||||
## From v1.x to 2.x (or latest)
|
||||
|
||||
Configuration handling has been updated: the ability to change settings from the CLI or the environment has been removed.
|
||||
|
||||
**Configuration is now handled with a yml config file**, which can be overridden with the `--config` CLI argument.
|
||||
|
||||
Here is the mapping from old cli argument / env variables to the new config file object:
|
||||
|
||||
- accesstoken => `backend.accessToken`
|
||||
- webdav => `backend.enableWebdav`
|
||||
- disablesmallestscreen => `frontend.showSmallestScreenIndicator`
|
@ -5,5 +5,4 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080/tcp"
|
||||
environment:
|
||||
- ACCESSTOKEN=mysecrettoken
|
||||
command: --config=./config.default.yml
|
||||
|
4033
package-lock.json
generated
4033
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
"build": "webpack --config config/webpack.build.js",
|
||||
"start:dev": "node scripts/server.js --mode=development",
|
||||
"start:prod": "npm run build && node scripts/server.js --mode=production",
|
||||
"test": "echo \"No tests needed!\" && exit 1",
|
||||
"test": "jest",
|
||||
"pretty-quick": "pretty-quick",
|
||||
"format": "prettier --write .",
|
||||
"style": "prettier --check ."
|
||||
@ -28,10 +28,12 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "6.12.2",
|
||||
"dompurify": "^2.0.7",
|
||||
"express": "4.*",
|
||||
"formidable": "1.*",
|
||||
"fs-extra": "7.*",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsdom": "^14.0.0",
|
||||
"pdfjs-dist": "^2.3.200",
|
||||
"socket.io": "2.*",
|
||||
@ -54,6 +56,7 @@
|
||||
"css-loader": "^3.5.2",
|
||||
"html-webpack-plugin": "^4.2.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "26.0.1",
|
||||
"jquery": "^3.2.1",
|
||||
"jquery-ui": "^1.12.1",
|
||||
"keymage": "^1.1.3",
|
||||
|
99
scripts/WhiteboardServerSideInfo.js
Normal file
99
scripts/WhiteboardServerSideInfo.js
Normal file
@ -0,0 +1,99 @@
|
||||
const config = require("./config/config");
|
||||
|
||||
/**
|
||||
* Class to hold information related to a whiteboard
|
||||
*/
|
||||
class WhiteboardServerSideInfo {
|
||||
static defaultScreenResolution = { w: 1000, h: 1000 };
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._nbConnectedUsers = 0;
|
||||
|
||||
/**
|
||||
* @type {Map<int, {w: number, h: number}>}
|
||||
* @private
|
||||
*/
|
||||
this._screenResolutionByClients = new Map();
|
||||
|
||||
/**
|
||||
* Variable to tell if these info have been sent or not
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._hasNonSentUpdates = false;
|
||||
}
|
||||
|
||||
incrementNbConnectedUsers() {
|
||||
this._nbConnectedUsers++;
|
||||
this._hasNonSentUpdates = true;
|
||||
}
|
||||
|
||||
decrementNbConnectedUsers() {
|
||||
this._nbConnectedUsers--;
|
||||
this._hasNonSentUpdates = true;
|
||||
}
|
||||
|
||||
hasConnectedUser() {
|
||||
return this._nbConnectedUsers > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store information about the client's screen resolution
|
||||
*
|
||||
* @param {number} clientId
|
||||
* @param {number} w client's width
|
||||
* @param {number} h client's hight
|
||||
*/
|
||||
setScreenResolutionForClient(clientId, { w, h }) {
|
||||
this._screenResolutionByClients.set(clientId, { w, h });
|
||||
this._hasNonSentUpdates = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the stored information about the client's screen resoltion
|
||||
* @param clientId
|
||||
*/
|
||||
deleteScreenResolutionOfClient(clientId) {
|
||||
this._screenResolutionByClients.delete(clientId);
|
||||
this._hasNonSentUpdates = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the smallest client's screen size on a whiteboard
|
||||
* @return {{w: number, h: number}}
|
||||
*/
|
||||
getSmallestScreenResolution() {
|
||||
const { _screenResolutionByClients: resolutions } = this;
|
||||
return {
|
||||
w: Math.min(...Array.from(resolutions.values()).map((res) => res.w)),
|
||||
h: Math.min(...Array.from(resolutions.values()).map((res) => res.h)),
|
||||
};
|
||||
}
|
||||
|
||||
infoWasSent() {
|
||||
this._hasNonSentUpdates = false;
|
||||
}
|
||||
|
||||
shouldSendInfo() {
|
||||
return this._hasNonSentUpdates;
|
||||
}
|
||||
|
||||
asObject() {
|
||||
const out = {
|
||||
nbConnectedUsers: this._nbConnectedUsers,
|
||||
};
|
||||
|
||||
if (config.frontend.showSmallestScreenIndicator) {
|
||||
out.smallestScreenResolution = this.getSmallestScreenResolution();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WhiteboardServerSideInfo;
|
90
scripts/config/config-schema.json
Normal file
90
scripts/config/config-schema.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Whiteboard config",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backend": {
|
||||
"type": "object",
|
||||
"required": ["accessToken", "performance", "enableWebdav"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"enableWebdav": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"performance": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": ["whiteboardInfoBroadcastFreq"],
|
||||
"properties": {
|
||||
"whiteboardInfoBroadcastFreq": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["onWhiteboardLoad", "showSmallestScreenIndicator", "performance"],
|
||||
"properties": {
|
||||
"onWhiteboardLoad": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["displayInfo", "setReadOnly"],
|
||||
"properties": {
|
||||
"setReadOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayInfo": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"showSmallestScreenIndicator": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"performance": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["pointerEventsThrottling", "refreshInfoFreq"],
|
||||
"properties": {
|
||||
"pointerEventsThrottling": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["fromUserCount", "minDistDelta", "maxFreq"],
|
||||
"properties": {
|
||||
"fromUserCount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"minDistDelta": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"maxFreq": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refreshInfoFreq": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["backend", "frontend"],
|
||||
"additionalProperties": false
|
||||
}
|
80
scripts/config/config.js
Normal file
80
scripts/config/config.js
Normal file
@ -0,0 +1,80 @@
|
||||
const util = require("util");
|
||||
|
||||
const { getDefaultConfig, getConfig, deepMergeConfigs, isConfigValid } = require("./utils");
|
||||
|
||||
const { getArgs } = require("./../utils");
|
||||
|
||||
const defaultConfig = getDefaultConfig();
|
||||
|
||||
const cliArgs = getArgs();
|
||||
let userConfig = {};
|
||||
|
||||
if (cliArgs["config"]) {
|
||||
userConfig = getConfig(cliArgs["config"]);
|
||||
}
|
||||
|
||||
const config = deepMergeConfigs(defaultConfig, userConfig);
|
||||
|
||||
/**
|
||||
* Update the config based on the CLI args
|
||||
* @param {object} startArgs
|
||||
*/
|
||||
function updateConfigFromStartArgs(startArgs) {
|
||||
function deprecateCliArg(key, callback) {
|
||||
const val = startArgs[key];
|
||||
if (val) {
|
||||
console.warn(
|
||||
"\x1b[33m\x1b[1m",
|
||||
`Setting config values (${key}) from the CLI is deprecated. ` +
|
||||
"This ability will be removed in the next major version. " +
|
||||
"You should use the config file. "
|
||||
);
|
||||
callback(val);
|
||||
}
|
||||
}
|
||||
|
||||
deprecateCliArg("accesstoken", (val) => (config.backend.accessToken = val));
|
||||
deprecateCliArg(
|
||||
"disablesmallestscreen",
|
||||
() => (config.backend.showSmallestScreenIndicator = false)
|
||||
);
|
||||
deprecateCliArg("webdav", () => (config.backend.enableWebdav = true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the config based on the env variables
|
||||
*/
|
||||
function updateConfigFromEnv() {
|
||||
function deprecateEnv(key, callback) {
|
||||
const val = process.env[key];
|
||||
if (val) {
|
||||
console.warn(
|
||||
"\x1b[33m\x1b[1m",
|
||||
`Setting config values (${key}) from the environment is deprecated. ` +
|
||||
"This ability will be removed in the next major version. " +
|
||||
"You should use the config file. "
|
||||
);
|
||||
callback(val);
|
||||
}
|
||||
}
|
||||
|
||||
deprecateEnv("accesstoken", (val) => (config.backend.accessToken = val));
|
||||
deprecateEnv(
|
||||
"disablesmallestscreen",
|
||||
() => (config.backend.showSmallestScreenIndicator = false)
|
||||
);
|
||||
deprecateEnv("webdav", () => (config.backend.enableWebdav = true));
|
||||
}
|
||||
|
||||
// compatibility layer
|
||||
// FIXME: remove this in next major
|
||||
updateConfigFromEnv();
|
||||
// FIXME: remove this in next major
|
||||
updateConfigFromStartArgs(cliArgs);
|
||||
|
||||
if (!isConfigValid(config, true)) {
|
||||
throw new Error("Config is not valid. Check logs for details");
|
||||
}
|
||||
console.info(util.inspect(config, { showHidden: false, depth: null, colors: true }));
|
||||
|
||||
module.exports = config;
|
92
scripts/config/utils.js
Normal file
92
scripts/config/utils.js
Normal file
@ -0,0 +1,92 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const Ajv = require("ajv");
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
|
||||
const configSchema = require("./config-schema.json");
|
||||
|
||||
/**
|
||||
* Load a yaml config file from a given path.
|
||||
*
|
||||
* @param path
|
||||
* @return {Object}
|
||||
*/
|
||||
function getConfig(path) {
|
||||
return yaml.safeLoad(fs.readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a config object is valid.
|
||||
*
|
||||
* @param {Object} config Config object
|
||||
* @param {boolean} warn Should we warn in console for errors
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isConfigValid(config, warn = true) {
|
||||
const validate = ajv.compile(configSchema);
|
||||
const isValidAgainstSchema = validate(config);
|
||||
|
||||
if (!isValidAgainstSchema && warn) console.warn(validate.errors);
|
||||
|
||||
let structureIsValid = false;
|
||||
try {
|
||||
structureIsValid = config.frontend.performance.pointerEventsThrottling.some(
|
||||
(item) => item.fromUserCount === 0
|
||||
);
|
||||
} catch (e) {
|
||||
if (!e instanceof TypeError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!structureIsValid && warn)
|
||||
console.warn(
|
||||
"At least one item under frontend.performance.pointerEventsThrottling" +
|
||||
"must have fromUserCount set to 0"
|
||||
);
|
||||
|
||||
return isValidAgainstSchema && structureIsValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the default project config
|
||||
* @return {Object}
|
||||
*/
|
||||
function getDefaultConfig() {
|
||||
const defaultConfigPath = path.join(__dirname, "..", "..", "config.default.yml");
|
||||
return getConfig(defaultConfigPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge of project config
|
||||
*
|
||||
* Objects are merged, not arrays
|
||||
*
|
||||
* @param baseConfig
|
||||
* @param overrideConfig
|
||||
* @return {Object}
|
||||
*/
|
||||
function deepMergeConfigs(baseConfig, overrideConfig) {
|
||||
const out = {};
|
||||
|
||||
Object.entries(baseConfig).forEach(([key, val]) => {
|
||||
out[key] = val;
|
||||
if (overrideConfig.hasOwnProperty(key)) {
|
||||
const overrideVal = overrideConfig[key];
|
||||
if (typeof val === "object" && !Array.isArray(val) && val !== null) {
|
||||
out[key] = deepMergeConfigs(val, overrideVal);
|
||||
} else {
|
||||
out[key] = overrideVal;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports.getConfig = getConfig;
|
||||
module.exports.getDefaultConfig = getDefaultConfig;
|
||||
module.exports.deepMergeConfigs = deepMergeConfigs;
|
||||
module.exports.isConfigValid = isConfigValid;
|
48
scripts/config/utils.test.js
Normal file
48
scripts/config/utils.test.js
Normal file
@ -0,0 +1,48 @@
|
||||
const { getDefaultConfig, deepMergeConfigs, isConfigValid } = require("./utils");
|
||||
|
||||
test("Load default config", () => {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
expect(typeof defaultConfig).toBe("object");
|
||||
});
|
||||
|
||||
test("Full config override", () => {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
expect(deepMergeConfigs(defaultConfig, defaultConfig)).toEqual(defaultConfig);
|
||||
});
|
||||
|
||||
test("Simple partial config override", () => {
|
||||
expect(deepMergeConfigs({ test: true }, { test: false }).test).toBe(false);
|
||||
expect(deepMergeConfigs({ test: false }, { test: true }).test).toBe(true);
|
||||
});
|
||||
|
||||
test("Simple deep config override", () => {
|
||||
expect(deepMergeConfigs({ stage1: { stage2: true } }, { stage1: { stage2: false } })).toEqual({
|
||||
stage1: { stage2: false },
|
||||
});
|
||||
});
|
||||
|
||||
test("Complex object config override", () => {
|
||||
expect(
|
||||
deepMergeConfigs({ stage1: { stage2: true, stage2b: true } }, { stage1: { stage2: false } })
|
||||
).toEqual({
|
||||
stage1: { stage2: false, stage2b: true },
|
||||
});
|
||||
});
|
||||
|
||||
test("Override default config", () => {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
const overrideConfig1 = { frontend: { onWhiteboardLoad: { setReadOnly: true } } };
|
||||
|
||||
expect(
|
||||
deepMergeConfigs(defaultConfig, overrideConfig1).frontend.onWhiteboardLoad.setReadOnly
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Dumb config is not valid", () => {
|
||||
expect(isConfigValid({}, false)).toBe(false);
|
||||
});
|
||||
|
||||
test("Default config is valid", () => {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
expect(isConfigValid(defaultConfig)).toBe(true);
|
||||
});
|
@ -1,11 +1,9 @@
|
||||
const { getArgs } = require("./utils");
|
||||
const path = require("path");
|
||||
|
||||
function startBackendServer(port) {
|
||||
var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN)
|
||||
var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true)
|
||||
var webdav = false; //Can be set to true if you want to allow webdav save (node server.js --webdav=true)
|
||||
const config = require("./config/config");
|
||||
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
|
||||
|
||||
function startBackendServer(port) {
|
||||
var fs = require("fs-extra");
|
||||
var express = require("express");
|
||||
var formidable = require("formidable"); //form upload processing
|
||||
@ -26,36 +24,8 @@ function startBackendServer(port) {
|
||||
server.listen(port);
|
||||
var io = require("socket.io")(server, { path: "/ws-api" });
|
||||
console.log("Webserver & socketserver running on port:" + port);
|
||||
if (process.env.accesstoken) {
|
||||
accessToken = process.env.accesstoken;
|
||||
}
|
||||
if (process.env.disablesmallestscreen) {
|
||||
disablesmallestscreen = true;
|
||||
}
|
||||
if (process.env.webdav) {
|
||||
webdav = true;
|
||||
}
|
||||
|
||||
var startArgs = getArgs();
|
||||
if (startArgs["accesstoken"]) {
|
||||
accessToken = startArgs["accesstoken"];
|
||||
}
|
||||
if (startArgs["disablesmallestscreen"]) {
|
||||
disableSmallestScreen = true;
|
||||
}
|
||||
if (startArgs["webdav"]) {
|
||||
webdav = true;
|
||||
}
|
||||
|
||||
if (accessToken !== "") {
|
||||
console.log("AccessToken set to: " + accessToken);
|
||||
}
|
||||
if (disableSmallestScreen) {
|
||||
console.log("Disabled showing smallest screen resolution!");
|
||||
}
|
||||
if (webdav) {
|
||||
console.log("Webdav save is enabled!");
|
||||
}
|
||||
const { accessToken, enableWebdav } = config.backend;
|
||||
|
||||
app.get("/api/loadwhiteboard", function (req, res) {
|
||||
var wid = req["query"]["wid"];
|
||||
@ -147,7 +117,7 @@ function startBackendServer(port) {
|
||||
} else {
|
||||
if (webdavaccess) {
|
||||
//Save image to webdav
|
||||
if (webdav) {
|
||||
if (enableWebdav) {
|
||||
saveImageToWebdav(
|
||||
"./public/uploads/" + filename,
|
||||
filename,
|
||||
@ -204,21 +174,41 @@ function startBackendServer(port) {
|
||||
}
|
||||
}
|
||||
|
||||
var smallestScreenResolutions = {};
|
||||
/**
|
||||
* @type {Map<string, WhiteboardServerSideInfo>}
|
||||
*/
|
||||
const infoByWhiteboard = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
infoByWhiteboard.forEach((info, whiteboardId) => {
|
||||
if (info.shouldSendInfo()) {
|
||||
io.sockets
|
||||
.in(whiteboardId)
|
||||
.compress(false)
|
||||
.emit("whiteboardInfoUpdate", info.asObject());
|
||||
info.infoWasSent();
|
||||
}
|
||||
});
|
||||
}, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000);
|
||||
|
||||
io.on("connection", function (socket) {
|
||||
var whiteboardId = null;
|
||||
|
||||
socket.on("disconnect", function () {
|
||||
if (
|
||||
smallestScreenResolutions &&
|
||||
smallestScreenResolutions[whiteboardId] &&
|
||||
socket &&
|
||||
socket.id
|
||||
) {
|
||||
delete smallestScreenResolutions[whiteboardId][socket.id];
|
||||
if (infoByWhiteboard.has(whiteboardId)) {
|
||||
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||
|
||||
if (socket && socket.id) {
|
||||
whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id);
|
||||
}
|
||||
|
||||
whiteboardServerSideInfo.decrementNbConnectedUsers();
|
||||
|
||||
if (whiteboardServerSideInfo.hasConnectedUser()) {
|
||||
socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges
|
||||
} else {
|
||||
infoByWhiteboard.delete(whiteboardId);
|
||||
}
|
||||
}
|
||||
socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges
|
||||
sendSmallestScreenResolution();
|
||||
});
|
||||
|
||||
socket.on("drawToWhiteboard", function (content) {
|
||||
@ -234,15 +224,20 @@ function startBackendServer(port) {
|
||||
socket.on("joinWhiteboard", function (content) {
|
||||
content = escapeAllContentStrings(content);
|
||||
if (accessToken === "" || accessToken == content["at"]) {
|
||||
socket.emit("whiteboardConfig", { common: config.frontend });
|
||||
|
||||
whiteboardId = content["wid"];
|
||||
socket.join(whiteboardId); //Joins room name=wid
|
||||
smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId]
|
||||
? smallestScreenResolutions[whiteboardId]
|
||||
: {};
|
||||
smallestScreenResolutions[whiteboardId][socket.id] = content[
|
||||
"windowWidthHeight"
|
||||
] || { w: 10000, h: 10000 };
|
||||
sendSmallestScreenResolution();
|
||||
if (!infoByWhiteboard.has(whiteboardId)) {
|
||||
infoByWhiteboard.set(whiteboardId, new WhiteboardServerSideInfo());
|
||||
}
|
||||
|
||||
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||
whiteboardServerSideInfo.incrementNbConnectedUsers();
|
||||
whiteboardServerSideInfo.setScreenResolutionForClient(
|
||||
socket.id,
|
||||
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
|
||||
);
|
||||
} else {
|
||||
socket.emit("wrongAccessToken", true);
|
||||
}
|
||||
@ -250,38 +245,14 @@ function startBackendServer(port) {
|
||||
|
||||
socket.on("updateScreenResolution", function (content) {
|
||||
content = escapeAllContentStrings(content);
|
||||
if (
|
||||
smallestScreenResolutions[whiteboardId] &&
|
||||
(accessToken === "" || accessToken == content["at"])
|
||||
) {
|
||||
smallestScreenResolutions[whiteboardId][socket.id] = content[
|
||||
"windowWidthHeight"
|
||||
] || { w: 10000, h: 10000 };
|
||||
sendSmallestScreenResolution();
|
||||
if (accessToken === "" || accessToken == content["at"]) {
|
||||
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||
whiteboardServerSideInfo.setScreenResolutionForClient(
|
||||
socket.id,
|
||||
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function sendSmallestScreenResolution() {
|
||||
if (disableSmallestScreen) {
|
||||
return;
|
||||
}
|
||||
var smallestWidth = 10000;
|
||||
var smallestHeight = 10000;
|
||||
for (var i in smallestScreenResolutions[whiteboardId]) {
|
||||
smallestWidth =
|
||||
smallestWidth > smallestScreenResolutions[whiteboardId][i]["w"]
|
||||
? smallestScreenResolutions[whiteboardId][i]["w"]
|
||||
: smallestWidth;
|
||||
smallestHeight =
|
||||
smallestHeight > smallestScreenResolutions[whiteboardId][i]["h"]
|
||||
? smallestScreenResolutions[whiteboardId][i]["h"]
|
||||
: smallestHeight;
|
||||
}
|
||||
io.to(whiteboardId).emit("updateSmallestScreenResolution", {
|
||||
w: smallestWidth,
|
||||
h: smallestHeight,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//Prevent cross site scripting (xss)
|
||||
|
@ -1,3 +1,7 @@
|
||||
:root {
|
||||
--selected-icon-bg-color: #dfdfdf;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
@ -78,7 +82,7 @@ button {
|
||||
}
|
||||
|
||||
.whiteboard-tool.active:not(:disabled) {
|
||||
background: #dfdfdf;
|
||||
background: var(--selected-icon-bg-color);
|
||||
}
|
||||
|
||||
#whiteboardThicknessSlider {
|
||||
@ -114,3 +118,17 @@ button {
|
||||
min-width: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#displayWhiteboardInfoBtn.active {
|
||||
background: var(--selected-icon-bg-color);
|
||||
}
|
||||
|
||||
#whiteboardInfoContainer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.displayNone {
|
||||
display: none;
|
||||
}
|
||||
|
@ -224,6 +224,10 @@
|
||||
<button id="shareWhiteboardBtn" title="share whiteboard" type="button">
|
||||
<i class="fas fa-share-square"></i>
|
||||
</button>
|
||||
|
||||
<button id="displayWhiteboardInfoBtn" title="Show whiteboard info" type="button">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group minGroup">
|
||||
@ -247,7 +251,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 10px; right: 10px; display: none;">
|
||||
<div id="whiteboardInfoContainer">
|
||||
<p><b>Whiteboard information:</b></p>
|
||||
<p># connected users: <i id="connectedUsersCount">0</i></p>
|
||||
<p>Smallest screen resolution: <i id="smallestScreenResolution">Unknown.</i></p>
|
||||
<p># msg. sent to server: <i id="messageSentCount">0</i></p>
|
||||
<p># msg. received from server: <i id="messageReceivedCount">0</i></p>
|
||||
</div>
|
||||
|
@ -1,34 +1,41 @@
|
||||
import { computeDist } from "../utils";
|
||||
|
||||
class Point {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#x;
|
||||
get x() {
|
||||
return this.#x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#y;
|
||||
get y() {
|
||||
return this.#y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
static #lastKnownPos = new Point(0, 0);
|
||||
static get lastKnownPos() {
|
||||
return Point.#lastKnownPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
constructor(x, y) {
|
||||
/**
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._x = x;
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._y = y;
|
||||
}
|
||||
|
||||
get x() {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
get y() {
|
||||
return this._y;
|
||||
this.#x = x;
|
||||
this.#y = y;
|
||||
}
|
||||
|
||||
get isZeroZero() {
|
||||
return this._x === 0 && this._y === 0;
|
||||
return this.#x === 0 && this.#y === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,20 +57,14 @@ class Point {
|
||||
y = touch.clientY - $("#mouseOverlay").offset().top;
|
||||
} else {
|
||||
// if it's a touchend event
|
||||
return Point._lastKnownPos;
|
||||
return Point.#lastKnownPos;
|
||||
}
|
||||
}
|
||||
|
||||
Point._lastKnownPos = new Point(x - epsilon, y - epsilon);
|
||||
return Point._lastKnownPos;
|
||||
Point.#lastKnownPos = new Point(x - epsilon, y - epsilon);
|
||||
return Point.#lastKnownPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
* @private
|
||||
*/
|
||||
static _lastKnownPos = new Point(0, 0);
|
||||
|
||||
/**
|
||||
* Compute euclidean distance between points
|
||||
*
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA = 1; // 1px
|
||||
export const POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA = 10; // 1ms
|
@ -18,6 +18,7 @@ import {
|
||||
faExpandArrowsAlt,
|
||||
faLock,
|
||||
faLockOpen,
|
||||
faInfoCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faSquare,
|
||||
@ -50,7 +51,8 @@ library.add(
|
||||
faFileAlt,
|
||||
faPlusSquare,
|
||||
faLock,
|
||||
faLockOpen
|
||||
faLockOpen,
|
||||
faInfoCircle
|
||||
);
|
||||
|
||||
dom.i2svg();
|
||||
|
113
src/js/main.js
113
src/js/main.js
@ -7,57 +7,64 @@ import { dom } from "@fortawesome/fontawesome-svg-core";
|
||||
import pdfjsLib from "pdfjs-dist/webpack";
|
||||
import shortcutFunctions from "./shortcutFunctions";
|
||||
import ReadOnlyService from "./services/ReadOnlyService";
|
||||
import InfoService from "./services/InfoService";
|
||||
import { getQueryVariable, getSubDir } from "./utils";
|
||||
import ConfigService from "./services/ConfigService";
|
||||
|
||||
let whiteboardId = getQueryVariable("whiteboardid");
|
||||
const randomid = getQueryVariable("randomid");
|
||||
if (randomid && !whiteboardId) {
|
||||
//set random whiteboard on empty whiteboardid
|
||||
whiteboardId = Array(2)
|
||||
.fill(null)
|
||||
.map(() => Math.random().toString(36).substr(2))
|
||||
.join("");
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set("whiteboardid", whiteboardId);
|
||||
window.location.search = urlParams;
|
||||
}
|
||||
|
||||
whiteboardId = whiteboardId || "myNewWhiteboard";
|
||||
whiteboardId = unescape(encodeURIComponent(whiteboardId)).replace(/[^a-zA-Z0-9 ]/g, "");
|
||||
const myUsername = getQueryVariable("username") || "unknown" + (Math.random() + "").substring(2, 6);
|
||||
const accessToken = getQueryVariable("accesstoken") || "";
|
||||
|
||||
// Custom Html Title
|
||||
const title = getQueryVariable("title");
|
||||
if (!title === false) {
|
||||
document.title = decodeURIComponent(title);
|
||||
}
|
||||
|
||||
const subdir = getSubDir();
|
||||
let signaling_socket;
|
||||
|
||||
function main() {
|
||||
var whiteboardId = getQueryVariable("whiteboardid");
|
||||
var randomid = getQueryVariable("randomid");
|
||||
if (randomid && !whiteboardId) {
|
||||
//set random whiteboard on empty whiteboardid
|
||||
whiteboardId = Array(2)
|
||||
.fill(null)
|
||||
.map(() => Math.random().toString(36).substr(2))
|
||||
.join("");
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set("whiteboardid", whiteboardId);
|
||||
window.location.search = urlParams;
|
||||
}
|
||||
|
||||
whiteboardId = whiteboardId || "myNewWhiteboard";
|
||||
whiteboardId = unescape(encodeURIComponent(whiteboardId)).replace(/[^a-zA-Z0-9 ]/g, "");
|
||||
var myUsername = getQueryVariable("username");
|
||||
var accessToken = getQueryVariable("accesstoken");
|
||||
myUsername = myUsername || "unknown" + (Math.random() + "").substring(2, 6);
|
||||
accessToken = accessToken || "";
|
||||
var accessDenied = false;
|
||||
|
||||
// Custom Html Title
|
||||
var title = getQueryVariable("title");
|
||||
if (!title === false) {
|
||||
document.title = decodeURIComponent(title);
|
||||
}
|
||||
|
||||
var url = document.URL.substr(0, document.URL.lastIndexOf("/"));
|
||||
var signaling_socket = null;
|
||||
var urlSplit = url.split("/");
|
||||
var subdir = "";
|
||||
for (var i = 3; i < urlSplit.length; i++) {
|
||||
subdir = subdir + "/" + urlSplit[i];
|
||||
}
|
||||
signaling_socket = io("", { path: subdir + "/ws-api" }); // Connect even if we are in a subdir behind a reverse proxy
|
||||
|
||||
signaling_socket.on("connect", function () {
|
||||
console.log("Websocket connected!");
|
||||
|
||||
let messageReceivedCount = 0;
|
||||
signaling_socket.on("whiteboardConfig", (serverResponse) => {
|
||||
ConfigService.initFromServer(serverResponse);
|
||||
// Inti whiteboard only when we have the config from the server
|
||||
initWhiteboard();
|
||||
});
|
||||
|
||||
signaling_socket.on("whiteboardInfoUpdate", (info) => {
|
||||
InfoService.updateInfoFromServer(info);
|
||||
whiteboard.updateSmallestScreenResolution();
|
||||
});
|
||||
|
||||
signaling_socket.on("drawToWhiteboard", function (content) {
|
||||
whiteboard.handleEventsAndData(content, true);
|
||||
$("#messageReceivedCount")[0].innerText = String(messageReceivedCount++);
|
||||
InfoService.incrementNbMessagesReceived();
|
||||
});
|
||||
|
||||
signaling_socket.on("refreshUserBadges", function () {
|
||||
whiteboard.refreshUserBadges();
|
||||
});
|
||||
|
||||
let accessDenied = false;
|
||||
signaling_socket.on("wrongAccessToken", function () {
|
||||
if (!accessDenied) {
|
||||
accessDenied = true;
|
||||
@ -65,17 +72,15 @@ function main() {
|
||||
}
|
||||
});
|
||||
|
||||
signaling_socket.on("updateSmallestScreenResolution", function (widthHeight) {
|
||||
whiteboard.updateSmallestScreenResolution(widthHeight["w"], widthHeight["h"]);
|
||||
});
|
||||
|
||||
signaling_socket.emit("joinWhiteboard", {
|
||||
wid: whiteboardId,
|
||||
at: accessToken,
|
||||
windowWidthHeight: { w: $(window).width(), h: $(window).height() },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initWhiteboard() {
|
||||
$(document).ready(function () {
|
||||
// by default set in readOnly mode
|
||||
ReadOnlyService.activateReadOnlyMode();
|
||||
@ -84,7 +89,6 @@ function main() {
|
||||
$("#uploadWebDavBtn").show();
|
||||
}
|
||||
|
||||
let messageSentCount = 0;
|
||||
whiteboard.loadWhiteboard("#whiteboardContainer", {
|
||||
//Load the whiteboard
|
||||
whiteboardId: whiteboardId,
|
||||
@ -97,7 +101,7 @@ function main() {
|
||||
// }
|
||||
content["at"] = accessToken;
|
||||
signaling_socket.emit("drawToWhiteboard", content);
|
||||
$("#messageSentCount")[0].innerText = String(messageSentCount++);
|
||||
InfoService.incrementNbMessagesSent();
|
||||
},
|
||||
});
|
||||
|
||||
@ -382,6 +386,10 @@ function main() {
|
||||
showBasicAlert("Copied Whiteboard-URL to clipboard.", { hideAfter: 2 });
|
||||
});
|
||||
|
||||
$("#displayWhiteboardInfoBtn").click(() => {
|
||||
InfoService.toggleDisplayInfo();
|
||||
});
|
||||
|
||||
var btnsMini = false;
|
||||
$("#minMaxBtn").click(function () {
|
||||
if (!btnsMini) {
|
||||
@ -596,9 +604,15 @@ function main() {
|
||||
whiteboard.refreshCursorAppearance();
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
ReadOnlyService.activateReadOnlyMode();
|
||||
if (ConfigService.readOnlyOnWhiteboardLoad) ReadOnlyService.activateReadOnlyMode();
|
||||
else ReadOnlyService.deactivateReadOnlyMode();
|
||||
|
||||
if (ConfigService.displayInfoOnWhiteboardLoad) InfoService.displayInfo();
|
||||
else InfoService.hideInfo();
|
||||
} else {
|
||||
// in dev
|
||||
ReadOnlyService.deactivateReadOnlyMode();
|
||||
InfoService.displayInfo();
|
||||
}
|
||||
});
|
||||
|
||||
@ -789,19 +803,6 @@ function main() {
|
||||
}, 1000 * options.hideAfter);
|
||||
}
|
||||
}
|
||||
|
||||
// get 'GET' parameter by variable name
|
||||
function getQueryVariable(variable) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] == variable) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default main;
|
||||
|
84
src/js/services/ConfigService.js
Normal file
84
src/js/services/ConfigService.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { getThrottling } from "./ConfigService.utils";
|
||||
|
||||
/**
|
||||
* Class to hold the configuration sent by the backend
|
||||
*/
|
||||
class ConfigService {
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
#configFromServer = {};
|
||||
get configFromServer() {
|
||||
return this.#configFromServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{displayInfo: boolean, setReadOnly: boolean}}
|
||||
* @readonly
|
||||
*/
|
||||
#onWhiteboardLoad = { setReadOnly: false, displayInfo: false };
|
||||
get readOnlyOnWhiteboardLoad() {
|
||||
return this.#onWhiteboardLoad.setReadOnly;
|
||||
}
|
||||
get displayInfoOnWhiteboardLoad() {
|
||||
return this.#onWhiteboardLoad.displayInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
#showSmallestScreenIndicator = true;
|
||||
get showSmallestScreenIndicator() {
|
||||
return this.#showSmallestScreenIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{minDistDelta: number, minTimeDelta: number}}
|
||||
*/
|
||||
#pointerEventsThrottling = { minDistDelta: 0, minTimeDelta: 0 };
|
||||
get pointerEventsThrottling() {
|
||||
return this.#pointerEventsThrottling;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#refreshInfoInterval = 1000;
|
||||
get refreshInfoInterval() {
|
||||
return this.#refreshInfoInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the service from the config sent by the server
|
||||
*
|
||||
* @param {object} configFromServer
|
||||
*/
|
||||
initFromServer(configFromServer) {
|
||||
this.#configFromServer = configFromServer;
|
||||
|
||||
const { common } = configFromServer;
|
||||
const { onWhiteboardLoad, showSmallestScreenIndicator, performance } = common;
|
||||
|
||||
this.#onWhiteboardLoad = onWhiteboardLoad;
|
||||
this.#showSmallestScreenIndicator = showSmallestScreenIndicator;
|
||||
this.#refreshInfoInterval = 1000 / performance.refreshInfoFreq;
|
||||
|
||||
console.log("Whiteboard config from server:", configFromServer, "parsed:", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh config that depends on the number of user connected to whiteboard
|
||||
*
|
||||
* @param {number} userCount
|
||||
*/
|
||||
refreshUserCountDependant(userCount) {
|
||||
const { configFromServer } = this;
|
||||
const { common } = configFromServer;
|
||||
const { performance } = common;
|
||||
const { pointerEventsThrottling } = performance;
|
||||
|
||||
this.#pointerEventsThrottling = getThrottling(pointerEventsThrottling, userCount);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConfigService();
|
21
src/js/services/ConfigService.utils.js
Normal file
21
src/js/services/ConfigService.utils.js
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Helper to extract the correct throttling values based on the config and the number of user
|
||||
*
|
||||
* @param {Array.<{fromUserCount: number, minDistDelta: number, maxFreq: number}>} pointerEventsThrottling
|
||||
* @param {number} userCount
|
||||
* @return {{minDistDelta: number, minTimeDelta: number}}
|
||||
*/
|
||||
export function getThrottling(pointerEventsThrottling, userCount) {
|
||||
let tmpOut = pointerEventsThrottling[0];
|
||||
let lastDistToUserCount = userCount - tmpOut.fromUserCount;
|
||||
if (lastDistToUserCount < 0) lastDistToUserCount = Number.MAX_VALUE;
|
||||
for (const el of pointerEventsThrottling) {
|
||||
const distToUserCount = userCount - el.fromUserCount;
|
||||
if (el.fromUserCount <= userCount && distToUserCount <= lastDistToUserCount) {
|
||||
tmpOut = el;
|
||||
lastDistToUserCount = distToUserCount;
|
||||
}
|
||||
}
|
||||
|
||||
return { minDistDelta: tmpOut.minDistDelta, minTimeDelta: 1000 * (1 / tmpOut.maxFreq) };
|
||||
}
|
29
src/js/services/ConfigService.utils.test.js
Normal file
29
src/js/services/ConfigService.utils.test.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { getThrottling } from "./ConfigService.utils";
|
||||
|
||||
test("Simple throttling config", () => {
|
||||
const throttling = [{ fromUserCount: 0, minDistDelta: 1, maxFreq: 1 }];
|
||||
|
||||
const target0 = { minDistDelta: 1, minTimeDelta: 1000 };
|
||||
expect(getThrottling(throttling, 0)).toEqual(target0);
|
||||
|
||||
const target100 = { minDistDelta: 1, minTimeDelta: 1000 };
|
||||
expect(getThrottling(throttling, 100)).toEqual(target100);
|
||||
});
|
||||
|
||||
test("Complex throttling config", () => {
|
||||
// mix ordering
|
||||
const throttling = [
|
||||
{ fromUserCount: 100, minDistDelta: 100, maxFreq: 1 },
|
||||
{ fromUserCount: 0, minDistDelta: 1, maxFreq: 1 },
|
||||
{ fromUserCount: 50, minDistDelta: 50, maxFreq: 1 },
|
||||
];
|
||||
|
||||
const target0 = { minDistDelta: 1, minTimeDelta: 1000 };
|
||||
expect(getThrottling(throttling, 0)).toEqual(target0);
|
||||
|
||||
const target50 = { minDistDelta: 50, minTimeDelta: 1000 };
|
||||
expect(getThrottling(throttling, 50)).toEqual(target50);
|
||||
|
||||
const target100 = { minDistDelta: 100, minTimeDelta: 1000 };
|
||||
expect(getThrottling(throttling, 100)).toEqual(target100);
|
||||
});
|
137
src/js/services/InfoService.js
Normal file
137
src/js/services/InfoService.js
Normal file
@ -0,0 +1,137 @@
|
||||
import ConfigService from "./ConfigService";
|
||||
|
||||
/**
|
||||
* Class the handle the information about the whiteboard
|
||||
*/
|
||||
class InfoService {
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
#infoAreDisplayed = false;
|
||||
get infoAreDisplayed() {
|
||||
return this.#infoAreDisplayed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the number of user connected to the server
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
#nbConnectedUsers = -1;
|
||||
get nbConnectedUsers() {
|
||||
return this.#nbConnectedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{w: number, h: number}}
|
||||
*/
|
||||
#smallestScreenResolution = undefined;
|
||||
get smallestScreenResolution() {
|
||||
return this.#smallestScreenResolution;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#nbMessagesSent = 0;
|
||||
get nbMessagesSent() {
|
||||
return this.#nbMessagesSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#nbMessagesReceived = 0;
|
||||
get nbMessagesReceived() {
|
||||
return this.#nbMessagesReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the interval Id
|
||||
* @type {number}
|
||||
*/
|
||||
#refreshInfoIntervalId = undefined;
|
||||
get refreshInfoIntervalId() {
|
||||
return this.#refreshInfoIntervalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} nbConnectedUsers
|
||||
* @param {{w: number, h: number}} smallestScreenResolution
|
||||
*/
|
||||
updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) {
|
||||
if (this.#nbConnectedUsers !== nbConnectedUsers) {
|
||||
// Refresh config service parameters on nb connected user change
|
||||
ConfigService.refreshUserCountDependant(nbConnectedUsers);
|
||||
}
|
||||
this.#nbConnectedUsers = nbConnectedUsers;
|
||||
if (smallestScreenResolution) {
|
||||
this.#smallestScreenResolution = smallestScreenResolution;
|
||||
}
|
||||
}
|
||||
|
||||
incrementNbMessagesReceived() {
|
||||
this.#nbMessagesReceived++;
|
||||
}
|
||||
|
||||
incrementNbMessagesSent() {
|
||||
this.#nbMessagesSent++;
|
||||
}
|
||||
|
||||
refreshDisplayedInfo() {
|
||||
const {
|
||||
nbMessagesReceived,
|
||||
nbMessagesSent,
|
||||
nbConnectedUsers,
|
||||
smallestScreenResolution: ssr,
|
||||
} = this;
|
||||
$("#messageReceivedCount")[0].innerText = String(nbMessagesReceived);
|
||||
$("#messageSentCount")[0].innerText = String(nbMessagesSent);
|
||||
$("#connectedUsersCount")[0].innerText = String(nbConnectedUsers);
|
||||
$("#smallestScreenResolution")[0].innerText = ssr ? `(${ssr.w}, ${ssr.h})` : "Unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the info div
|
||||
*/
|
||||
displayInfo() {
|
||||
$("#whiteboardInfoContainer").toggleClass("displayNone", false);
|
||||
$("#displayWhiteboardInfoBtn").toggleClass("active", true);
|
||||
this.#infoAreDisplayed = true;
|
||||
|
||||
this.refreshDisplayedInfo();
|
||||
this.#refreshInfoIntervalId = setInterval(() => {
|
||||
// refresh only on a specific interval to reduce
|
||||
// refreshing cost
|
||||
this.refreshDisplayedInfo();
|
||||
}, ConfigService.refreshInfoInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the info div
|
||||
*/
|
||||
hideInfo() {
|
||||
$("#whiteboardInfoContainer").toggleClass("displayNone", true);
|
||||
$("#displayWhiteboardInfoBtn").toggleClass("active", false);
|
||||
this.#infoAreDisplayed = false;
|
||||
const { refreshInfoIntervalId } = this;
|
||||
if (refreshInfoIntervalId) {
|
||||
clearInterval(refreshInfoIntervalId);
|
||||
this.#refreshInfoIntervalId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between hiding and showing the info div
|
||||
*/
|
||||
toggleDisplayInfo() {
|
||||
const { infoAreDisplayed } = this;
|
||||
if (infoAreDisplayed) {
|
||||
this.hideInfo();
|
||||
} else {
|
||||
this.displayInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new InfoService();
|
@ -4,23 +4,27 @@
|
||||
class ReadOnlyService {
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
_readOnlyActive = true;
|
||||
#readOnlyActive = true;
|
||||
get readOnlyActive() {
|
||||
return this.#readOnlyActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
_previousToolHtmlElem = null;
|
||||
#previousToolHtmlElem = null;
|
||||
get previousToolHtmlElem() {
|
||||
return this.#previousToolHtmlElem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate read-only mode
|
||||
*/
|
||||
activateReadOnlyMode() {
|
||||
this._readOnlyActive = true;
|
||||
this.#readOnlyActive = true;
|
||||
|
||||
this._previousToolHtmlElem = $(".whiteboard-tool.active");
|
||||
this.#previousToolHtmlElem = $(".whiteboard-tool.active");
|
||||
|
||||
// switch to mouse tool to prevent the use of the
|
||||
// other tools
|
||||
@ -36,7 +40,7 @@ class ReadOnlyService {
|
||||
* Deactivate read-only mode
|
||||
*/
|
||||
deactivateReadOnlyMode() {
|
||||
this._readOnlyActive = false;
|
||||
this.#readOnlyActive = false;
|
||||
|
||||
$(".whiteboard-tool").prop("disabled", false);
|
||||
$(".whiteboard-edit-group > button").prop("disabled", false);
|
||||
@ -45,15 +49,8 @@ class ReadOnlyService {
|
||||
$("#whiteboardLockBtn").hide();
|
||||
|
||||
// restore previously selected tool
|
||||
if (this._previousToolHtmlElem) this._previousToolHtmlElem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the read-only status
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get readOnlyActive() {
|
||||
return this._readOnlyActive;
|
||||
const { previousToolHtmlElem } = this;
|
||||
if (previousToolHtmlElem) previousToolHtmlElem.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
48
src/js/services/ThrottlingService.js
Normal file
48
src/js/services/ThrottlingService.js
Normal file
@ -0,0 +1,48 @@
|
||||
import Point from "../classes/Point";
|
||||
import { getCurrentTimeMs } from "../utils";
|
||||
import ConfigService from "./ConfigService";
|
||||
|
||||
/**
|
||||
* Class to handle all the throttling logic
|
||||
*/
|
||||
class ThrottlingService {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#lastSuccessTime = 0;
|
||||
get lastSuccessTime() {
|
||||
return this.#lastSuccessTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
#lastPointPosition = new Point(0, 0);
|
||||
get lastPointPosition() {
|
||||
return this.#lastPointPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to throttle events based on the configuration.
|
||||
* Only if checks are ok, the onSuccess callback will be called.
|
||||
*
|
||||
* @param {Point} newPosition New point position to base the throttling on
|
||||
* @param {function()} onSuccess Callback called when the throttling is successful
|
||||
*/
|
||||
throttle(newPosition, onSuccess) {
|
||||
const newTime = getCurrentTimeMs();
|
||||
const { lastPointPosition, lastSuccessTime } = this;
|
||||
if (newTime - lastSuccessTime > ConfigService.pointerEventsThrottling.minTimeDelta) {
|
||||
if (
|
||||
lastPointPosition.distTo(newPosition) >
|
||||
ConfigService.pointerEventsThrottling.minDistDelta
|
||||
) {
|
||||
onSuccess();
|
||||
this.#lastPointPosition = newPosition;
|
||||
this.#lastSuccessTime = newTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ThrottlingService();
|
@ -14,3 +14,31 @@ export function computeDist(p1, p2) {
|
||||
export function getCurrentTimeMs() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* get 'GET' parameter by variable name
|
||||
* @param variable
|
||||
* @return {boolean|*}
|
||||
*/
|
||||
export function getQueryVariable(variable) {
|
||||
const query = window.location.search.substring(1);
|
||||
const vars = query.split("&");
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split("=");
|
||||
if (pair[0] === variable) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getSubDir() {
|
||||
const url = document.URL.substr(0, document.URL.lastIndexOf("/"));
|
||||
const urlSplit = url.split("/");
|
||||
let subdir = "";
|
||||
for (let i = 3; i < urlSplit.length; i++) {
|
||||
subdir = subdir + "/" + urlSplit[i];
|
||||
}
|
||||
|
||||
return subdir;
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { dom } from "@fortawesome/fontawesome-svg-core";
|
||||
import { getCurrentTimeMs } from "./utils";
|
||||
import Point from "./classes/Point";
|
||||
import {
|
||||
POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA,
|
||||
POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA,
|
||||
} from "./const";
|
||||
import ReadOnlyService from "./services/ReadOnlyService";
|
||||
import InfoService from "./services/InfoService";
|
||||
import ThrottlingService from "./services/ThrottlingService";
|
||||
import ConfigService from "./services/ConfigService";
|
||||
|
||||
const RAD_TO_DEG = 180.0 / Math.PI;
|
||||
const DEG_TO_RAD = Math.PI / 180.0;
|
||||
@ -211,25 +209,15 @@ const whiteboard = {
|
||||
|
||||
const currentPos = Point.fromEvent(e);
|
||||
|
||||
const pointerSentTime = getCurrentTimeMs();
|
||||
if (
|
||||
pointerSentTime - _this.lastPointerSentTime >
|
||||
POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA
|
||||
) {
|
||||
if (
|
||||
_this.lastPointerPosition.distTo(currentPos) >
|
||||
POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA
|
||||
) {
|
||||
_this.lastPointerSentTime = pointerSentTime;
|
||||
_this.lastPointerPosition = currentPos;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [currentPos.x, currentPos.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
ThrottlingService.throttle(currentPos, () => {
|
||||
_this.lastPointerPosition = currentPos;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [currentPos.x, currentPos.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
_this.mouseOverlay.on("mousemove touchmove", function (e) {
|
||||
@ -363,7 +351,7 @@ const whiteboard = {
|
||||
```<div class="dragMe" style="position:absolute; left: ${left}px; top: ${top}px; width: ${width}px; border: 2px dotted gray; overflow: hidden; height: ${height}px;" cursor:move;">
|
||||
<canvas style="cursor:move; position:absolute; top:0px; left:0px;" width="${width}" height="${height}"/>
|
||||
<div style="position:absolute; right:5px; top:3px;">
|
||||
<button draw="1" style="margin: 0px 0px; background: #03a9f4; padding: 5px; margin-top: 3px; color: white;" class="addToCanvasBtn btn btn-default">Drop</button>
|
||||
<button draw="1" style="margin: 0px 0px; background: #03a9f4; padding: 5px; margin-top: 3px; color: white;" class="addToCanvasBtn btn btn-default">Drop</button>
|
||||
<button style="margin: 0px 0px; background: #03a9f4; padding: 5px; margin-top: 3px; color: white;" class="xCanvasBtn btn btn-default">x</button>
|
||||
</div>
|
||||
</div>```
|
||||
@ -543,23 +531,15 @@ const whiteboard = {
|
||||
_this.prevPos = currentPos;
|
||||
});
|
||||
|
||||
const pointerSentTime = getCurrentTimeMs();
|
||||
if (pointerSentTime - _this.lastPointerSentTime > POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA) {
|
||||
const newPointerPosition = currentPos;
|
||||
if (
|
||||
_this.lastPointerPosition.distTo(newPointerPosition) >
|
||||
POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA
|
||||
) {
|
||||
_this.lastPointerSentTime = pointerSentTime;
|
||||
_this.lastPointerPosition = newPointerPosition;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [newPointerPosition.x, newPointerPosition.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
ThrottlingService.throttle(currentPos, () => {
|
||||
_this.lastPointerPosition = currentPos;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [currentPos.x, currentPos.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
});
|
||||
},
|
||||
triggerMouseOver: function () {
|
||||
var _this = this;
|
||||
@ -876,28 +856,17 @@ const whiteboard = {
|
||||
currX += textBox.width() - 4;
|
||||
}
|
||||
|
||||
const pointerSentTime = getCurrentTimeMs();
|
||||
const newPointerPosition = new Point(currX, currY);
|
||||
// At least 100 ms between messages to reduce server load
|
||||
if (
|
||||
pointerSentTime - _this.lastPointerSentTime >
|
||||
POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA
|
||||
) {
|
||||
// Minimal distance between messages to reduce server load
|
||||
if (
|
||||
_this.lastPointerPosition.distTo(newPointerPosition) >
|
||||
POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA
|
||||
) {
|
||||
_this.lastPointerSentTime = pointerSentTime;
|
||||
_this.lastPointerPosition = newPointerPosition;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [newPointerPosition.x, newPointerPosition.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ThrottlingService.throttle(newPointerPosition, () => {
|
||||
_this.lastPointerPosition = newPointerPosition;
|
||||
_this.sendFunction({
|
||||
t: "cursor",
|
||||
event: "move",
|
||||
d: [newPointerPosition.x, newPointerPosition.y],
|
||||
username: _this.settings.username,
|
||||
});
|
||||
});
|
||||
});
|
||||
this.textContainer.append(textBox);
|
||||
textBox.draggable({
|
||||
@ -1061,21 +1030,26 @@ const whiteboard = {
|
||||
_this.setTextboxFontColor(_this.latestActiveTextBoxId, color);
|
||||
}
|
||||
},
|
||||
updateSmallestScreenResolution(width, height) {
|
||||
this.backgroundGrid.empty();
|
||||
if (width < $(window).width() || height < $(window).height()) {
|
||||
this.backgroundGrid.append(
|
||||
'<div style="position:absolute; left:0px; top:0px; border-right:3px dotted black; border-bottom:3px dotted black; width:' +
|
||||
width +
|
||||
"px; height:" +
|
||||
height +
|
||||
'px;"></div>'
|
||||
);
|
||||
this.backgroundGrid.append(
|
||||
'<div style="position:absolute; left:' +
|
||||
(width + 5) +
|
||||
'px; top:0px;">smallest screen participating</div>'
|
||||
);
|
||||
updateSmallestScreenResolution() {
|
||||
const { smallestScreenResolution } = InfoService;
|
||||
const { showSmallestScreenIndicator } = ConfigService;
|
||||
if (showSmallestScreenIndicator && smallestScreenResolution) {
|
||||
const { w: width, h: height } = smallestScreenResolution;
|
||||
this.backgroundGrid.empty();
|
||||
if (width < $(window).width() || height < $(window).height()) {
|
||||
this.backgroundGrid.append(
|
||||
'<div style="position:absolute; left:0px; top:0px; border-right:3px dotted black; border-bottom:3px dotted black; width:' +
|
||||
width +
|
||||
"px; height:" +
|
||||
height +
|
||||
'px;"></div>'
|
||||
);
|
||||
this.backgroundGrid.append(
|
||||
'<div style="position:absolute; left:' +
|
||||
(width + 5) +
|
||||
'px; top:0px;">smallest screen participating</div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleEventsAndData: function (content, isNewData, doneCallback) {
|
||||
|
Loading…
Reference in New Issue
Block a user