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
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -19,3 +19,4 @@ jobs:
|
|||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run style
|
- run: npm run style
|
||||||
|
- run: npm run test
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
/config.run.yml
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:11 as base
|
FROM node:12 as base
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
RUN mkdir -p /opt/app
|
RUN mkdir -p /opt/app
|
||||||
@ -19,7 +19,7 @@ RUN npm run build
|
|||||||
# Final image
|
# Final image
|
||||||
#####################
|
#####################
|
||||||
|
|
||||||
FROM node:11-alpine
|
FROM node:12-alpine
|
||||||
ENV NODE_ENV=prod
|
ENV NODE_ENV=prod
|
||||||
|
|
||||||
MAINTAINER cracker0dks
|
MAINTAINER cracker0dks
|
||||||
@ -28,7 +28,7 @@ MAINTAINER cracker0dks
|
|||||||
RUN mkdir -p /opt/app
|
RUN mkdir -p /opt/app
|
||||||
WORKDIR /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
|
RUN npm ci --only=prod
|
||||||
|
|
||||||
COPY scripts ./scripts
|
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)
|
[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
|
## Some Features
|
||||||
|
|
||||||
- Shows remote user cursors while drawing
|
- Shows remote user cursors while drawing
|
||||||
@ -34,7 +38,7 @@ You can run this app with and without docker
|
|||||||
|
|
||||||
### Without Docker
|
### Without Docker
|
||||||
|
|
||||||
1. install the latest NodeJs
|
1. install the latest NodeJs (version >= 12)
|
||||||
2. Clone the app
|
2. Clone the app
|
||||||
3. Run `npm ci` inside the folder
|
3. Run `npm ci` inside the folder
|
||||||
4. Run `npm run start:prod`
|
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
|
- title => Change the name of the Browser Tab
|
||||||
- randomid => if set to true, a random whiteboardId will be generated if not given aswell
|
- 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:
|
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!
|
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.
|
This function allows your users to save the whiteboard directly to a webdav server (Nextcloud) as image without downloading it.
|
||||||
|
|
||||||
To enable it:
|
To enable set `enableWebdav` to `true` in the [configuration](./config.default.yml).
|
||||||
|
|
||||||
<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`
|
|
||||||
|
|
||||||
Then set the same parameter on the client side as well:
|
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!
|
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
|
## Things you may want to know
|
||||||
|
|
||||||
- Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard)
|
- 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)
|
- 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
|
## ToDo
|
||||||
|
|
||||||
- Make undo function more reliable on texts
|
- 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
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080/tcp"
|
- "8080:8080/tcp"
|
||||||
environment:
|
command: --config=./config.default.yml
|
||||||
- ACCESSTOKEN=mysecrettoken
|
|
||||||
|
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",
|
"build": "webpack --config config/webpack.build.js",
|
||||||
"start:dev": "node scripts/server.js --mode=development",
|
"start:dev": "node scripts/server.js --mode=development",
|
||||||
"start:prod": "npm run build && node scripts/server.js --mode=production",
|
"start:prod": "npm run build && node scripts/server.js --mode=production",
|
||||||
"test": "echo \"No tests needed!\" && exit 1",
|
"test": "jest",
|
||||||
"pretty-quick": "pretty-quick",
|
"pretty-quick": "pretty-quick",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"style": "prettier --check ."
|
"style": "prettier --check ."
|
||||||
@ -28,10 +28,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "6.12.2",
|
||||||
"dompurify": "^2.0.7",
|
"dompurify": "^2.0.7",
|
||||||
"express": "4.*",
|
"express": "4.*",
|
||||||
"formidable": "1.*",
|
"formidable": "1.*",
|
||||||
"fs-extra": "7.*",
|
"fs-extra": "7.*",
|
||||||
|
"js-yaml": "3.13.1",
|
||||||
"jsdom": "^14.0.0",
|
"jsdom": "^14.0.0",
|
||||||
"pdfjs-dist": "^2.3.200",
|
"pdfjs-dist": "^2.3.200",
|
||||||
"socket.io": "2.*",
|
"socket.io": "2.*",
|
||||||
@ -54,6 +56,7 @@
|
|||||||
"css-loader": "^3.5.2",
|
"css-loader": "^3.5.2",
|
||||||
"html-webpack-plugin": "^4.2.0",
|
"html-webpack-plugin": "^4.2.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
|
"jest": "26.0.1",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"jquery-ui": "^1.12.1",
|
"jquery-ui": "^1.12.1",
|
||||||
"keymage": "^1.1.3",
|
"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");
|
const path = require("path");
|
||||||
|
|
||||||
function startBackendServer(port) {
|
const config = require("./config/config");
|
||||||
var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN)
|
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
|
||||||
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)
|
|
||||||
|
|
||||||
|
function startBackendServer(port) {
|
||||||
var fs = require("fs-extra");
|
var fs = require("fs-extra");
|
||||||
var express = require("express");
|
var express = require("express");
|
||||||
var formidable = require("formidable"); //form upload processing
|
var formidable = require("formidable"); //form upload processing
|
||||||
@ -26,36 +24,8 @@ function startBackendServer(port) {
|
|||||||
server.listen(port);
|
server.listen(port);
|
||||||
var io = require("socket.io")(server, { path: "/ws-api" });
|
var io = require("socket.io")(server, { path: "/ws-api" });
|
||||||
console.log("Webserver & socketserver running on port:" + port);
|
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();
|
const { accessToken, enableWebdav } = config.backend;
|
||||||
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!");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get("/api/loadwhiteboard", function (req, res) {
|
app.get("/api/loadwhiteboard", function (req, res) {
|
||||||
var wid = req["query"]["wid"];
|
var wid = req["query"]["wid"];
|
||||||
@ -147,7 +117,7 @@ function startBackendServer(port) {
|
|||||||
} else {
|
} else {
|
||||||
if (webdavaccess) {
|
if (webdavaccess) {
|
||||||
//Save image to webdav
|
//Save image to webdav
|
||||||
if (webdav) {
|
if (enableWebdav) {
|
||||||
saveImageToWebdav(
|
saveImageToWebdav(
|
||||||
"./public/uploads/" + filename,
|
"./public/uploads/" + filename,
|
||||||
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) {
|
io.on("connection", function (socket) {
|
||||||
var whiteboardId = null;
|
var whiteboardId = null;
|
||||||
|
|
||||||
socket.on("disconnect", function () {
|
socket.on("disconnect", function () {
|
||||||
if (
|
if (infoByWhiteboard.has(whiteboardId)) {
|
||||||
smallestScreenResolutions &&
|
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||||
smallestScreenResolutions[whiteboardId] &&
|
|
||||||
socket &&
|
if (socket && socket.id) {
|
||||||
socket.id
|
whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id);
|
||||||
) {
|
}
|
||||||
delete smallestScreenResolutions[whiteboardId][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) {
|
socket.on("drawToWhiteboard", function (content) {
|
||||||
@ -234,15 +224,20 @@ function startBackendServer(port) {
|
|||||||
socket.on("joinWhiteboard", function (content) {
|
socket.on("joinWhiteboard", function (content) {
|
||||||
content = escapeAllContentStrings(content);
|
content = escapeAllContentStrings(content);
|
||||||
if (accessToken === "" || accessToken == content["at"]) {
|
if (accessToken === "" || accessToken == content["at"]) {
|
||||||
|
socket.emit("whiteboardConfig", { common: config.frontend });
|
||||||
|
|
||||||
whiteboardId = content["wid"];
|
whiteboardId = content["wid"];
|
||||||
socket.join(whiteboardId); //Joins room name=wid
|
socket.join(whiteboardId); //Joins room name=wid
|
||||||
smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId]
|
if (!infoByWhiteboard.has(whiteboardId)) {
|
||||||
? smallestScreenResolutions[whiteboardId]
|
infoByWhiteboard.set(whiteboardId, new WhiteboardServerSideInfo());
|
||||||
: {};
|
}
|
||||||
smallestScreenResolutions[whiteboardId][socket.id] = content[
|
|
||||||
"windowWidthHeight"
|
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||||
] || { w: 10000, h: 10000 };
|
whiteboardServerSideInfo.incrementNbConnectedUsers();
|
||||||
sendSmallestScreenResolution();
|
whiteboardServerSideInfo.setScreenResolutionForClient(
|
||||||
|
socket.id,
|
||||||
|
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
socket.emit("wrongAccessToken", true);
|
socket.emit("wrongAccessToken", true);
|
||||||
}
|
}
|
||||||
@ -250,38 +245,14 @@ function startBackendServer(port) {
|
|||||||
|
|
||||||
socket.on("updateScreenResolution", function (content) {
|
socket.on("updateScreenResolution", function (content) {
|
||||||
content = escapeAllContentStrings(content);
|
content = escapeAllContentStrings(content);
|
||||||
if (
|
if (accessToken === "" || accessToken == content["at"]) {
|
||||||
smallestScreenResolutions[whiteboardId] &&
|
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||||
(accessToken === "" || accessToken == content["at"])
|
whiteboardServerSideInfo.setScreenResolutionForClient(
|
||||||
) {
|
socket.id,
|
||||||
smallestScreenResolutions[whiteboardId][socket.id] = content[
|
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
|
||||||
"windowWidthHeight"
|
);
|
||||||
] || { w: 10000, h: 10000 };
|
|
||||||
sendSmallestScreenResolution();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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)
|
//Prevent cross site scripting (xss)
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
:root {
|
||||||
|
--selected-icon-bg-color: #dfdfdf;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
@ -78,7 +82,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.whiteboard-tool.active:not(:disabled) {
|
.whiteboard-tool.active:not(:disabled) {
|
||||||
background: #dfdfdf;
|
background: var(--selected-icon-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#whiteboardThicknessSlider {
|
#whiteboardThicknessSlider {
|
||||||
@ -114,3 +118,17 @@ button {
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
cursor: pointer;
|
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">
|
<button id="shareWhiteboardBtn" title="share whiteboard" type="button">
|
||||||
<i class="fas fa-share-square"></i>
|
<i class="fas fa-share-square"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button id="displayWhiteboardInfoBtn" title="Show whiteboard info" type="button">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group minGroup">
|
<div class="btn-group minGroup">
|
||||||
@ -247,7 +251,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</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. sent to server: <i id="messageSentCount">0</i></p>
|
||||||
<p># msg. received from server: <i id="messageReceivedCount">0</i></p>
|
<p># msg. received from server: <i id="messageReceivedCount">0</i></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,34 +1,41 @@
|
|||||||
import { computeDist } from "../utils";
|
import { computeDist } from "../utils";
|
||||||
|
|
||||||
class Point {
|
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} x
|
||||||
* @param {number} y
|
* @param {number} y
|
||||||
*/
|
*/
|
||||||
constructor(x, y) {
|
constructor(x, y) {
|
||||||
/**
|
this.#x = x;
|
||||||
* @type {number}
|
this.#y = y;
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._x = x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
get x() {
|
|
||||||
return this._x;
|
|
||||||
}
|
|
||||||
|
|
||||||
get y() {
|
|
||||||
return this._y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isZeroZero() {
|
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;
|
y = touch.clientY - $("#mouseOverlay").offset().top;
|
||||||
} else {
|
} else {
|
||||||
// if it's a touchend event
|
// if it's a touchend event
|
||||||
return Point._lastKnownPos;
|
return Point.#lastKnownPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Point._lastKnownPos = new Point(x - epsilon, y - epsilon);
|
Point.#lastKnownPos = new Point(x - epsilon, y - epsilon);
|
||||||
return Point._lastKnownPos;
|
return Point.#lastKnownPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Point}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
static _lastKnownPos = new Point(0, 0);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute euclidean distance between points
|
* 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,
|
faExpandArrowsAlt,
|
||||||
faLock,
|
faLock,
|
||||||
faLockOpen,
|
faLockOpen,
|
||||||
|
faInfoCircle,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {
|
import {
|
||||||
faSquare,
|
faSquare,
|
||||||
@ -50,7 +51,8 @@ library.add(
|
|||||||
faFileAlt,
|
faFileAlt,
|
||||||
faPlusSquare,
|
faPlusSquare,
|
||||||
faLock,
|
faLock,
|
||||||
faLockOpen
|
faLockOpen,
|
||||||
|
faInfoCircle
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.i2svg();
|
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 pdfjsLib from "pdfjs-dist/webpack";
|
||||||
import shortcutFunctions from "./shortcutFunctions";
|
import shortcutFunctions from "./shortcutFunctions";
|
||||||
import ReadOnlyService from "./services/ReadOnlyService";
|
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() {
|
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 = io("", { path: subdir + "/ws-api" }); // Connect even if we are in a subdir behind a reverse proxy
|
||||||
|
|
||||||
signaling_socket.on("connect", function () {
|
signaling_socket.on("connect", function () {
|
||||||
console.log("Websocket connected!");
|
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) {
|
signaling_socket.on("drawToWhiteboard", function (content) {
|
||||||
whiteboard.handleEventsAndData(content, true);
|
whiteboard.handleEventsAndData(content, true);
|
||||||
$("#messageReceivedCount")[0].innerText = String(messageReceivedCount++);
|
InfoService.incrementNbMessagesReceived();
|
||||||
});
|
});
|
||||||
|
|
||||||
signaling_socket.on("refreshUserBadges", function () {
|
signaling_socket.on("refreshUserBadges", function () {
|
||||||
whiteboard.refreshUserBadges();
|
whiteboard.refreshUserBadges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let accessDenied = false;
|
||||||
signaling_socket.on("wrongAccessToken", function () {
|
signaling_socket.on("wrongAccessToken", function () {
|
||||||
if (!accessDenied) {
|
if (!accessDenied) {
|
||||||
accessDenied = true;
|
accessDenied = true;
|
||||||
@ -65,17 +72,15 @@ function main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
signaling_socket.on("updateSmallestScreenResolution", function (widthHeight) {
|
|
||||||
whiteboard.updateSmallestScreenResolution(widthHeight["w"], widthHeight["h"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
signaling_socket.emit("joinWhiteboard", {
|
signaling_socket.emit("joinWhiteboard", {
|
||||||
wid: whiteboardId,
|
wid: whiteboardId,
|
||||||
at: accessToken,
|
at: accessToken,
|
||||||
windowWidthHeight: { w: $(window).width(), h: $(window).height() },
|
windowWidthHeight: { w: $(window).width(), h: $(window).height() },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWhiteboard() {
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// by default set in readOnly mode
|
// by default set in readOnly mode
|
||||||
ReadOnlyService.activateReadOnlyMode();
|
ReadOnlyService.activateReadOnlyMode();
|
||||||
@ -84,7 +89,6 @@ function main() {
|
|||||||
$("#uploadWebDavBtn").show();
|
$("#uploadWebDavBtn").show();
|
||||||
}
|
}
|
||||||
|
|
||||||
let messageSentCount = 0;
|
|
||||||
whiteboard.loadWhiteboard("#whiteboardContainer", {
|
whiteboard.loadWhiteboard("#whiteboardContainer", {
|
||||||
//Load the whiteboard
|
//Load the whiteboard
|
||||||
whiteboardId: whiteboardId,
|
whiteboardId: whiteboardId,
|
||||||
@ -97,7 +101,7 @@ function main() {
|
|||||||
// }
|
// }
|
||||||
content["at"] = accessToken;
|
content["at"] = accessToken;
|
||||||
signaling_socket.emit("drawToWhiteboard", content);
|
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 });
|
showBasicAlert("Copied Whiteboard-URL to clipboard.", { hideAfter: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#displayWhiteboardInfoBtn").click(() => {
|
||||||
|
InfoService.toggleDisplayInfo();
|
||||||
|
});
|
||||||
|
|
||||||
var btnsMini = false;
|
var btnsMini = false;
|
||||||
$("#minMaxBtn").click(function () {
|
$("#minMaxBtn").click(function () {
|
||||||
if (!btnsMini) {
|
if (!btnsMini) {
|
||||||
@ -596,9 +604,15 @@ function main() {
|
|||||||
whiteboard.refreshCursorAppearance();
|
whiteboard.refreshCursorAppearance();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
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 {
|
} else {
|
||||||
|
// in dev
|
||||||
ReadOnlyService.deactivateReadOnlyMode();
|
ReadOnlyService.deactivateReadOnlyMode();
|
||||||
|
InfoService.displayInfo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -789,19 +803,6 @@ function main() {
|
|||||||
}, 1000 * options.hideAfter);
|
}, 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;
|
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 {
|
class ReadOnlyService {
|
||||||
/**
|
/**
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_readOnlyActive = true;
|
#readOnlyActive = true;
|
||||||
|
get readOnlyActive() {
|
||||||
|
return this.#readOnlyActive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {object}
|
* @type {object}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_previousToolHtmlElem = null;
|
#previousToolHtmlElem = null;
|
||||||
|
get previousToolHtmlElem() {
|
||||||
|
return this.#previousToolHtmlElem;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate read-only mode
|
* Activate read-only mode
|
||||||
*/
|
*/
|
||||||
activateReadOnlyMode() {
|
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
|
// switch to mouse tool to prevent the use of the
|
||||||
// other tools
|
// other tools
|
||||||
@ -36,7 +40,7 @@ class ReadOnlyService {
|
|||||||
* Deactivate read-only mode
|
* Deactivate read-only mode
|
||||||
*/
|
*/
|
||||||
deactivateReadOnlyMode() {
|
deactivateReadOnlyMode() {
|
||||||
this._readOnlyActive = false;
|
this.#readOnlyActive = false;
|
||||||
|
|
||||||
$(".whiteboard-tool").prop("disabled", false);
|
$(".whiteboard-tool").prop("disabled", false);
|
||||||
$(".whiteboard-edit-group > button").prop("disabled", false);
|
$(".whiteboard-edit-group > button").prop("disabled", false);
|
||||||
@ -45,15 +49,8 @@ class ReadOnlyService {
|
|||||||
$("#whiteboardLockBtn").hide();
|
$("#whiteboardLockBtn").hide();
|
||||||
|
|
||||||
// restore previously selected tool
|
// restore previously selected tool
|
||||||
if (this._previousToolHtmlElem) this._previousToolHtmlElem.click();
|
const { previousToolHtmlElem } = this;
|
||||||
}
|
if (previousToolHtmlElem) previousToolHtmlElem.click();
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the read-only status
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
get readOnlyActive() {
|
|
||||||
return this._readOnlyActive;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
export function getCurrentTimeMs() {
|
||||||
return new Date().getTime();
|
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 { dom } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { getCurrentTimeMs } from "./utils";
|
|
||||||
import Point from "./classes/Point";
|
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 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 RAD_TO_DEG = 180.0 / Math.PI;
|
||||||
const DEG_TO_RAD = Math.PI / 180.0;
|
const DEG_TO_RAD = Math.PI / 180.0;
|
||||||
@ -211,25 +209,15 @@ const whiteboard = {
|
|||||||
|
|
||||||
const currentPos = Point.fromEvent(e);
|
const currentPos = Point.fromEvent(e);
|
||||||
|
|
||||||
const pointerSentTime = getCurrentTimeMs();
|
ThrottlingService.throttle(currentPos, () => {
|
||||||
if (
|
_this.lastPointerPosition = currentPos;
|
||||||
pointerSentTime - _this.lastPointerSentTime >
|
_this.sendFunction({
|
||||||
POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA
|
t: "cursor",
|
||||||
) {
|
event: "move",
|
||||||
if (
|
d: [currentPos.x, currentPos.y],
|
||||||
_this.lastPointerPosition.distTo(currentPos) >
|
username: _this.settings.username,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
_this.mouseOverlay.on("mousemove touchmove", function (e) {
|
_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;">
|
```<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}"/>
|
<canvas style="cursor:move; position:absolute; top:0px; left:0px;" width="${width}" height="${height}"/>
|
||||||
<div style="position:absolute; right:5px; top:3px;">
|
<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>
|
<button style="margin: 0px 0px; background: #03a9f4; padding: 5px; margin-top: 3px; color: white;" class="xCanvasBtn btn btn-default">x</button>
|
||||||
</div>
|
</div>
|
||||||
</div>```
|
</div>```
|
||||||
@ -543,23 +531,15 @@ const whiteboard = {
|
|||||||
_this.prevPos = currentPos;
|
_this.prevPos = currentPos;
|
||||||
});
|
});
|
||||||
|
|
||||||
const pointerSentTime = getCurrentTimeMs();
|
ThrottlingService.throttle(currentPos, () => {
|
||||||
if (pointerSentTime - _this.lastPointerSentTime > POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA) {
|
_this.lastPointerPosition = currentPos;
|
||||||
const newPointerPosition = currentPos;
|
_this.sendFunction({
|
||||||
if (
|
t: "cursor",
|
||||||
_this.lastPointerPosition.distTo(newPointerPosition) >
|
event: "move",
|
||||||
POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA
|
d: [currentPos.x, currentPos.y],
|
||||||
) {
|
username: _this.settings.username,
|
||||||
_this.lastPointerSentTime = pointerSentTime;
|
});
|
||||||
_this.lastPointerPosition = newPointerPosition;
|
});
|
||||||
_this.sendFunction({
|
|
||||||
t: "cursor",
|
|
||||||
event: "move",
|
|
||||||
d: [newPointerPosition.x, newPointerPosition.y],
|
|
||||||
username: _this.settings.username,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
triggerMouseOver: function () {
|
triggerMouseOver: function () {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
@ -876,28 +856,17 @@ const whiteboard = {
|
|||||||
currX += textBox.width() - 4;
|
currX += textBox.width() - 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerSentTime = getCurrentTimeMs();
|
|
||||||
const newPointerPosition = new Point(currX, currY);
|
const newPointerPosition = new Point(currX, currY);
|
||||||
// At least 100 ms between messages to reduce server load
|
|
||||||
if (
|
ThrottlingService.throttle(newPointerPosition, () => {
|
||||||
pointerSentTime - _this.lastPointerSentTime >
|
_this.lastPointerPosition = newPointerPosition;
|
||||||
POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA
|
_this.sendFunction({
|
||||||
) {
|
t: "cursor",
|
||||||
// Minimal distance between messages to reduce server load
|
event: "move",
|
||||||
if (
|
d: [newPointerPosition.x, newPointerPosition.y],
|
||||||
_this.lastPointerPosition.distTo(newPointerPosition) >
|
username: _this.settings.username,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.textContainer.append(textBox);
|
this.textContainer.append(textBox);
|
||||||
textBox.draggable({
|
textBox.draggable({
|
||||||
@ -1061,21 +1030,26 @@ const whiteboard = {
|
|||||||
_this.setTextboxFontColor(_this.latestActiveTextBoxId, color);
|
_this.setTextboxFontColor(_this.latestActiveTextBoxId, color);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateSmallestScreenResolution(width, height) {
|
updateSmallestScreenResolution() {
|
||||||
this.backgroundGrid.empty();
|
const { smallestScreenResolution } = InfoService;
|
||||||
if (width < $(window).width() || height < $(window).height()) {
|
const { showSmallestScreenIndicator } = ConfigService;
|
||||||
this.backgroundGrid.append(
|
if (showSmallestScreenIndicator && smallestScreenResolution) {
|
||||||
'<div style="position:absolute; left:0px; top:0px; border-right:3px dotted black; border-bottom:3px dotted black; width:' +
|
const { w: width, h: height } = smallestScreenResolution;
|
||||||
width +
|
this.backgroundGrid.empty();
|
||||||
"px; height:" +
|
if (width < $(window).width() || height < $(window).height()) {
|
||||||
height +
|
this.backgroundGrid.append(
|
||||||
'px;"></div>'
|
'<div style="position:absolute; left:0px; top:0px; border-right:3px dotted black; border-bottom:3px dotted black; width:' +
|
||||||
);
|
width +
|
||||||
this.backgroundGrid.append(
|
"px; height:" +
|
||||||
'<div style="position:absolute; left:' +
|
height +
|
||||||
(width + 5) +
|
'px;"></div>'
|
||||||
'px; top:0px;">smallest screen participating</div>'
|
);
|
||||||
);
|
this.backgroundGrid.append(
|
||||||
|
'<div style="position:absolute; left:' +
|
||||||
|
(width + 5) +
|
||||||
|
'px; top:0px;">smallest screen participating</div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleEventsAndData: function (content, isNewData, doneCallback) {
|
handleEventsAndData: function (content, isNewData, doneCallback) {
|
||||||
|
Loading…
Reference in New Issue
Block a user