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:
Cracker 2020-05-11 22:59:37 +02:00 committed by GitHub
commit 3e9c3184f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 5116 additions and 298 deletions

View File

@ -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
View File

@ -1,3 +1,5 @@
/config.run.yml
# Logs
logs
*.log

View File

@ -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

View File

@ -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
View 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
View 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`

View File

@ -5,5 +5,4 @@ services:
restart: always
ports:
- "8080:8080/tcp"
environment:
- ACCESSTOKEN=mysecrettoken
command: --config=./config.default.yml

4033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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;

View 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
View 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
View 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;

View 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);
});

View File

@ -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)

View File

@ -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;
}

View File

@ -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>

View File

@ -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
*

View File

@ -1,2 +0,0 @@
export const POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA = 1; // 1px
export const POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA = 10; // 1ms

View File

@ -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();

View File

@ -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;

View 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();

View 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) };
}

View 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);
});

View 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();

View File

@ -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();
}
}

View 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();

View File

@ -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;
}

View File

@ -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) {