mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
UBERF-7090: Add Office plugins (#5725)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Generated
+333
-3
@@ -35,6 +35,12 @@ dependencies:
|
||||
'@koa/cors':
|
||||
specifier: ^3.1.0
|
||||
version: 3.4.3
|
||||
'@livekit/krisp-noise-filter':
|
||||
specifier: ~0.2.1
|
||||
version: 0.2.2(livekit-client@2.2.0)
|
||||
'@livekit/track-processors':
|
||||
specifier: ~0.3.1
|
||||
version: 0.3.2(livekit-client@2.2.0)
|
||||
'@playwright/test':
|
||||
specifier: ^1.41.2
|
||||
version: 1.41.2
|
||||
@@ -239,6 +245,15 @@ dependencies:
|
||||
'@rush-temp/login-resources':
|
||||
specifier: file:./projects/login-resources.tgz
|
||||
version: file:projects/login-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(file-loader@6.2.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)(webpack@5.90.3)
|
||||
'@rush-temp/love':
|
||||
specifier: file:./projects/love.tgz
|
||||
version: file:projects/love.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
'@rush-temp/love-assets':
|
||||
specifier: file:./projects/love-assets.tgz
|
||||
version: file:projects/love-assets.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
'@rush-temp/love-resources':
|
||||
specifier: file:./projects/love-resources.tgz
|
||||
version: file:projects/love-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
|
||||
'@rush-temp/middleware':
|
||||
specifier: file:./projects/middleware.tgz
|
||||
version: file:projects/middleware.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
@@ -299,6 +314,9 @@ dependencies:
|
||||
'@rush-temp/model-lead':
|
||||
specifier: file:./projects/model-lead.tgz
|
||||
version: file:projects/model-lead.tgz
|
||||
'@rush-temp/model-love':
|
||||
specifier: file:./projects/model-love.tgz
|
||||
version: file:projects/model-love.tgz
|
||||
'@rush-temp/model-notification':
|
||||
specifier: file:./projects/model-notification.tgz
|
||||
version: file:projects/model-notification.tgz
|
||||
@@ -368,6 +386,9 @@ dependencies:
|
||||
'@rush-temp/model-server-lead':
|
||||
specifier: file:./projects/model-server-lead.tgz
|
||||
version: file:projects/model-server-lead.tgz
|
||||
'@rush-temp/model-server-love':
|
||||
specifier: file:./projects/model-server-love.tgz
|
||||
version: file:projects/model-server-love.tgz
|
||||
'@rush-temp/model-server-notification':
|
||||
specifier: file:./projects/model-server-notification.tgz
|
||||
version: file:projects/model-server-notification.tgz
|
||||
@@ -653,6 +674,12 @@ dependencies:
|
||||
'@rush-temp/server-lead-resources':
|
||||
specifier: file:./projects/server-lead-resources.tgz
|
||||
version: file:projects/server-lead-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
'@rush-temp/server-love':
|
||||
specifier: file:./projects/server-love.tgz
|
||||
version: file:projects/server-love.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
'@rush-temp/server-love-resources':
|
||||
specifier: file:./projects/server-love-resources.tgz
|
||||
version: file:projects/server-love-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
'@rush-temp/server-notification':
|
||||
specifier: file:./projects/server-notification.tgz
|
||||
version: file:projects/server-notification.tgz(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
@@ -1256,6 +1283,9 @@ dependencies:
|
||||
libphonenumber-js:
|
||||
specifier: ^1.9.46
|
||||
version: 1.10.56
|
||||
livekit-client:
|
||||
specifier: ^2.0.10
|
||||
version: 2.2.0
|
||||
mime-types:
|
||||
specifier: ~2.1.34
|
||||
version: 2.1.35
|
||||
@@ -3422,6 +3452,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
dev: false
|
||||
|
||||
/@bufbuild/protobuf@1.10.0:
|
||||
resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==}
|
||||
dev: false
|
||||
|
||||
/@colors/colors@1.5.0:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@@ -4615,6 +4649,30 @@ packages:
|
||||
resolution: {integrity: sha512-GlM2AbzrErd/TmLL3E8hAHmb5Q7VhDJp35vIbyPVA5Rz55LZuRr8pwL3qrwwkVNo05gMX1J44gURKb4MHQZo7w==}
|
||||
dev: false
|
||||
|
||||
/@livekit/krisp-noise-filter@0.2.2(livekit-client@2.2.0):
|
||||
resolution: {integrity: sha512-jGAxvw07+6jF3iU7jEeZ6dnr2c2nSIW3J9zt18KM+dwVsLpLNAfFchlwUStpJEmQoDhw9sQgRSOOKpqHcjj7GQ==}
|
||||
peerDependencies:
|
||||
livekit-client: ^2.0.8
|
||||
dependencies:
|
||||
livekit-client: 2.2.0
|
||||
dev: false
|
||||
|
||||
/@livekit/protocol@1.16.0:
|
||||
resolution: {integrity: sha512-xZZTZVh2FmWmUgNS3n+oGNbA4GcS4XOwhg8CWy75jenYxbgQ89ds7ixfMQ+F+oxktcXfJ1qsph086oRTlg8e5Q==}
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 1.10.0
|
||||
dev: false
|
||||
|
||||
/@livekit/track-processors@0.3.2(livekit-client@2.2.0):
|
||||
resolution: {integrity: sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA==}
|
||||
peerDependencies:
|
||||
livekit-client: ^1.12.0 || ^2.1.0
|
||||
dependencies:
|
||||
'@mediapipe/holistic': 0.5.1675471629
|
||||
'@mediapipe/tasks-vision': 0.10.9
|
||||
livekit-client: 2.2.0
|
||||
dev: false
|
||||
|
||||
/@mdx-js/react@2.3.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==}
|
||||
peerDependencies:
|
||||
@@ -4625,6 +4683,14 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mediapipe/holistic@0.5.1675471629:
|
||||
resolution: {integrity: sha512-qY+cxtDeSOvVtevrLgnodiwXYaAtPi7dHZtNv/bUCGEjFicAOYtMmrZSqMmbPkTB2+4jLnPF1vgshkAqQRSYAw==}
|
||||
dev: false
|
||||
|
||||
/@mediapipe/tasks-vision@0.10.9:
|
||||
resolution: {integrity: sha512-/gFguyJm1ng4Qr7VVH2vKO+zZcQd8wc3YafUfvBuYFX0Y5+CvrV+VNPEVkl5W/gUZF5KNKNZAiaHPULGPCIjyQ==}
|
||||
dev: false
|
||||
|
||||
/@mongodb-js/saslprep@1.1.4:
|
||||
resolution: {integrity: sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==}
|
||||
dependencies:
|
||||
@@ -13687,6 +13753,19 @@ packages:
|
||||
resolution: {integrity: sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==}
|
||||
dev: false
|
||||
|
||||
/livekit-client@2.2.0:
|
||||
resolution: {integrity: sha512-Iuk17l269P7SxvE3Gs3ExwoW3+lrz3BfiXOU7gTf/GqansxPx7Y+rfkLHWSBmvmAXOt+Rld4EFEpk8ARSagfnQ==}
|
||||
dependencies:
|
||||
'@livekit/protocol': 1.16.0
|
||||
events: 3.3.0
|
||||
loglevel: 1.9.1
|
||||
sdp-transform: 2.14.2
|
||||
ts-debounce: 4.0.0
|
||||
tslib: 2.6.2
|
||||
typed-emitter: 2.1.0
|
||||
webrtc-adapter: 8.2.3
|
||||
dev: false
|
||||
|
||||
/load-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-QNZzJ2wMrTmCdeobMuMNEXHN1QGk8HG6louEkzD/zwQ7EU2RarrzlhQ4GnUYEFzLhK+Jq7IGyF/qy+XYBSO7AQ==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -13765,6 +13844,11 @@ packages:
|
||||
is-unicode-supported: 0.1.0
|
||||
dev: false
|
||||
|
||||
/loglevel@1.9.1:
|
||||
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@@ -15947,6 +16031,14 @@ packages:
|
||||
queue-microtask: 1.2.3
|
||||
dev: false
|
||||
|
||||
/rxjs@7.8.1:
|
||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/sade@1.8.1:
|
||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -16064,6 +16156,15 @@ packages:
|
||||
ajv-keywords: 5.1.0(ajv@8.12.0)
|
||||
dev: false
|
||||
|
||||
/sdp-transform@2.14.2:
|
||||
resolution: {integrity: sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/sdp@3.2.0:
|
||||
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
|
||||
dev: false
|
||||
|
||||
/secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
dev: false
|
||||
@@ -17203,6 +17304,10 @@ packages:
|
||||
typescript: 5.3.3
|
||||
dev: false
|
||||
|
||||
/ts-debounce@4.0.0:
|
||||
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
|
||||
dev: false
|
||||
|
||||
/ts-dedent@2.2.0:
|
||||
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
||||
engines: {node: '>=6.10'}
|
||||
@@ -17411,6 +17516,12 @@ packages:
|
||||
possible-typed-array-names: 1.0.0
|
||||
dev: false
|
||||
|
||||
/typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||
optionalDependencies:
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
|
||||
/typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
dev: false
|
||||
@@ -18040,6 +18151,13 @@ packages:
|
||||
- uglify-js
|
||||
dev: false
|
||||
|
||||
/webrtc-adapter@8.2.3:
|
||||
resolution: {integrity: sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==}
|
||||
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||
dependencies:
|
||||
sdp: 3.2.0
|
||||
dev: false
|
||||
|
||||
/websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -20884,6 +21002,118 @@ packages:
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/love-assets.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-zHTnvj12ye1KJ+1xP0m9vPS8e4VSAv+xDkvPO8VLFITyGZ5gvCbaYZSrnq4hqpDwlyexJa3D8yPvFQl0sFtq6Q==, tarball: file:projects/love-assets.tgz}
|
||||
id: file:projects/love-assets.tgz
|
||||
name: '@rush-temp/love-assets'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- svelte
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/love-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-G+/i2n7d4qY/bR9jCFeJzftPXbbfA5izwCKe9DdsPloofD3kvrZvbk/j5IT4r3GggbvaJqNqWg/0CA7fU8XdBw==, tarball: file:projects/love-resources.tgz}
|
||||
id: file:projects/love-resources.tgz
|
||||
name: '@rush-temp/love-resources'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@livekit/krisp-noise-filter': 0.2.2(livekit-client@2.2.0)
|
||||
'@livekit/track-processors': 0.3.2(livekit-client@2.2.0)
|
||||
'@types/jest': 29.5.12
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
livekit-client: 2.2.0
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
sass: 1.71.1
|
||||
svelte: 4.2.12
|
||||
svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)
|
||||
svelte-eslint-parser: 0.33.1(svelte@4.2.12)
|
||||
svelte-loader: 3.2.0(svelte@4.2.12)
|
||||
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@types/node'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- coffeescript
|
||||
- esbuild
|
||||
- less
|
||||
- node-notifier
|
||||
- postcss
|
||||
- postcss-load-config
|
||||
- pug
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/love.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-ta6PzoIdhiwwHaDOqY8S9UlifA3M94ks7iRRHrwLEFfTFJKdWNC1DM+M5/RvVe5L+wEyyp9rBtUQxlGvrO/sMg==, tarball: file:projects/love.tgz}
|
||||
id: file:projects/love.tgz
|
||||
name: '@rush-temp/love'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@types/node'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- svelte
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/middleware.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-TCRTOdjjnR5k1q3QSV18aGhCAjDDtZ79BpVt01TuDdL8tPNyDCkKbPg2vRlWGNd5LgOLUp0m+jcnlaF+eFKTUA==, tarball: file:projects/middleware.tgz}
|
||||
id: file:projects/middleware.tgz
|
||||
@@ -20969,7 +21199,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/model-all.tgz:
|
||||
resolution: {integrity: sha512-eT/WXN3UKeAlTfImDOcLVfmHGhZo/yinjhkqvpVSSCGWJhiCJBNX5vJLn9HkEem6Xh9/YCMxp2MN49n50bV7+Q==, tarball: file:projects/model-all.tgz}
|
||||
resolution: {integrity: sha512-UcKhN9sdUh7HJT5ZwADZSx/EET/kr/xe8xs8BYhJn5Wci3xw43EXc9psEZHfv4i/4ilV+oqkSNuTNgW9wZf75g==, tarball: file:projects/model-all.tgz}
|
||||
name: '@rush-temp/model-all'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@@ -21277,6 +21507,24 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/model-love.tgz:
|
||||
resolution: {integrity: sha512-ddbw4PY5W9pEfY34DqzSn6NAbK0ziwcr05SdDYiRg9JTCqsFZSVFdeWP5ni688c7ZALu2wm5RQAt8VY2j2l7Pg==, tarball: file:projects/model-love.tgz}
|
||||
name: '@rush-temp/model-love'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
prettier: 3.2.5
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/model-notification.tgz:
|
||||
resolution: {integrity: sha512-2SQ44sYlL77E69Ue3MyqlNhCBVkJC6/fXTzKoUFN1QSsS2slObQAeqy2TYPFvjaDwkOQ1BgjgWffqbTAofKpag==, tarball: file:projects/model-notification.tgz}
|
||||
name: '@rush-temp/model-notification'
|
||||
@@ -21729,6 +21977,24 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/model-server-love.tgz:
|
||||
resolution: {integrity: sha512-DSbG9+YE+zZltUGAdEut6QrTG0ZDg4P+yOLBj/aYfi5rke0bOFPH9Z1b/ZsToP06Mu9yKamcwIh/D7LMQ6AemA==, tarball: file:projects/model-server-love.tgz}
|
||||
name: '@rush-temp/model-server-love'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
prettier: 3.2.5
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/model-server-notification.tgz:
|
||||
resolution: {integrity: sha512-KU51P+6VSkvKGtoLLtFCgELbp0lzt+DHTHqWwQSYsrf+Y/zSZWNaY4Jte5yukyyzYCEvP2guyoRkEzej+DV16w==, tarball: file:projects/model-server-notification.tgz}
|
||||
name: '@rush-temp/model-server-notification'
|
||||
@@ -22736,7 +23002,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/pod-server.tgz:
|
||||
resolution: {integrity: sha512-yo0cpcy3zneQGR3O47VO6G/jusFUsmVlFLigKzkDD1PRetuC/AtBJdSjK5sKKiTNDFu6LL//wlAcOF3OMXzAhQ==, tarball: file:projects/pod-server.tgz}
|
||||
resolution: {integrity: sha512-UdAsK0melQjy2f20MCFyUbLi+Dyl/ghoAqXnic76/zV7Qxji9OTxVLHwCyvuQA3MO12rm+sZ5eMXUwgx0Ov/Qw==, tarball: file:projects/pod-server.tgz}
|
||||
name: '@rush-temp/pod-server'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@@ -22990,7 +23256,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.3):
|
||||
resolution: {integrity: sha512-ajlwjCy+8Cb6To3+gHmC4iRhhFBUMgCmSFkasybI2sbQDwJQgcK8mWSGobfr//sQORFOHY5uhTWBndpTIVdErA==, tarball: file:projects/prod.tgz}
|
||||
resolution: {integrity: sha512-fzosw3oI6kx0MdnTkai+NmWBMHbjCWe/qruSPiOtBJuBsxDdU+gDT680W3cpnWjfwu6uV6Jd0J7p1FiQnwv10Q==, tarball: file:projects/prod.tgz}
|
||||
id: file:projects/prod.tgz
|
||||
name: '@rush-temp/prod'
|
||||
version: 0.0.0
|
||||
@@ -24578,6 +24844,70 @@ packages:
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/server-love-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-JDv5GrpI49Mz99PocGgaUnCr6qximXi1doYhGyLIhEKrS5iujQJ6BkzgSsn4HHDyHRChKjvj/MkbYhTe//rvNA==, tarball: file:projects/server-love-resources.tgz}
|
||||
id: file:projects/server-love-resources.tgz
|
||||
name: '@rush-temp/server-love-resources'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@types/node'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- svelte
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/server-love.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-CiYBKI1SQv/VBr54hHnKfF+Wfv0f+FLMdZMOsihmalYrySXpQ3r4ag25lWdleX2k8BDth5bnqn9wEOMQzRBcMw==, tarball: file:projects/server-love.tgz}
|
||||
id: file:projects/server-love.tgz
|
||||
name: '@rush-temp/server-love'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- svelte
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/server-notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-uhmmKoMg/lF5f1B/DlmNUTfo8sDauKZSZEgqSNbNw8SsYoRJG9/xQHlXRPHs71tKeskt+jvwgs/z2385w5R3IA==, tarball: file:projects/server-notification-resources.tgz}
|
||||
id: file:projects/server-notification-resources.tgz
|
||||
|
||||
@@ -191,6 +191,9 @@
|
||||
"@hcengineering/guest-assets": "^0.6.0",
|
||||
"@hcengineering/guest-resources": "^0.6.0",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"@hcengineering/love-assets": "^0.6.0",
|
||||
"@hcengineering/love-resources": "^0.6.0",
|
||||
"@hcengineering/sign": "^0.6.0",
|
||||
"@hcengineering/print": "^0.6.0",
|
||||
"@hcengineering/print-assets": "^0.6.0",
|
||||
|
||||
@@ -48,6 +48,7 @@ import view, { viewId } from '@hcengineering/view'
|
||||
import workbench, { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
import { bitrixId } from '@hcengineering/bitrix'
|
||||
import love, { loveId } from '@hcengineering/love'
|
||||
import print, { printId } from '@hcengineering/print'
|
||||
import sign from '@hcengineering/sign'
|
||||
import { productsId } from '@hcengineering/products'
|
||||
@@ -84,6 +85,7 @@ import '@hcengineering/time-assets'
|
||||
import '@hcengineering/tracker-assets'
|
||||
import '@hcengineering/view-assets'
|
||||
import '@hcengineering/workbench-assets'
|
||||
import '@hcengineering/love-assets'
|
||||
import '@hcengineering/print-assets'
|
||||
import '@hcengineering/questions-assets'
|
||||
import '@hcengineering/training-assets'
|
||||
@@ -114,6 +116,10 @@ interface Config {
|
||||
PUSH_PUBLIC_KEY: string
|
||||
BRANDING_URL?: string
|
||||
PREVIEW_CONFIG: string
|
||||
LOVE_ENDPOINT?: string
|
||||
LIVEKIT_WS?: string
|
||||
SIGN_URL?: string
|
||||
PRINT_URL?: string
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
@@ -177,6 +183,7 @@ function configureI18n(): void {
|
||||
addStringsLoader(productsId, async (lang: string) => await import(`@hcengineering/products-assets/lang/${lang}.json`))
|
||||
addStringsLoader(questionsId, async (lang: string) => await import(`@hcengineering/questions-assets/lang/${lang}.json`))
|
||||
addStringsLoader(trainingId, async (lang: string) => await import(`@hcengineering/training-assets/lang/${lang}.json`))
|
||||
addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`))
|
||||
addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`))
|
||||
}
|
||||
|
||||
@@ -249,7 +256,10 @@ export async function configurePlatform() {
|
||||
setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT)
|
||||
|
||||
setMetadata(rekoni.metadata.RekoniUrl, config.REKONI_URL)
|
||||
|
||||
setMetadata(love.metadata.ServiceEnpdoint, config.LOVE_ENDPOINT)
|
||||
setMetadata(love.metadata.WebSocketURL, config.LIVEKIT_WS)
|
||||
setMetadata(print.metadata.PrintURL, config.PRINT_URL)
|
||||
setMetadata(sign.metadata.SignURL, config.SIGN_URL)
|
||||
setMetadata(textEditor.metadata.CollaboratorUrl, config.COLLABORATOR_URL ?? 'ws://locahost:3078')
|
||||
|
||||
setMetadata(uiPlugin.metadata.DefaultApplication, login.component.LoginApp)
|
||||
@@ -307,6 +317,7 @@ export async function configurePlatform() {
|
||||
addLocation(trainingId, () => import(/* webpackChunkName: "training" */ '@hcengineering/training-resources'))
|
||||
addLocation(productsId, () => import(/* webpackChunkName: "products" */ '@hcengineering/products-resources'))
|
||||
addLocation(documentsId, () => import(/* webpackChunkName: "documents" */ '@hcengineering/controlled-documents-resources'))
|
||||
addLocation(loveId, () => import(/* webpackChunkName: "love" */ '@hcengineering/love-resources'))
|
||||
addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources'))
|
||||
|
||||
setMetadata(client.metadata.FilterModel, true)
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
"@hcengineering/model-server-time": "^0.6.0",
|
||||
"@hcengineering/model-drive": "^0.6.0",
|
||||
"@hcengineering/model-server-drive": "^0.6.0",
|
||||
"@hcengineering/model-love": "^0.6.0",
|
||||
"@hcengineering/model-server-love": "^0.6.0",
|
||||
"@hcengineering/model-print": "^0.6.0",
|
||||
"@hcengineering/model-questions": "^0.1.0",
|
||||
"@hcengineering/model-training": "^0.1.0",
|
||||
|
||||
@@ -79,6 +79,8 @@ import workbench, { workbenchId, createModel as workbenchModel } from '@hcengine
|
||||
import { openAIId, createModel as serverOpenAI } from '@hcengineering/model-server-openai'
|
||||
import { createModel as serverTranslate, translateId } from '@hcengineering/model-server-translate'
|
||||
|
||||
import love, { loveId, createModel as loveModel } from '@hcengineering/model-love'
|
||||
import { serverLoveId, createModel as serverLoveModel } from '@hcengineering/model-server-love'
|
||||
import { printId, createModel as printModel } from '@hcengineering/model-print'
|
||||
|
||||
import { questionsId, createModel as questionsModel } from '@hcengineering/model-questions'
|
||||
@@ -311,6 +313,18 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
|
||||
}
|
||||
],
|
||||
[timeModel, timeId],
|
||||
[
|
||||
loveModel,
|
||||
loveId,
|
||||
{
|
||||
label: love.string.Office,
|
||||
description: love.string.LoveDescription,
|
||||
enabled: true,
|
||||
beta: true,
|
||||
icon: love.icon.Love,
|
||||
classFilter: defaultFilter
|
||||
}
|
||||
],
|
||||
[printModel, printId],
|
||||
[driveModel, driveId],
|
||||
[supportModel, supportId],
|
||||
@@ -381,6 +395,7 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
|
||||
[serverTranslate, translateId],
|
||||
[serverOpenAI, openAIId],
|
||||
[serverDocumentModel, serverDocumentId],
|
||||
[serverLoveModel, serverLoveId],
|
||||
[serverTimeModel, serverTimeId],
|
||||
[serverGuestModel, serverGuestId],
|
||||
[serverDriveModel, serverDriveId],
|
||||
|
||||
@@ -40,6 +40,7 @@ import { calendarOperation } from '@hcengineering/model-calendar'
|
||||
import { timeOperation } from '@hcengineering/model-time'
|
||||
import { activityOperation } from '@hcengineering/model-activity'
|
||||
import { activityServerOperation } from '@hcengineering/model-server-activity'
|
||||
import { loveId, loveOperation } from '@hcengineering/model-love'
|
||||
import { documentOperation } from '@hcengineering/model-document'
|
||||
import { textEditorOperation } from '@hcengineering/model-text-editor'
|
||||
import { questionsOperation } from '@hcengineering/model-questions'
|
||||
@@ -76,6 +77,7 @@ export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['inventiry', inventoryOperation],
|
||||
['time', timeOperation],
|
||||
['activityServer', activityServerOperation],
|
||||
[loveId, loveOperation],
|
||||
['document', documentOperation],
|
||||
['textEditor', textEditorOperation],
|
||||
// We should call it after activityServer and chunter
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "model"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@hcengineering/model-love",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Uberflow Contributors",
|
||||
"template": "@hcengineering/model-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/model-presentation": "^0.6.0",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/setting": "^0.6.11",
|
||||
"@hcengineering/workbench": "^0.6.9",
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/model-preference": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/drive": "^0.6.0",
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"@hcengineering/love-resources": "^0.6.0",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/model-notification": "^0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import contact, { type Employee, type Person } from '@hcengineering/contact'
|
||||
import { AccountRole, DOMAIN_TRANSIENT, IndexKind, type Domain, type Ref } from '@hcengineering/core'
|
||||
import { Index, Model, Prop, TypeRef, type Builder } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import {
|
||||
loveId,
|
||||
type DevicesPreference,
|
||||
type Floor,
|
||||
type Invite,
|
||||
type JoinRequest,
|
||||
type Office,
|
||||
type ParticipantInfo,
|
||||
type RequestStatus,
|
||||
type Room,
|
||||
type RoomAccess,
|
||||
type RoomInfo,
|
||||
type RoomType
|
||||
} from '@hcengineering/love'
|
||||
import love from './plugin'
|
||||
|
||||
export { loveId } from '@hcengineering/love'
|
||||
export * from './migration'
|
||||
export const DOMAIN_LOVE = 'love' as Domain
|
||||
|
||||
@Model(love.class.Room, core.class.Doc, DOMAIN_LOVE)
|
||||
export class TRoom extends TDoc implements Room {
|
||||
name!: string
|
||||
|
||||
type!: RoomType
|
||||
|
||||
access!: RoomAccess
|
||||
|
||||
@Prop(TypeRef(love.class.Floor), getEmbeddedLabel('Floor'))
|
||||
// @Index(IndexKind.Indexed)
|
||||
floor!: Ref<Floor>
|
||||
|
||||
width!: number
|
||||
height!: number
|
||||
x!: number
|
||||
y!: number
|
||||
}
|
||||
|
||||
@Model(love.class.Office, love.class.Room)
|
||||
export class TOffice extends TRoom implements Office {
|
||||
@Prop(TypeRef(contact.mixin.Employee), contact.string.Employee)
|
||||
@Index(IndexKind.Indexed)
|
||||
person!: Ref<Employee> | null
|
||||
}
|
||||
|
||||
@Model(love.class.Floor, core.class.Doc, DOMAIN_LOVE)
|
||||
export class TFloor extends TDoc implements Floor {
|
||||
name!: string
|
||||
}
|
||||
|
||||
@Model(love.class.ParticipantInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TParticipantInfo extends TDoc implements ParticipantInfo {
|
||||
name!: string
|
||||
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('Person'))
|
||||
person!: Ref<Person>
|
||||
|
||||
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
|
||||
room!: Ref<Room>
|
||||
|
||||
x!: number
|
||||
y!: number
|
||||
}
|
||||
|
||||
@Model(love.class.JoinRequest, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TJoinRequest extends TDoc implements JoinRequest {
|
||||
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('From'))
|
||||
person!: Ref<Person>
|
||||
|
||||
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
|
||||
room!: Ref<Room>
|
||||
|
||||
status!: RequestStatus
|
||||
}
|
||||
|
||||
@Model(love.class.Invite, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TInvite extends TDoc implements Invite {
|
||||
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('From'))
|
||||
from!: Ref<Person>
|
||||
|
||||
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('Target'))
|
||||
target!: Ref<Person>
|
||||
|
||||
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
|
||||
room!: Ref<Room>
|
||||
|
||||
status!: RequestStatus
|
||||
}
|
||||
|
||||
@Model(love.class.DevicesPreference, preference.class.Preference)
|
||||
export class TDevicesPreference extends TPreference implements DevicesPreference {
|
||||
blurRadius!: number
|
||||
noiseCancellation!: boolean
|
||||
micEnabled!: boolean
|
||||
camEnabled!: boolean
|
||||
}
|
||||
|
||||
@Model(love.class.RoomInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TRoomInfo extends TDoc implements RoomInfo {
|
||||
persons!: Ref<Person>[]
|
||||
room!: Ref<Room>
|
||||
isOffice!: boolean
|
||||
}
|
||||
|
||||
export default love
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TRoom, TFloor, TOffice, TParticipantInfo, TJoinRequest, TDevicesPreference, TRoomInfo, TInvite)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
{
|
||||
label: love.string.Office,
|
||||
icon: love.icon.Love,
|
||||
alias: loveId,
|
||||
hidden: false,
|
||||
position: 'top',
|
||||
modern: true,
|
||||
component: love.component.Main
|
||||
},
|
||||
love.app.Love
|
||||
)
|
||||
|
||||
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
|
||||
extension: workbench.extensions.WorkbenchExtensions,
|
||||
component: love.component.WorkbenchExtension
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
setting.class.SettingsCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: loveId,
|
||||
label: love.string.Office,
|
||||
icon: love.icon.Love,
|
||||
component: love.component.Settings,
|
||||
group: 'settings-account',
|
||||
role: AccountRole.Guest,
|
||||
order: 1600
|
||||
},
|
||||
love.ids.Settings
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
label: love.string.Office,
|
||||
icon: love.icon.Love
|
||||
},
|
||||
love.ids.LoveNotificationGroup
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
hidden: false,
|
||||
generated: false,
|
||||
label: love.string.InivitingLabel,
|
||||
group: love.ids.LoveNotificationGroup,
|
||||
txClasses: [core.class.TxCreateDoc],
|
||||
objectClass: love.class.Invite,
|
||||
providers: {
|
||||
[notification.providers.PlatformNotification]: true,
|
||||
[notification.providers.BrowserNotification]: true
|
||||
}
|
||||
},
|
||||
love.ids.InviteNotification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
hidden: false,
|
||||
generated: false,
|
||||
label: love.string.KnockingLabel,
|
||||
group: love.ids.LoveNotificationGroup,
|
||||
txClasses: [],
|
||||
objectClass: love.class.JoinRequest,
|
||||
providers: {
|
||||
[notification.providers.PlatformNotification]: true,
|
||||
[notification.providers.BrowserNotification]: true
|
||||
}
|
||||
},
|
||||
love.ids.KnockNotification
|
||||
)
|
||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||
domain: DOMAIN_LOVE,
|
||||
disabled: [{ space: 1 }, { modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }]
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ActionCategory,
|
||||
core.space.Model,
|
||||
{ label: love.string.Office, visible: true },
|
||||
love.category.Office
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: love.actionImpl.ToggleMic,
|
||||
label: love.string.Microphone,
|
||||
icon: love.icon.Mic,
|
||||
keyBinding: ['Meta + keyD'],
|
||||
category: love.category.Office,
|
||||
allowedForEditableContent: true,
|
||||
input: 'none',
|
||||
target: core.class.Doc,
|
||||
context: {
|
||||
mode: ['workbench', 'browser', 'panel', 'editor', 'input']
|
||||
}
|
||||
},
|
||||
love.action.ToggleMic
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: love.actionImpl.ToggleVideo,
|
||||
label: love.string.Camera,
|
||||
icon: love.icon.Cam,
|
||||
allowedForEditableContent: true,
|
||||
keyBinding: ['Meta + keyE'],
|
||||
category: love.category.Office,
|
||||
input: 'none',
|
||||
target: core.class.Doc,
|
||||
context: {
|
||||
mode: ['workbench', 'browser', 'panel', 'editor', 'input']
|
||||
}
|
||||
},
|
||||
love.action.ToggleVideo
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import { TxOperations, type Ref } from '@hcengineering/core'
|
||||
import drive from '@hcengineering/drive'
|
||||
import {
|
||||
createDefaultSpace,
|
||||
tryUpgrade,
|
||||
type MigrateOperation,
|
||||
type MigrationClient,
|
||||
type MigrationUpgradeClient
|
||||
} from '@hcengineering/model'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { RoomAccess, RoomType, createDefaultRooms, isOffice, loveId, type Floor } from '@hcengineering/love'
|
||||
import love from './plugin'
|
||||
|
||||
async function createDefaultFloor (tx: TxOperations): Promise<void> {
|
||||
const current = await tx.findOne(love.class.Floor, {
|
||||
_id: love.ids.MainFloor
|
||||
})
|
||||
if (current === undefined) {
|
||||
await tx.createDoc(
|
||||
love.class.Floor,
|
||||
love.space.Rooms,
|
||||
{
|
||||
name: 'Main'
|
||||
},
|
||||
love.ids.MainFloor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function createRooms (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
const rooms = await client.findAll(love.class.Room, {})
|
||||
for (const room of rooms) {
|
||||
await tx.remove(room)
|
||||
}
|
||||
const employees = await client.findAll(contact.mixin.Employee, { active: true })
|
||||
const data = createDefaultRooms(employees.map((p) => p._id))
|
||||
for (const room of data) {
|
||||
const _class = isOffice(room) ? love.class.Office : love.class.Room
|
||||
await tx.createDoc(_class, love.space.Rooms, room)
|
||||
}
|
||||
}
|
||||
|
||||
async function createReception (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
const current = await tx.findOne(love.class.Room, {
|
||||
_id: love.ids.Reception
|
||||
})
|
||||
if (current !== undefined) return
|
||||
await tx.createDoc(
|
||||
love.class.Room,
|
||||
love.space.Rooms,
|
||||
{
|
||||
name: 'Reception',
|
||||
type: RoomType.Reception,
|
||||
access: RoomAccess.Open,
|
||||
floor: '' as Ref<Floor>,
|
||||
width: 100,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
love.ids.Reception
|
||||
)
|
||||
}
|
||||
|
||||
export const loveOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
await tryUpgrade(client, loveId, [
|
||||
{
|
||||
state: 'create-defaults-v2',
|
||||
func: async (client) => {
|
||||
await createDefaultSpace(client, love.space.Rooms, { name: 'Rooms', description: 'Space for all rooms' })
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'initial-defaults',
|
||||
func: async (client) => {
|
||||
await createDefaultFloor(tx)
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'createRooms_v2',
|
||||
func: createRooms
|
||||
},
|
||||
|
||||
{
|
||||
state: 'create-reception',
|
||||
func: async (client) => {
|
||||
await createReception(client)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
await createDefaultSpace(
|
||||
client,
|
||||
love.space.Drive,
|
||||
{
|
||||
name: 'Records',
|
||||
description: 'Office records',
|
||||
type: drive.spaceType.DefaultDrive,
|
||||
autoJoin: true
|
||||
},
|
||||
drive.class.Drive
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type NotificationGroup } from '@hcengineering/notification'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import { type ActionCategory, type ViewAction } from '@hcengineering/view'
|
||||
import { loveId } from '@hcengineering/love'
|
||||
import love from '@hcengineering/love-resources/src/plugin'
|
||||
|
||||
export default mergeIds(loveId, love, {
|
||||
component: {
|
||||
Main: '' as AnyComponent,
|
||||
WorkbenchExtension: '' as AnyComponent,
|
||||
Settings: '' as AnyComponent
|
||||
},
|
||||
app: {
|
||||
Love: '' as Ref<Doc>
|
||||
},
|
||||
category: {
|
||||
Office: '' as Ref<ActionCategory>
|
||||
},
|
||||
actionImpl: {
|
||||
ToggleMic: '' as ViewAction,
|
||||
ToggleVideo: '' as ViewAction
|
||||
},
|
||||
ids: {
|
||||
Settings: '' as Ref<Doc>,
|
||||
LoveNotificationGroup: '' as Ref<NotificationGroup>
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "model"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@hcengineering/model-server-love",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"template": "@hcengineering/model-package",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-love": "^0.6.0",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"@hcengineering/server-love": "^0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import core from '@hcengineering/core'
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import serverCore from '@hcengineering/server-core'
|
||||
import love from '@hcengineering/love'
|
||||
import serverLove from '@hcengineering/server-love'
|
||||
|
||||
export { serverLoveId } from '@hcengineering/server-love'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLove.trigger.OnEmployee,
|
||||
txMatch: {
|
||||
_class: core.class.TxMixin,
|
||||
objectClass: contact.class.Person,
|
||||
mixin: contact.mixin.Employee,
|
||||
'attributes.active': { $exists: true }
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLove.trigger.OnUserStatus,
|
||||
txMatch: {
|
||||
objectClass: core.class.UserStatus
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLove.trigger.OnParticipantInfo,
|
||||
txMatch: {
|
||||
objectClass: love.class.ParticipantInfo
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLove.trigger.OnKnock,
|
||||
txMatch: {
|
||||
objectClass: love.class.JoinRequest,
|
||||
_class: core.class.TxCreateDoc
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLove.trigger.OnInvite,
|
||||
txMatch: {
|
||||
objectClass: love.class.Invite,
|
||||
_class: core.class.TxCreateDoc
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="office" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10.001 5H11.251V6.25H10.001V5Z" fill="currentColor"/>
|
||||
<path d="M13.751 5H12.501V6.25H13.751V5Z" fill="currentColor"/>
|
||||
<path d="M10.001 10H11.251V11.25H10.001V10Z" fill="currentColor"/>
|
||||
<path d="M13.751 10H12.501V11.25H13.751V10Z" fill="currentColor"/>
|
||||
<path d="M10.001 12.5H11.251V13.75H10.001V12.5Z" fill="currentColor"/>
|
||||
<path d="M13.751 12.5H12.501V13.75H13.751V12.5Z" fill="currentColor"/>
|
||||
<path d="M10.001 7.5H11.251V8.75H10.001V7.5Z" fill="currentColor"/>
|
||||
<path d="M13.751 7.5H12.501V8.75H13.751V7.5Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75098 2.5C8.06062 2.5 7.50098 3.05964 7.50098 3.75V8.125H5.00098C4.31062 8.125 3.75098 8.68464 3.75098 9.375V16.875H3.12598C2.7808 16.875 2.50098 17.1548 2.50098 17.5C2.50098 17.8452 2.7808 18.125 3.12598 18.125H16.876C17.2212 18.125 17.501 17.8452 17.501 17.5C17.501 17.1548 17.2212 16.875 16.876 16.875H16.251V3.75C16.251 3.05964 15.6913 2.5 15.001 2.5H8.75098ZM5.00098 9.375H7.50098V11.25H6.25098V12.5H7.50098V13.75H6.25098V15H7.50098V16.875H5.00098V9.375ZM8.75098 16.875H11.251V15H12.501V16.875H15.001V3.75H8.75098V16.875Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="knock" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12.4,11.3c-0.2,0.2-0.3,0.5-0.3,0.7c0,0.1,0,0.3,0.1,0.4c0.1,0.1,0.1,0.2,0.2,0.3c0.2,0.2,0.4,0.3,0.7,0.3c0.1,0,0.3,0,0.4-0.1c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.2-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.3-0.1-0.5-0.3-0.7c-0.1-0.1-0.2-0.2-0.3-0.2C13.1,10.9,12.7,11,12.4,11.3z" fill="currentColor" />
|
||||
<path d="M22,21h-3.4v-7.3c0-0.6-0.4-1-1-1s-1,0.4-1,1V21H5.2V4.2c0-0.3,0.1-0.6,0.3-0.8c0,0,0.1,0,0.1-0.1C5.8,3.1,6.1,3,6.4,3h6.2c0.6,0,1-0.4,1-1s-0.4-1-1-1H6.4C5.6,1,4.8,1.3,4.2,1.9c-0.7,0.6-1,1.4-1,2.3V21H2c-0.6,0-1,0.4-1,1s0.4,1,1,1h20c0.6,0,1-0.4,1-1S22.6,21,22,21z" fill="currentColor" />
|
||||
<path d="M16,4.7c0.6,0,1-0.4,1-1V2c0-0.6-0.4-1-1-1s-1,0.4-1,1v1.7C15,4.3,15.4,4.7,16,4.7z" fill="currentColor" />
|
||||
<path d="M19.3,7.2c0,0.6,0.4,1,1,1H22c0.6,0,1-0.4,1-1s-0.4-1-1-1h-1.7C19.7,6.2,19.3,6.6,19.3,7.2z" fill="currentColor" />
|
||||
<path d="M19.6,12.2c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4l-1.3-1.3c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4L19.6,12.2z" fill="currentColor" />
|
||||
<path d="M19,5.2c0.3,0,0.5-0.1,0.7-0.3L21,3.6c0.4-0.4,0.4-1,0-1.4s-1-0.4-1.4,0l-1.3,1.3c-0.4,0.4-0.4,1,0,1.4C18.5,5.1,18.7,5.2,19,5.2z" fill="currentColor" />
|
||||
</symbol>
|
||||
<symbol id="open" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12.1,12c0,0.6,0.4,1,1,1c0.6,0,1-0.4,1-1v0c0-0.6-0.4-1-1-1C12.6,11,12.1,11.5,12.1,12z" fill="#49a26b" />
|
||||
<path d="M22,21h-3.4v-7.3c0-0.6-0.4-1-1-1s-1,0.4-1,1V21H5.2V4.2c0-0.3,0.1-0.6,0.4-0.9C5.8,3.1,6.1,3,6.4,3h6.7c0.6,0,1-0.4,1-1s-0.4-1-1-1H6.4C5.6,1,4.8,1.3,4.2,1.9C3.6,2.5,3.2,3.4,3.2,4.2V21H2c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h20c0.6,0,1-0.4,1-1C23,21.4,22.6,21,22,21z" fill="#49a26b" />
|
||||
<path d="M13.5,5.8L13.5,5.8C13.5,5.8,13.5,5.8,13.5,5.8c-0.1,0.1-0.2,0.2-0.2,0.3c0,0.1-0.1,0.2-0.1,0.4c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0.1,0,0.2,0.1,0.4c0,0.1,0.1,0.2,0.2,0.3c0,0,0,0,0,0v0c0,0,0,0,0,0l3.3,3.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4l-1.6-1.6H22c0.6,0,1-0.4,1-1s-0.4-1-1-1h-5.4l1.6-1.6c0.4-0.4,0.4-1,0-1.4c-0.4-0.4-1-0.4-1.4,0L13.5,5.8C13.5,5.7,13.5,5.7,13.5,5.8z" fill="#49a26b" />
|
||||
</symbol>
|
||||
<symbol id="dnd" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.9,22.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-20-20c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l3.1,3.1V21H2c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h20c0.1,0,0.3,0,0.4-0.1C22.6,22.8,22.8,22.6,22.9,22.4z M6.4,7.8L17.6,19v2H6.4V7.8z" fill="#FF6711" />
|
||||
<path d="M6.9,3.2C7.2,3.1,7.4,3,7.6,3h8.8c0.4,0,0.6,0.1,0.9,0.3c0.2,0.2,0.3,0.5,0.3,0.9v10c0,0.6,0.4,1,1,1c0.6,0,1-0.4,1-1v-10c0-0.9-0.3-1.7-0.9-2.3C18.1,1.3,17.3,1,16.4,1H7.6C7.1,1,6.7,1.1,6.3,1.3C5.8,1.4,5.5,2,5.7,2.5C5.9,3.1,6.4,3.3,6.9,3.2z" fill="#FF6711" />
|
||||
</symbol>
|
||||
<symbol id="micOn" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12,15.7c1,0,1.9-0.4,2.6-1.1c0.7-0.7,1.1-1.6,1.1-2.6V4.6c0-1-0.4-1.9-1.1-2.6c-1.4-1.4-3.8-1.4-5.2,0C8.7,2.7,8.3,3.7,8.3,4.6V12c0,1,0.4,1.9,1.1,2.6C10.1,15.3,11,15.7,12,15.7z M10.2,4.6c0-0.5,0.2-1,0.5-1.3C11,3,11.5,2.8,12,2.8c0.5,0,1,0.2,1.3,0.5c0.3,0.3,0.5,0.8,0.5,1.3V12c0,0.5-0.2,1-0.5,1.3c-0.7,0.7-1.9,0.7-2.6,0c-0.3-0.3-0.5-0.8-0.5-1.3V4.6z" fill="#49a26b" />
|
||||
<path d="M18.4,9.2c-0.5,0-0.9,0.4-0.9,0.9V12c0,1.5-0.6,2.9-1.6,3.9c-1,1-2.4,1.6-3.9,1.6c-1.5,0-2.9-0.6-3.9-1.6c-1-1-1.6-2.4-1.6-3.9v-1.8c0-0.5-0.4-0.9-0.9-0.9s-0.9,0.4-0.9,0.9V12c0,2,0.8,3.8,2.2,5.2c1.2,1.2,2.7,1.9,4.3,2.1v1.9H8.3c-0.5,0-0.9,0.4-0.9,0.9S7.8,23,8.3,23h7.3c0.5,0,0.9-0.4,0.9-0.9s-0.4-0.9-0.9-0.9h-2.8v-1.9c1.6-0.2,3.1-0.9,4.3-2.1c1.4-1.4,2.2-3.2,2.2-5.2v-1.8C19.3,9.6,18.9,9.2,18.4,9.2z" fill="#49a26b" />
|
||||
</symbol>
|
||||
<symbol id="micOff" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.7,21.4L2.6,1.3c-0.4-0.4-0.9-0.4-1.3,0c-0.4,0.4-0.4,0.9,0,1.3l7.1,7.1V12c0,0.7,0.2,1.4,0.6,2c0.4,0.6,1,1.1,1.6,1.3c0.4,0.2,0.9,0.3,1.4,0.3c0.2,0,0.5,0,0.7-0.1c0.4-0.1,0.8-0.2,1.1-0.4l1.4,1.4c-0.6,0.5-1.3,0.8-2.1,0.9c-1.1,0.2-2.2,0.1-3.2-0.3c-1-0.4-1.9-1.1-2.5-2c-0.6-0.9-0.9-2-0.9-3.1v-1.8c0-0.5-0.4-0.9-0.9-0.9s-0.9,0.4-0.9,0.9V12c0,1.5,0.4,2.9,1.2,4.1c0.8,1.2,2,2.2,3.3,2.7c0.6,0.3,1.2,0.4,1.9,0.5v1.8H8.3c-0.5,0-0.9,0.4-0.9,0.9S7.8,23,8.3,23h7.3c0.5,0,0.9-0.4,0.9-0.9s-0.4-0.9-0.9-0.9h-2.8v-1.8c0.2,0,0.4,0,0.6-0.1c1.1-0.2,2.2-0.7,3-1.4l4.9,4.9c0.2,0.2,0.4,0.3,0.6,0.3s0.5-0.1,0.6-0.3C23.1,22.4,23.1,21.8,22.7,21.4z M12.4,13.8c-0.4,0.1-0.7,0-1.1-0.1c-0.3-0.1-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.3-1v-0.5l2.3,2.3C12.4,13.8,12.4,13.8,12.4,13.8z" fill="#FF6711" />
|
||||
<path d="M9.1,5c0.5,0.1,1-0.2,1.1-0.7c0.1-0.4,0.3-0.8,0.7-1.1c0.4-0.3,0.8-0.4,1.3-0.3c0.5,0,0.9,0.3,1.2,0.6c0.3,0.3,0.5,0.8,0.5,1.2v4.9c0,0.5,0.4,0.9,0.9,0.9s0.9-0.4,0.9-0.9V4.7c0-0.9-0.3-1.8-0.9-2.5c-0.6-0.7-1.4-1.1-2.4-1.2c-0.9-0.1-1.8,0.2-2.5,0.7C9.1,2.3,8.6,3,8.4,3.9C8.3,4.4,8.6,4.9,9.1,5z" fill="#FF6711" />
|
||||
<path d="M18.2,14c0.1,0,0.1,0,0.2,0c0.4,0,0.8-0.3,0.9-0.8c0.1-0.4,0.1-0.9,0.1-1.3v-1.8c0-0.5-0.4-0.9-0.9-0.9c-0.5,0-0.9,0.4-0.9,0.9V12c0,0.3,0,0.6-0.1,1C17.3,13.5,17.7,13.9,18.2,14z" fill="#FF6711" />
|
||||
</symbol>
|
||||
<symbol id="mic" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12,15.7c1,0,1.9-0.4,2.6-1.1c0.7-0.7,1.1-1.6,1.1-2.6V4.6c0-1-0.4-1.9-1.1-2.6c-1.4-1.4-3.8-1.4-5.2,0C8.7,2.7,8.3,3.7,8.3,4.6V12c0,1,0.4,1.9,1.1,2.6C10.1,15.3,11,15.7,12,15.7z M10.2,4.6c0-0.5,0.2-1,0.5-1.3C11,3,11.5,2.8,12,2.8c0.5,0,1,0.2,1.3,0.5c0.3,0.3,0.5,0.8,0.5,1.3V12c0,0.5-0.2,1-0.5,1.3c-0.7,0.7-1.9,0.7-2.6,0c-0.3-0.3-0.5-0.8-0.5-1.3V4.6z" fill="currentColor" />
|
||||
<path d="M18.4,9.2c-0.5,0-0.9,0.4-0.9,0.9V12c0,1.5-0.6,2.9-1.6,3.9c-1,1-2.4,1.6-3.9,1.6c-1.5,0-2.9-0.6-3.9-1.6c-1-1-1.6-2.4-1.6-3.9v-1.8c0-0.5-0.4-0.9-0.9-0.9s-0.9,0.4-0.9,0.9V12c0,2,0.8,3.8,2.2,5.2c1.2,1.2,2.7,1.9,4.3,2.1v1.9H8.3c-0.5,0-0.9,0.4-0.9,0.9S7.8,23,8.3,23h7.3c0.5,0,0.9-0.4,0.9-0.9s-0.4-0.9-0.9-0.9h-2.8v-1.9c1.6-0.2,3.1-0.9,4.3-2.1c1.4-1.4,2.2-3.2,2.2-5.2v-1.8C19.3,9.6,18.9,9.2,18.4,9.2z" fill="currentColor" />
|
||||
</symbol>
|
||||
<symbol id="camera" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.5,6.6c-0.3-0.2-0.7-0.1-1,0.1l-4.8,3.4V7.5c0-1.6-1.3-2.8-2.8-2.8h-10C2.3,4.6,1,5.9,1,7.5v9.1c0,1.6,1.3,2.8,2.8,2.8h10c1.6,0,2.8-1.3,2.8-2.8v-2.6l4.8,3.4c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.3,0,0.5-0.1c0.3-0.2,0.5-0.5,0.5-0.9V7.5C23,7.1,22.8,6.7,22.5,6.6z M14.6,16.5c0,0.5-0.4,0.8-0.8,0.8h-10C3.4,17.4,3,17,3,16.5V7.5C3,7,3.4,6.6,3.8,6.6h10c0.5,0,0.8,0.4,0.8,0.8V16.5z M21,14.6L17.4,12L21,9.4V14.6z" fill="currentColor" />
|
||||
</symbol>
|
||||
<symbol id="cameraOn" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.5,6.6c-0.3-0.2-0.7-0.1-1,0.1l-4.8,3.4V7.5c0-1.6-1.3-2.8-2.8-2.8h-10C2.3,4.6,1,5.9,1,7.5v9.1c0,1.6,1.3,2.8,2.8,2.8h10c1.6,0,2.8-1.3,2.8-2.8v-2.6l4.8,3.4c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.3,0,0.5-0.1c0.3-0.2,0.5-0.5,0.5-0.9V7.5C23,7.1,22.8,6.7,22.5,6.6z M14.6,16.5c0,0.5-0.4,0.8-0.8,0.8h-10C3.4,17.4,3,17,3,16.5V7.5C3,7,3.4,6.6,3.8,6.6h10c0.5,0,0.8,0.4,0.8,0.8V16.5z M21,14.6L17.4,12L21,9.4V14.6z" fill="#49a26b" />
|
||||
</symbol>
|
||||
<symbol id="cameraOff" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.5,6.6c-0.3-0.2-0.7-0.1-1,0.1l-4.9,3.5l-0.1-0.1V7.4c0-0.7-0.3-1.4-0.8-1.9s-1.2-0.8-1.9-0.8h-3.1c-0.5,0-0.9,0.4-0.9,0.9s0.4,0.9,0.9,0.9h3.1c0.2,0,0.5,0.1,0.6,0.3c0.2,0.2,0.3,0.4,0.3,0.6v3.1c0,0.2,0.1,0.5,0.3,0.6l0.9,0.9c0.3,0.3,0.8,0.4,1.2,0.1l4-2.9v7.4c0,0.5,0.4,0.9,0.9,0.9s0.9-0.4,0.9-0.9V7.4C23,7.1,22.8,6.8,22.5,6.6z" fill="#FF6711" />
|
||||
<path d="M16.3,15C16.3,15,16.3,15,16.3,15L2.6,1.3c-0.4-0.4-0.9-0.4-1.3,0c-0.4,0.4-0.4,0.9,0,1.3l2.1,2.1C2.8,4.8,2.2,5,1.8,5.5C1.3,6,1,6.7,1,7.4v9.2c0,0.7,0.3,1.4,0.8,1.9C2.3,19,3,19.3,3.8,19.3h10.1c0.7,0,1.4-0.3,1.9-0.8c0.3-0.3,0.4-0.5,0.6-0.9l5.1,5.1c0.2,0.2,0.4,0.3,0.6,0.3s0.5-0.1,0.6-0.3c0.4-0.4,0.4-0.9,0-1.3L16.3,15z M14.8,16.6c0,0.2-0.1,0.5-0.3,0.6c-0.2,0.2-0.4,0.3-0.6,0.3H3.8c-0.2,0-0.5-0.1-0.6-0.3c-0.2-0.2-0.3-0.4-0.3-0.6V7.4c0-0.2,0.1-0.5,0.3-0.6c0.2-0.2,0.4-0.3,0.6-0.3h1.5l9.5,9.5V16.6z" fill="#FF6711" />
|
||||
</symbol>
|
||||
<symbol id="shareOn" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20,2H4C2.3,2,1,3.3,1,5v10c0,1.7,1.3,3,3,3h7v2H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h8c0.6,0,1-0.4,1-1s-0.4-1-1-1h-3v-2h7c1.7,0,3-1.3,3-3V5C23,3.3,21.7,2,20,2z M21,15c0,0.6-0.4,1-1,1H4c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1V15z" fill="#FF6711" />
|
||||
<path d="M15.7,6.3c-0.4-0.4-1-0.4-1.4,0L12,8.6L9.7,6.3c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l2.3,2.3l-2.3,2.3c-0.4,0.4-0.4,1,0,1.4C8.5,13.9,8.7,14,9,14s0.5-0.1,0.7-0.3l2.3-2.3l2.3,2.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L13.4,10l2.3-2.3C16.1,7.3,16.1,6.7,15.7,6.3z" fill="#FF6711" />
|
||||
</symbol>
|
||||
<symbol id="shareOff" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20,2H4C2.3,2,1,3.3,1,5v10c0,1.7,1.3,3,3,3h7v2H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h8c0.6,0,1-0.4,1-1s-0.4-1-1-1h-3v-2h7c1.7,0,3-1.3,3-3V5C23,3.3,21.7,2,20,2z M21,15c0,0.6-0.4,1-1,1H4c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1V15z" fill="currentColor" />
|
||||
<path d="M12.7,5.9c-0.2-0.2-0.4-0.3-0.7-0.3s-0.5,0.1-0.7,0.3L8.5,8.7c-0.4,0.4-0.4,1,0,1.4c0.4,0.4,1,0.4,1.4,0L11,9v4.4c0,0.6,0.4,1,1,1s1-0.4,1-1V9l1.1,1.1c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L12.7,5.9z" fill="currentColor" />
|
||||
</symbol>
|
||||
<symbol id="leave" viewBox="0 0 24 24">
|
||||
<path d="M9,20H5c-0.3,0-0.5-0.1-0.7-0.3C4.1,19.5,4,19.3,4,19V5c0-0.3,0.1-0.5,0.3-0.7C4.5,4.1,4.7,4,5,4h4c0.6,0,1-0.4,1-1S9.6,2,9,2H5C4.2,2,3.4,2.3,2.9,2.9C2.3,3.4,2,4.2,2,5v14c0,0.8,0.3,1.6,0.9,2.1C3.4,21.7,4.2,22,5,22h4c0.6,0,1-0.4,1-1S9.6,20,9,20z" />
|
||||
<path d="M21.9,12.4C22,12.3,22,12.1,22,12s0-0.3-0.1-0.4c-0.1-0.1-0.1-0.2-0.2-0.3l-5-5c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l3.3,3.3H9c-0.6,0-1,0.4-1,1s0.4,1,1,1h9.6l-3.3,3.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l5-5C21.8,12.6,21.9,12.5,21.9,12.4z" />
|
||||
</symbol>
|
||||
<symbol id="enter" viewBox="0 0 24 24">
|
||||
<path d="M10.6,16.6l3.6-3.6H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h13.2l-3.6-3.6L12,6l6,6l-6,6L10.6,16.6z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12,2c-1.1,0-2,0.9-2,2v1c0,0.6-0.4,1-1,1S8,5.6,8,5V4c0-2.2,1.8-4,4-4h8c2.2,0,4,1.8,4,4v16c0,2.2-1.8,4-4,4 h-8c-2.2,0-4-1.8-4-4v-1c0-0.6,0.4-1,1-1s1,0.4,1,1v1c0,1.1,0.9,2,2,2h8c1.1,0,2-0.9,2-2V4c0-1.1-0.9-2-2-2H12z" />
|
||||
</symbol>
|
||||
<symbol id="record" viewBox="0 0 256 256">
|
||||
<path d="M128,24A104,104,0,1,0,232,128,104.11791,104.11791,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.09957,88.09957,0,0,1,128,216Zm72-88a72,72,0,1,1-72-72A72.08124,72.08124,0,0,1,200,128Z"/>
|
||||
</symbol>
|
||||
<symbol id="stopRecord" viewBox="0 0 256 256">
|
||||
<path d="M128,24A104,104,0,1,0,232,128,104.11791,104.11791,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.09957,88.09957,0,0,1,128,216Zm72-88a72,72,0,1,1-72-72A72.08124,72.08124,0,0,1,200,128Z" fill="#FF6711"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "assets"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"string": {
|
||||
"Office": "Office",
|
||||
"Room": "Room",
|
||||
"RoomType": "Room type",
|
||||
"Share": "Share",
|
||||
"StopShare": "Stop share",
|
||||
"StartVideo": "Start video",
|
||||
"StopVideo": "Stop video",
|
||||
"Mute": "Mute",
|
||||
"UnMute": "Unmute",
|
||||
"LeaveRoom": "Leave room",
|
||||
"Floors": "Floors",
|
||||
"MyOffice": "My office",
|
||||
"EditOffice": "Edit office",
|
||||
"FinalizeEditing": "Done",
|
||||
"Floor": "Floor",
|
||||
"ChangeFloor": "Change floor",
|
||||
"IsKnocking": "{name} is knocking...",
|
||||
"Accept": "Accept",
|
||||
"Decline": "Decline",
|
||||
"EnterRoom": "Enter room",
|
||||
"TeamRoom": "Team room",
|
||||
"MeetingRoom": "Meeting room",
|
||||
"KnockingTo": "Knocking to ...",
|
||||
"Cancel": "Cancel",
|
||||
"AnotherWindowError": "Connection already established in another window. Please utilize that window or reconnect",
|
||||
"Speaker": "Speaker",
|
||||
"Microphone": "Microphone",
|
||||
"Camera": "Camera",
|
||||
"Settings": "Settings",
|
||||
"LoveDescription": "Live Office Virtual Environment",
|
||||
"DefaultDevice": "Default device",
|
||||
"Knock": "Knock required",
|
||||
"Open": "Open",
|
||||
"DND": "Do not disturb",
|
||||
"ChangeAccess": "Change access",
|
||||
"AddAFloor": "Add a floor",
|
||||
"RenameAFloor": "Rename a floor",
|
||||
"StartWithoutVideo": "Always start with camera off",
|
||||
"StartWithMutedMic": "Always start with muted mic",
|
||||
"InvitingYou": "{name} is asking you to join",
|
||||
"YouInivite": "You are inviting to join",
|
||||
"NoiseCancellation": "Noise cancellation",
|
||||
"KnockingLabel": "Knocking",
|
||||
"InivitingLabel": "Invite",
|
||||
"NoiseCancellationNotSupported": "Noise cancellation is not supported in this browser",
|
||||
"Blur": "Background blur",
|
||||
"BlurRadius": "Radius",
|
||||
"BlurTooltip": "Blur feature significantly increases CPU consumption, which may affect overall system performance.",
|
||||
"GuestLink": "Guest link",
|
||||
"CopyGuestLink": "Copy guest link",
|
||||
"Record": "Record",
|
||||
"StopRecord": "Stop record",
|
||||
"LeaveRoomConfirmation": "Are you sure you want to leave the room?",
|
||||
"ServiceNotConfigured": "Service is not configured"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"string": {
|
||||
"Office": "Oficina",
|
||||
"Room": "Sala",
|
||||
"RoomType": "Tipo de sala",
|
||||
"Share": "Compartir",
|
||||
"StopShare": "Detener compartir",
|
||||
"StartVideo": "Iniciar video",
|
||||
"StopVideo": "Detener video",
|
||||
"Mute": "Silenciar",
|
||||
"UnMute": "Activar sonido",
|
||||
"LeaveRoom": "Salir de la sala",
|
||||
"Floors": "Pisos",
|
||||
"MyOffice": "Mi Oficina",
|
||||
"EditOffice": "Editar oficina",
|
||||
"FinalizeEditing": "Hecho",
|
||||
"Floor": "Piso",
|
||||
"ChangeFloor": "Cambiar piso",
|
||||
"IsKnocking": "{name} está tocando...",
|
||||
"Accept": "Aceptar",
|
||||
"Decline": "Rechazar",
|
||||
"EnterRoom": "Entrar a la sala",
|
||||
"MeetingRoom": "Sala de reuniones",
|
||||
"TeamRoom": "Sala de equipo",
|
||||
"KnockingTo": "Estamos tocando a...",
|
||||
"Cancel": "Cancelar",
|
||||
"AnotherWindowError": "Ya estás conectado en otra ventana. Utiliza esa ventana o vuelve a conectarte",
|
||||
"Speaker": "Altavoz",
|
||||
"Microphone": "Micrófono",
|
||||
"Camera": "Cámara",
|
||||
"Settings": "Configuración",
|
||||
"LoveDescription": "Entorno Virtual de Oficina en Vivo",
|
||||
"DefaultDevice": "Dispositivo por defecto",
|
||||
"Knock": "Permiso requerido",
|
||||
"Open": "Abierto",
|
||||
"DND": "No molestar",
|
||||
"ChangeAccess": "Cambiar acceso",
|
||||
"AddAFloor": "Añadir un piso",
|
||||
"RenameAFloor": "Cambiar el nombre de un piso",
|
||||
"StartWithoutVideo": "Siempre empezar sin video",
|
||||
"StartWithMutedMic": "Siempre empezar con el micrófono silenciado",
|
||||
"InvitingYou": "{name} te está invitando a unirte",
|
||||
"YouInivite": "Estás invitando a unirte",
|
||||
"NoiseCancellation": "Cancelación de ruido",
|
||||
"KnockingLabel": "Tocando",
|
||||
"InivitingLabel": "Invitar",
|
||||
"NoiseCancellationNotSupported": "La cancelación de ruido no es compatible con tu navegador",
|
||||
"Blur": "Desenfoque de fondo",
|
||||
"BlurRadius": "Radio de desenfoque",
|
||||
"BlurTooltip": "La función de desenfoque aumenta significativamente el consumo de CPU, lo que puede afectar al rendimiento general del sistema.",
|
||||
"GuestLink": "Enlace de invitado",
|
||||
"CopyGuestLink": "Copiar enlace de invitado",
|
||||
"Record": "Grabar",
|
||||
"StopRecord": "Detener grabación",
|
||||
"LeaveRoomConfirmation": "¿Estás seguro de que quieres salir de la sala?",
|
||||
"ServiceNotConfigured": "El servicio no está configurado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"string": {
|
||||
"Office": "Escritório",
|
||||
"Room": "Sala",
|
||||
"RoomType": "Tipo de sala",
|
||||
"Share": "Compartilhar",
|
||||
"StopShare": "Parar compartilhamento",
|
||||
"StartVideo": "Iniciar vídeo",
|
||||
"StopVideo": "Parar vídeo",
|
||||
"Mute": "Mudo",
|
||||
"UnMute": "Desmutar",
|
||||
"LeaveRoom": "Sair da sala",
|
||||
"Floors": "Andares",
|
||||
"MyOffice": "Meu Escritório",
|
||||
"EditOffice": "Editar escritório",
|
||||
"FinalizeEditing": "Feito",
|
||||
"Floor": "Andar",
|
||||
"ChangeFloor": "Trocar andar",
|
||||
"IsKnocking": "{name} está batendo...",
|
||||
"Accept": "Aceitar",
|
||||
"Decline": "Recusar",
|
||||
"EnterRoom": "Entrar na sala",
|
||||
"TeamRoom": "Sala de equipe",
|
||||
"MeetingRoom": "Sala de reunião",
|
||||
"KnockingTo": "Estamos batendo em...",
|
||||
"Cancel": "Cancelar",
|
||||
"AnotherWindowError": "Você já está conectado em outra janela. Por favor, utilize essa janela ou reconecte-se",
|
||||
"Speaker": "Alto-falante",
|
||||
"Microphone": "Microfone",
|
||||
"Camera": "Câmera",
|
||||
"Settings": "Configurações",
|
||||
"LoveDescription": "Ambiente Virtual de Escritório ao Vivo",
|
||||
"DefaultDevice": "Dispositivo Padrão",
|
||||
"Knock": "Bater",
|
||||
"Open": "Aberto",
|
||||
"DND": "Não Perturbe",
|
||||
"ChangeAccess": "Alterar acesso",
|
||||
"AddAFloor": "Adicionar um piso",
|
||||
"RenameAFloor": "Mudar o nome de um piso",
|
||||
"StartWithoutVideo": "Sempre começar sem vídeo",
|
||||
"StartWithMutedMic": "Sempre começar com o microfone silenciado",
|
||||
"InvitingYou": "{name} está te convidando para entrar",
|
||||
"YouInivite": "Você está convidando para entrar",
|
||||
"NoiseCancellation": "Cancelamento de ruído",
|
||||
"KnockingLabel": "Batendo",
|
||||
"InivitingLabel": "Convidar",
|
||||
"NoiseCancellationNotSupported": "Seu navegador não suporta cancelamento de ruído",
|
||||
"Blur": "Desfoque de fundo",
|
||||
"BlurRadius": "Raio de desfoque",
|
||||
"BlurTooltip": "A função de desfoque aumenta significativamente o consumo da CPU, o que pode afetar o desempenho geral do sistema.",
|
||||
"GuestLink": "Link de convidado",
|
||||
"CopyGuestLink": "Copiar link de convidado",
|
||||
"Record": "Gravar",
|
||||
"StopRecord": "Parar gravação",
|
||||
"LeaveRoomConfirmation": "Tem certeza de que deseja sair da sala?",
|
||||
"ServiceNotConfigured": "O serviço não está configurado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"string": {
|
||||
"Office": "Офис",
|
||||
"Room": "Комната",
|
||||
"RoomType": "Тип комнаты",
|
||||
"Share": "Поделиться",
|
||||
"StopShare": "Прекратить показ",
|
||||
"StartVideo": "Начать видео",
|
||||
"StopVideo": "Остановить видео",
|
||||
"Mute": "Выключить звук",
|
||||
"UnMute": "Включить звук",
|
||||
"LeaveRoom": "Покинуть комнату",
|
||||
"Floors": "Этажи",
|
||||
"MyOffice": "Мой офис",
|
||||
"EditOffice": "Редактировать офис",
|
||||
"FinalizeEditing": "Завершить",
|
||||
"Floor": "Этаж",
|
||||
"ChangeFloor": "Сменить этаж",
|
||||
"IsKnocking": "{name} стучится...",
|
||||
"Accept": "Принять",
|
||||
"Decline": "Отклонить",
|
||||
"EnterRoom": "Войти в комнату",
|
||||
"TeamRoom": "Командная комната",
|
||||
"MeetingRoom": "Переговорная",
|
||||
"KnockingTo": "Стучимся в ...",
|
||||
"Cancel": "Отмена",
|
||||
"AnotherWindowError": "Вы уже подключены в другом окне. Используйте это окно или подключитесь снова",
|
||||
"Speaker": "Динамик",
|
||||
"Microphone": "Микрофон",
|
||||
"Camera": "Камера",
|
||||
"Settings": "Настройки",
|
||||
"LoveDescription": "Виртуальная среда Live Office",
|
||||
"DefaultDevice": "Устройство по умолчанию",
|
||||
"Knock": "Необходимо разрешение",
|
||||
"Open": "Открыто",
|
||||
"DND": "Не беспокоить",
|
||||
"ChangeAccess": "Изменить доступ",
|
||||
"AddAFloor": "Добавить этаж",
|
||||
"RenameAFloor": "Переименовать этаж",
|
||||
"StartWithoutVideo": "Всегда начинать с выключенной камерой",
|
||||
"StartWithMutedMic": "Всегда начинать с выключенным звуком",
|
||||
"InvitingYou": "{name} приглашает вас присоединиться",
|
||||
"YouInivite": "Вы приглашаете присоединиться",
|
||||
"NoiseCancellation": "Подавление шума",
|
||||
"KnockingLabel": "Стучится",
|
||||
"InivitingLabel": "Приглашение",
|
||||
"NoiseCancellationNotSupported": "Подавление шума не поддерживается вашим браузером",
|
||||
"Blur": "Размытие фона",
|
||||
"BlurRadius": "Радиус",
|
||||
"BlurTooltip": "Функция размытия значительно увеличивает потребление ресурсов процессора, что может повлиять на общую производительность системы.",
|
||||
"GuestLink": "Гостевая ссылка",
|
||||
"CopyGuestLink": "Скопировать гостевую ссылку",
|
||||
"Record": "Запись",
|
||||
"StopRecord": "Остановить запись",
|
||||
"LeaveRoomConfirmation": "Вы уверены, что хотите покинуть комнату?",
|
||||
"ServiceNotConfigured": "Сервис не настроен"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@hcengineering/love-assets",
|
||||
"version": "0.6.0",
|
||||
"main": "src/index.ts",
|
||||
"author": "Uberflow Contributors",
|
||||
"template": "@hcengineering/assets-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"build:docs": "",
|
||||
"format": "format src",
|
||||
"build:watch": "compile",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint": "^8.54.0",
|
||||
"prettier": "^3.1.0",
|
||||
"@types/node": "~20.11.16",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/love": "^0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { makeLocalesTest } from '@hcengineering/platform'
|
||||
|
||||
it(
|
||||
'Locales are equale',
|
||||
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { loadMetadata } from '@hcengineering/platform'
|
||||
import love from '@hcengineering/love'
|
||||
|
||||
const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
||||
loadMetadata(love.icon, {
|
||||
Love: `${icons}#office`,
|
||||
SharingEnabled: `${icons}#shareOn`,
|
||||
SharingDisabled: `${icons}#shareOff`,
|
||||
CamEnabled: `${icons}#cameraOn`,
|
||||
CamDisabled: `${icons}#cameraOff`,
|
||||
Cam: `${icons}#camera`,
|
||||
Mic: `${icons}#mic`,
|
||||
MicEnabled: `${icons}#micOn`,
|
||||
MicDisabled: `${icons}#micOff`,
|
||||
LeaveRoom: `${icons}#leave`,
|
||||
EnterRoom: `${icons}#enter`,
|
||||
Open: `${icons}#open`,
|
||||
Knock: `${icons}#knock`,
|
||||
DND: `${icons}#dnd`,
|
||||
Record: `${icons}#record`,
|
||||
StopRecord: `${icons}#stopRecord`
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"types": ["node", "jest"],
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
|
||||
parserOptions: { tsconfigRootDir: __dirname }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"proseWrap": "preserve",
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "ui"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@hcengineering/love-resources",
|
||||
"version": "0.6.0",
|
||||
"main": "src/index.ts",
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile ui",
|
||||
"build:docs": "api-extractor run --local",
|
||||
"format": "format src",
|
||||
"svelte-check": "do-svelte-check",
|
||||
"_phase:svelte-check": "do-svelte-check",
|
||||
"build:watch": "compile ui",
|
||||
"_phase:build": "compile ui",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte-loader": "^3.2.0",
|
||||
"sass": "^1.53.0",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"eslint": "^8.54.0",
|
||||
"prettier": "^3.1.0",
|
||||
"svelte-check": "^3.6.9",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"svelte-eslint-parser": "^0.33.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"svelte": "^4.2.12",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/login": "^0.6.8",
|
||||
"@hcengineering/panel": "^0.6.15",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"livekit-client": "^2.0.10",
|
||||
"@livekit/krisp-noise-filter": "~0.2.1",
|
||||
"@livekit/track-processors": "~0.3.1",
|
||||
"@hcengineering/view": "^0.6.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { get } from 'svelte/store'
|
||||
import {
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
$isCurrentInstanceConnected,
|
||||
isMicEnabled,
|
||||
isSharingEnabled,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare
|
||||
} from './utils'
|
||||
import { getCurrentLocation, location } from '@hcengineering/ui'
|
||||
|
||||
let key = 'love' + getCurrentLocation().path?.[2]
|
||||
|
||||
let bc: BroadcastChannel | undefined
|
||||
|
||||
location.subscribe((newLocation) => {
|
||||
if (newLocation.path?.[2] !== undefined && 'BroadcastChannel' in window) {
|
||||
const newKey = 'love' + newLocation.path[2]
|
||||
if (key !== newKey) {
|
||||
key = newKey
|
||||
bc = new BroadcastChannel(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
type BroadcastMessage =
|
||||
| BroadcastConnectMessage
|
||||
| BroadcastChangeHostMessage
|
||||
| BroadcastStatusAskMessage
|
||||
| BroadcastSetMessage
|
||||
| BroadcastStatusMessage
|
||||
|
||||
interface BroadcastConnectMessage {
|
||||
type: 'connect'
|
||||
value: boolean
|
||||
}
|
||||
|
||||
interface BroadcastChangeHostMessage {
|
||||
type: 'change_host'
|
||||
}
|
||||
|
||||
interface BroadcastStatusAskMessage {
|
||||
type: 'status_ask'
|
||||
}
|
||||
|
||||
interface BroadcastSetMessage {
|
||||
type: 'set_mic' | 'set_cam' | 'set_share'
|
||||
value: boolean
|
||||
}
|
||||
|
||||
interface BroadcastStatusMessage {
|
||||
type: 'mic' | 'cam' | 'share'
|
||||
value: boolean
|
||||
}
|
||||
|
||||
export function sendMessage (req: BroadcastMessage): void {
|
||||
bc?.postMessage(req)
|
||||
}
|
||||
|
||||
if (bc !== undefined) {
|
||||
bc.onmessage = async (e: MessageEvent<BroadcastMessage>) => {
|
||||
if (e.data.type === 'set_mic') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setMic(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_cam') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setCam(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'set_share') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await setShare(e.data.value)
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'share') {
|
||||
isSharingEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'mic') {
|
||||
isMicEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'cam') {
|
||||
isCameraEnabled.set(e.data.value)
|
||||
}
|
||||
if (e.data.type === 'status_ask') {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
sendMessage({ type: 'mic', value: get(isMicEnabled) })
|
||||
sendMessage({ type: 'cam', value: get(isCameraEnabled) })
|
||||
sendMessage({ type: 'share', value: get(isSharingEnabled) })
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'connect') {
|
||||
if (!$isCurrentInstanceConnected) {
|
||||
isConnected.set(e.data.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage({ type: 'status_ask' })
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import { Avatar, CombineAvatars, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { Invite, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
export let invites: Invite[]
|
||||
|
||||
$: persons = invites.map((p) => p.target)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function cancel () {
|
||||
for (const invite of invites) {
|
||||
if (invite.status === RequestStatus.Pending) {
|
||||
await client.remove(invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup flex-col-center">
|
||||
<span class="title">
|
||||
<Label label={love.string.YouInivite} />
|
||||
</span>
|
||||
<div class="p-4">
|
||||
<CombineAvatars _class={contact.class.Person} size={'large'} items={persons} limit={5} />
|
||||
</div>
|
||||
<div class="flex-row-center p-1 w-full">
|
||||
<Button label={love.string.Cancel} width={'100%'} on:click={cancel} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
padding-top: 1rem;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--caption-color);
|
||||
font-size: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { Class, Data, Ref } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, DropdownIntlItem } from '@hcengineering/ui'
|
||||
import { Floor, Office, Room, RoomAccess, RoomType, getFreePosition } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { rooms } from '../stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let floor: Ref<Floor>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type TypeIds = 'meetingRoom' | 'teamRoom' | 'office'
|
||||
|
||||
interface Item extends DropdownIntlItem {
|
||||
id: TypeIds
|
||||
type: RoomType
|
||||
_class: Ref<Class<Room>>
|
||||
access: RoomAccess
|
||||
}
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
id: 'meetingRoom',
|
||||
label: love.string.MeetingRoom,
|
||||
type: RoomType.Video,
|
||||
_class: love.class.Room,
|
||||
access: RoomAccess.Open
|
||||
},
|
||||
{
|
||||
id: 'teamRoom',
|
||||
label: love.string.TeamRoom,
|
||||
type: RoomType.Audio,
|
||||
_class: love.class.Room,
|
||||
access: RoomAccess.Open
|
||||
},
|
||||
{
|
||||
id: 'office',
|
||||
label: love.string.Office,
|
||||
type: RoomType.Audio,
|
||||
_class: love.class.Office,
|
||||
access: RoomAccess.Knock
|
||||
}
|
||||
] as const
|
||||
|
||||
async function createRoom (value: TypeIds): Promise<void> {
|
||||
const val = items.find((i) => i.id === value)
|
||||
if (val === undefined) return
|
||||
const client = getClient()
|
||||
const floorRooms = $rooms.filter((r) => r.floor === floor)
|
||||
const pos = getFreePosition(floorRooms, 2, 1)
|
||||
const data: Data<Room> = {
|
||||
floor,
|
||||
name: val._class === love.class.Office ? '' : await translate(val.label, {}),
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: 2,
|
||||
height: 1,
|
||||
type: val.type,
|
||||
access: val.access
|
||||
}
|
||||
if (val._class === love.class.Office) {
|
||||
;(data as Data<Office>).person = null
|
||||
}
|
||||
await client.createDoc(val._class, love.space.Rooms, data)
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4 flex-gap-2">
|
||||
{#each items as item}
|
||||
<div>
|
||||
<Button label={item.label} on:click={() => createRoom(item.id)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { DropdownLabels, DropdownTextItem, Label, Progress, Toggle } from '@hcengineering/ui'
|
||||
import { Room } from 'livekit-client'
|
||||
import love from '../plugin'
|
||||
import { myPreferences } from '../stores'
|
||||
import { blurProcessor, getActive, lk, selectedCamId, updateBlurRadius } from '../utils'
|
||||
|
||||
void Room.getLocalDevices().then(async (devices) => {
|
||||
devices.forEach((device) => {
|
||||
if (device.kind === 'videoinput') {
|
||||
cameras.push({ label: device.label, id: device.deviceId })
|
||||
}
|
||||
})
|
||||
if (cameras.length === 0) {
|
||||
cameras.push({ label: await translate(love.string.DefaultDevice, {}), id: 'default' })
|
||||
}
|
||||
cameras = cameras
|
||||
})
|
||||
|
||||
let cameras: DropdownTextItem[] = []
|
||||
|
||||
$: blurRadius = $myPreferences?.blurRadius ?? 0
|
||||
$: activeCamera = getActive(cameras, lk.getActiveDevice('videoinput'), localStorage.getItem(selectedCamId))
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4 grid">
|
||||
<Label label={love.string.Camera} />
|
||||
<DropdownLabels
|
||||
items={cameras}
|
||||
placeholder={love.string.Camera}
|
||||
enableSearch={false}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
disabled={cameras.length === 0}
|
||||
selected={activeCamera?.id}
|
||||
on:selected={async (item) => {
|
||||
if (item.detail != null && item.detail !== 'default') {
|
||||
await lk.switchActiveDevice('videoinput', item.detail)
|
||||
localStorage.setItem(selectedCamId, item.detail)
|
||||
activeCamera = cameras.find((p) => p.id === item.detail) ?? cameras[0]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if blurProcessor !== undefined}
|
||||
<Label label={love.string.Blur} />
|
||||
<Toggle
|
||||
showTooltip={{ label: love.string.BlurTooltip }}
|
||||
on={blurRadius >= 0.5}
|
||||
on:change={(e) => {
|
||||
updateBlurRadius(e.detail ? 0.5 : 0)
|
||||
}}
|
||||
/>
|
||||
{#if blurRadius >= 0.5}
|
||||
<Label label={love.string.BlurRadius} />
|
||||
<Progress
|
||||
editable
|
||||
max={10}
|
||||
min={0.5}
|
||||
value={blurRadius}
|
||||
on:change={(e) => {
|
||||
const value = Math.round(e.detail * 2) / 2
|
||||
updateBlurRadius(value)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
row-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { copyTextToClipboard, getClient } from '@hcengineering/presentation'
|
||||
import { IconUpOutline, ModernButton, SplitButton, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { Room, RoomType, isOffice, roomAccessIcon } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, myInfo, myOffice } from '../stores'
|
||||
import {
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicEnabled,
|
||||
isRecording,
|
||||
isRecordingAvailable,
|
||||
isSharingEnabled,
|
||||
leaveRoom,
|
||||
record,
|
||||
screenSharing,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare
|
||||
} from '../utils'
|
||||
import CamSettingPopup from './CamSettingPopup.svelte'
|
||||
import MicSettingPopup from './MicSettingPopup.svelte'
|
||||
import RoomAccessPopup from './RoomAccessPopup.svelte'
|
||||
|
||||
export let room: Room
|
||||
|
||||
let allowCam: boolean = false
|
||||
const allowShare: boolean = true
|
||||
let allowLeave: boolean = false
|
||||
|
||||
let barWidth: number
|
||||
$: noLabel = barWidth < 900
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
|
||||
async function changeMute (): Promise<void> {
|
||||
await setMic(!$isMicEnabled)
|
||||
}
|
||||
|
||||
async function changeCam (): Promise<void> {
|
||||
await setCam(!$isCameraEnabled)
|
||||
}
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
await setShare(!$isSharingEnabled)
|
||||
}
|
||||
|
||||
async function leave (): Promise<void> {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
}
|
||||
|
||||
function micSettings (e: MouseEvent): void {
|
||||
showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
function camSettings (e: MouseEvent): void {
|
||||
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
function setAccess (e: MouseEvent): void {
|
||||
if (isOffice(room) && room.person !== me) return
|
||||
showPopup(RoomAccessPopup, { room }, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
async function copyGuestLink (): Promise<void> {
|
||||
const getLink = await getResource(login.function.GetInviteLink)
|
||||
const link = await getLink(24 * 30, '', -1, AccountRole.Guest)
|
||||
await copyTextToClipboard(link)
|
||||
linkCopied = true
|
||||
clearTimeout(linkTimeout)
|
||||
linkTimeout = setTimeout(() => {
|
||||
linkCopied = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
let linkCopied: boolean = false
|
||||
let linkTimeout: any | undefined = undefined
|
||||
|
||||
const me = (getCurrentAccount() as PersonAccount).person
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleVideo })?.[0]?.keyBinding
|
||||
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleMic })?.[0]?.keyBinding
|
||||
</script>
|
||||
|
||||
<div bind:clientWidth={barWidth} class="bar w-full flex-center flex-gap-2 flex-no-shrink">
|
||||
{#if room._id !== love.ids.Reception}
|
||||
<ModernButton
|
||||
icon={roomAccessIcon[room.access]}
|
||||
tooltip={{ label: love.string.ChangeAccess }}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
disabled={isOffice(room) && room.person !== me}
|
||||
on:click={setAccess}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isConnected}
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
showTooltip={{ label: $isMicEnabled ? love.string.Mute : love.string.UnMute, keys: micKeys }}
|
||||
action={changeMute}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={micSettings}
|
||||
separate
|
||||
/>
|
||||
{#if allowCam}
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
showTooltip={{ label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo, keys: camKeys }}
|
||||
disabled={!$isConnected}
|
||||
action={changeCam}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={camSettings}
|
||||
separate
|
||||
/>
|
||||
{/if}
|
||||
{#if allowShare}
|
||||
<ModernButton
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
tooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
on:click={changeShare}
|
||||
/>
|
||||
{/if}
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isRecordingAvailable}
|
||||
<ModernButton
|
||||
icon={$isRecording ? love.icon.StopRecord : love.icon.Record}
|
||||
tooltip={{ label: $isRecording ? love.string.StopRecord : love.string.Record }}
|
||||
disabled={!$isConnected}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
on:click={() => record(room)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="bar__left-panel flex-gap-2 flex-center">
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User)}
|
||||
<ModernButton
|
||||
icon={view.icon.Copy}
|
||||
tooltip={{ label: !linkCopied ? love.string.CopyGuestLink : view.string.Copied }}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
on:click={copyGuestLink}
|
||||
/>
|
||||
{/if}
|
||||
{#if allowLeave}
|
||||
<ModernButton
|
||||
icon={love.icon.LeaveRoom}
|
||||
label={noLabel ? undefined : love.string.LeaveRoom}
|
||||
tooltip={{ label: love.string.LeaveRoom }}
|
||||
kind={'negative'}
|
||||
size={'large'}
|
||||
on:click={leave}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.bar {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
|
||||
&__left-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,488 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PersonAccount, formatName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { IdMap, Ref, getCurrentAccount, toIdMap } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
ActionIcon,
|
||||
Label,
|
||||
Location,
|
||||
PopupResult,
|
||||
closePopup,
|
||||
eventToHTMLElement,
|
||||
location,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import {
|
||||
Floor,
|
||||
Invite,
|
||||
JoinRequest,
|
||||
Office,
|
||||
ParticipantInfo,
|
||||
RequestStatus,
|
||||
Room,
|
||||
RoomType,
|
||||
isOffice,
|
||||
loveId
|
||||
} from '@hcengineering/love'
|
||||
import { onDestroy } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import {
|
||||
activeFloor,
|
||||
activeInvites,
|
||||
currentRoom,
|
||||
floors,
|
||||
infos,
|
||||
myInfo,
|
||||
myInvites,
|
||||
myOffice,
|
||||
myRequests,
|
||||
rooms
|
||||
} from '../stores'
|
||||
import {
|
||||
connectRoom,
|
||||
disconnect,
|
||||
getRoomName,
|
||||
invite,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isCurrentInstanceConnected,
|
||||
isMicEnabled,
|
||||
isSharingEnabled,
|
||||
leaveRoom,
|
||||
screenSharing,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare
|
||||
} from '../utils'
|
||||
import ActiveInvitesPopup from './ActiveInvitesPopup.svelte'
|
||||
import CamSettingPopup from './CamSettingPopup.svelte'
|
||||
import FloorPopup from './FloorPopup.svelte'
|
||||
import InvitePopup from './InvitePopup.svelte'
|
||||
import MicSettingPopup from './MicSettingPopup.svelte'
|
||||
import RequestPopup from './RequestPopup.svelte'
|
||||
import RequestingPopup from './RequestingPopup.svelte'
|
||||
import RoomPopup from './RoomPopup.svelte'
|
||||
import VideoPopup from './VideoPopup.svelte'
|
||||
|
||||
let allowCam: boolean = false
|
||||
let allowLeave: boolean = false
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: allowLeave = $myInfo !== undefined && $myInfo.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
|
||||
async function changeMute (): Promise<void> {
|
||||
if (!$isConnected || $currentRoom?.type === RoomType.Reception) return
|
||||
await setMic(!$isMicEnabled)
|
||||
}
|
||||
|
||||
async function changeCam (): Promise<void> {
|
||||
if (!$isConnected || !allowCam) return
|
||||
await setCam(!$isCameraEnabled)
|
||||
}
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
if (!$isConnected) return
|
||||
await setShare(!$isSharingEnabled)
|
||||
}
|
||||
|
||||
async function leave (): Promise<void> {
|
||||
showPopup(MessageBox, {
|
||||
label: love.string.LeaveRoom,
|
||||
message: love.string.LeaveRoomConfirmation,
|
||||
action: async () => {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const me = (getCurrentAccount() as PersonAccount).person
|
||||
|
||||
interface ActiveRoom extends Room {
|
||||
participants: ParticipantInfo[]
|
||||
}
|
||||
|
||||
function getActiveRooms (rooms: Room[], infos: ParticipantInfo[]): ActiveRoom[] {
|
||||
const roomMap = toIdMap(rooms)
|
||||
const map: IdMap<ActiveRoom> = new Map()
|
||||
for (const info of infos) {
|
||||
if (info.room === love.ids.Reception) continue
|
||||
const room = roomMap.get(info.room)
|
||||
if (room === undefined) continue
|
||||
// temprory disabled check for floor
|
||||
// if (room.floor !== selectedFloor?._id) continue
|
||||
const _id = room._id as Ref<ActiveRoom>
|
||||
const active = map.get(_id) ?? { ...room, _id, participants: [] }
|
||||
active.participants.push(info)
|
||||
map.set(_id, active)
|
||||
}
|
||||
const arr = Array.from(map.values()).filter(
|
||||
(r) => !isOffice(r) || r.participants.length > 1 || r.person !== r.participants[0]?.person
|
||||
)
|
||||
return arr
|
||||
}
|
||||
|
||||
let selectedFloor: Floor | undefined = $floors.find((f) => f._id === $activeFloor)
|
||||
$: selectedFloor = $floors.find((f) => f._id === $activeFloor)
|
||||
|
||||
$: activeRooms = getActiveRooms($rooms, $infos)
|
||||
|
||||
function selectFloor (): void {
|
||||
showPopup(FloorPopup, { selectedFloor }, myOfficeElement, (res) => {
|
||||
if (res === undefined) return
|
||||
selectedFloor = $floors.find((p) => p._id === res)
|
||||
})
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
let requests: JoinRequest[] = []
|
||||
query.query(love.class.JoinRequest, { status: RequestStatus.Pending }, (res) => {
|
||||
requests = res
|
||||
})
|
||||
|
||||
let activeRequest: JoinRequest | undefined = undefined
|
||||
const joinRequestCategory = 'joinRequest'
|
||||
function checkRequests (requests: JoinRequest[], $myInfo: ParticipantInfo | undefined): void {
|
||||
if (activeRequest !== undefined) {
|
||||
// try to find active request, if it not exists close popup
|
||||
if (requests.find((r) => r._id === activeRequest?._id) === undefined) {
|
||||
closePopup(joinRequestCategory)
|
||||
activeRequest = undefined
|
||||
}
|
||||
}
|
||||
if (activeRequest === undefined) {
|
||||
activeRequest = requests.find((r) => r.room === $myInfo?.room)
|
||||
if (activeRequest !== undefined) {
|
||||
showPopup(RequestPopup, { request: activeRequest }, myOfficeElement, undefined, undefined, {
|
||||
category: joinRequestCategory,
|
||||
overlay: false,
|
||||
fixed: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const myJoinRequestCategory = 'MyJoinRequest'
|
||||
let myRequestsPopup: PopupResult | undefined = undefined
|
||||
|
||||
function checkMyRequests (requests: JoinRequest[]): void {
|
||||
if (requests.length > 0) {
|
||||
if (myRequestsPopup === undefined) {
|
||||
myRequestsPopup = showPopup(RequestingPopup, { request: requests[0] }, myOfficeElement, undefined, undefined, {
|
||||
category: myJoinRequestCategory,
|
||||
overlay: false,
|
||||
fixed: true
|
||||
})
|
||||
}
|
||||
} else if (myRequestsPopup !== undefined) {
|
||||
myRequestsPopup.close()
|
||||
myRequestsPopup = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: checkMyRequests($myRequests)
|
||||
|
||||
let myOfficeElement: HTMLDivElement
|
||||
|
||||
$: checkRequests(requests, $myInfo)
|
||||
|
||||
function openRoom (ev: MouseEvent, room: Room): void {
|
||||
showPopup(RoomPopup, { room }, ev.currentTarget as HTMLElement)
|
||||
}
|
||||
|
||||
let activeInvite: Invite | undefined = undefined
|
||||
const inviteCategory = 'inviteReq'
|
||||
function checkInvites (invites: Invite[]): void {
|
||||
if (activeInvite !== undefined) {
|
||||
// try to find active request, if it not exists close popup
|
||||
if (invites.find((r) => r._id === activeInvite?._id) === undefined) {
|
||||
closePopup(inviteCategory)
|
||||
activeInvite = undefined
|
||||
}
|
||||
}
|
||||
if (activeInvite === undefined) {
|
||||
activeInvite = invites[0]
|
||||
if (activeInvite !== undefined) {
|
||||
showPopup(InvitePopup, { invite: activeInvite }, myOfficeElement, undefined, undefined, {
|
||||
category: inviteCategory,
|
||||
overlay: false,
|
||||
fixed: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: checkInvites($myInvites)
|
||||
|
||||
async function checkOwnRoomConnection (
|
||||
infos: ParticipantInfo[],
|
||||
myInfo: ParticipantInfo | undefined,
|
||||
myOffice: Office | undefined,
|
||||
isConnected: boolean
|
||||
): Promise<void> {
|
||||
if (myOffice !== undefined) {
|
||||
if (myInfo !== undefined && myInfo.room === myOffice._id) {
|
||||
const filtered = infos.filter((p) => p.room === myOffice._id && p.person !== myInfo.person)
|
||||
if (filtered.length === 0) {
|
||||
if (isConnected) {
|
||||
await disconnect()
|
||||
}
|
||||
} else if (!isConnected) {
|
||||
const person = $personByIdStore.get(me)
|
||||
if (person === undefined) return
|
||||
await connectRoom(0, 0, myInfo, person, myOffice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: checkOwnRoomConnection($infos, $myInfo, $myOffice, $isConnected)
|
||||
|
||||
const myInvitesCategory = 'myInvites'
|
||||
|
||||
let myInvitesPopup: PopupResult | undefined = undefined
|
||||
|
||||
function checkActiveInvites (invites: Invite[]): void {
|
||||
if (invites.length > 0) {
|
||||
if (myInvitesPopup === undefined) {
|
||||
myInvitesPopup = showPopup(ActiveInvitesPopup, { invites }, myOfficeElement, undefined, undefined, {
|
||||
category: myInvitesCategory,
|
||||
overlay: false,
|
||||
fixed: true
|
||||
})
|
||||
}
|
||||
} else if (myInvitesPopup !== undefined) {
|
||||
myInvitesPopup.close()
|
||||
myInvitesPopup = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: reception = $rooms.find((f) => f._id === love.ids.Reception)
|
||||
|
||||
$: receptionParticipants = $infos.filter((p) => p.room === love.ids.Reception)
|
||||
|
||||
$: checkActiveInvites($activeInvites)
|
||||
|
||||
function micSettings (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
function camSettings (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
const videoPopupCategory = 'videoPopup'
|
||||
let videoPopup: PopupResult | undefined = undefined
|
||||
|
||||
function checkActiveVideo (loc: Location, video: boolean, room: Ref<Room> | undefined): void {
|
||||
if (room === undefined) return
|
||||
if (loc.path[2] !== loveId && video) {
|
||||
if (videoPopup !== undefined) return
|
||||
videoPopup = showPopup(
|
||||
VideoPopup,
|
||||
{
|
||||
room
|
||||
},
|
||||
'movable',
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
category: videoPopupCategory,
|
||||
overlay: false,
|
||||
fixed: true,
|
||||
refId: videoPopupCategory
|
||||
}
|
||||
)
|
||||
} else if (videoPopup) {
|
||||
videoPopup.close()
|
||||
videoPopup = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: checkActiveVideo(
|
||||
$location,
|
||||
$isCurrentInstanceConnected && ($currentRoom?.type === RoomType.Video || ($screenSharing && !isSharingEnabled)),
|
||||
$currentRoom?._id
|
||||
)
|
||||
|
||||
$: joined = activeRooms.filter((r) => $myInfo?.room === r._id)
|
||||
|
||||
onDestroy(() => {
|
||||
closePopup(myInvitesCategory)
|
||||
closePopup(inviteCategory)
|
||||
closePopup(joinRequestCategory)
|
||||
closePopup(myJoinRequestCategory)
|
||||
closePopup(videoPopupCategory)
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleVideo })?.[0]?.keyBinding
|
||||
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleMic })?.[0]?.keyBinding
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="container main flex-row-center flex-gap-2" bind:this={myOfficeElement} on:click={selectFloor}>
|
||||
{#if selectedFloor}
|
||||
<Label label={love.string.Floor} />
|
||||
<span class="label overflow-label">
|
||||
{selectedFloor?.name}
|
||||
</span>
|
||||
{/if}
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
size={'small'}
|
||||
action={changeMute}
|
||||
on:contextmenu={micSettings}
|
||||
disabled={!$isConnected}
|
||||
keys={micKeys}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={!$isConnected || !allowCam
|
||||
? love.icon.Cam
|
||||
: $isCameraEnabled
|
||||
? love.icon.CamEnabled
|
||||
: love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
size={'small'}
|
||||
action={changeCam}
|
||||
on:contextmenu={camSettings}
|
||||
disabled={!$isConnected || !allowCam}
|
||||
keys={camKeys}
|
||||
/>
|
||||
{#if $isConnected}
|
||||
<ActionIcon
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
label={$isSharingEnabled ? love.string.StopShare : love.string.Share}
|
||||
disabled={$screenSharing && !$isSharingEnabled}
|
||||
size={'small'}
|
||||
action={changeShare}
|
||||
/>
|
||||
{/if}
|
||||
{#if allowLeave}
|
||||
<ActionIcon
|
||||
icon={love.icon.LeaveRoom}
|
||||
iconProps={{ color: '#FF6711' }}
|
||||
label={love.string.LeaveRoom}
|
||||
size={'small'}
|
||||
action={leave}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if activeRooms.length > 0}
|
||||
<div class="divider" />
|
||||
{#each activeRooms as active}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="container flex-row-center"
|
||||
class:active={joined.find((r) => r._id === active._id)}
|
||||
on:click={(ev) => {
|
||||
openRoom(ev, active)
|
||||
}}
|
||||
>
|
||||
<div class="mr-2 overflow-label">{getRoomName(active, $personByIdStore)}</div>
|
||||
<div class="flex-row-center avatars">
|
||||
{#each active.participants as participant}
|
||||
<div use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}>
|
||||
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if reception && receptionParticipants.length > 0}
|
||||
<div class="divider" />
|
||||
<div class="container flex-row-center flex-gap-2">
|
||||
<div>{getRoomName(reception, $personByIdStore)}</div>
|
||||
<div class="flex-row-center avatars">
|
||||
{#each receptionParticipants as participant (participant._id)}
|
||||
<div
|
||||
use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}
|
||||
on:click={() => {
|
||||
if (myInfo) {
|
||||
invite(participant.person, $myInfo?.room)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
|
||||
height: 1.625rem;
|
||||
font-weight: 500;
|
||||
background-color: var(--theme-button-pressed);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
&.main {
|
||||
order: -3;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
& + .divider {
|
||||
order: -2;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
border-color: var(--theme-navpanel-divider);
|
||||
}
|
||||
&.active {
|
||||
order: -1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
|
||||
background-color: var(--highlight-select);
|
||||
border-color: var(--highlight-select-border);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-select-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1.5rem;
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Floor } from '@hcengineering/love'
|
||||
|
||||
export let id: Ref<Floor> | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let name: string = ''
|
||||
$: if (id !== undefined) {
|
||||
client.findOne(love.class.Floor, { _id: id }).then((res) => {
|
||||
name = res?.name ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
async function createFloor (): Promise<void> {
|
||||
await client.createDoc(love.class.Floor, love.space.Rooms, { name })
|
||||
}
|
||||
async function updateFloor (): Promise<void> {
|
||||
if (id === undefined) return
|
||||
await client.updateDoc(love.class.Floor, love.space.Rooms, id, { name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={love.string.Floor}
|
||||
okAction={() => {
|
||||
if (id === undefined) createFloor()
|
||||
else updateFloor()
|
||||
}}
|
||||
okLabel={id === undefined ? love.string.AddAFloor : love.string.RenameAFloor}
|
||||
canSave={name.trim().length > 0}
|
||||
on:close={() => dispatch('close')}
|
||||
>
|
||||
<EditBox placeholder={love.string.Floor} bind:value={name} kind={'large-style'} autoFocus focusIndex={1} />
|
||||
</Card>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import { Breadcrumb, Header, IconEdit, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
import { Floor, ParticipantInfo, Room } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import lovePlg from '../plugin'
|
||||
import { currentRoom, floors, infos } from '../stores'
|
||||
import { calculateFloorSize } from '../utils'
|
||||
import ControlBar from './ControlBar.svelte'
|
||||
import FloorGrid from './FloorGrid.svelte'
|
||||
import RoomPreview from './RoomPreview.svelte'
|
||||
|
||||
export let rooms: Room[] = []
|
||||
export let floor: Ref<Floor>
|
||||
export let visibleNav: boolean
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let floorContainer: HTMLDivElement
|
||||
|
||||
$: selectedFloor = $floors.filter((fl) => fl._id === floor)[0]
|
||||
|
||||
function getInfo (room: Ref<Room>, info: ParticipantInfo[]): ParticipantInfo[] {
|
||||
return info.filter((p) => p.room === room)
|
||||
}
|
||||
|
||||
const me = getCurrentAccount()
|
||||
let editable: boolean = false
|
||||
$: editable = hasAccountRole(me, AccountRole.Maintainer)
|
||||
$: rows = calculateFloorSize(rooms) - 1
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent">
|
||||
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
|
||||
<Breadcrumb title={selectedFloor?.name ?? ''} size={'large'} isCurrent />
|
||||
<svelte:fragment slot="actions">
|
||||
{#if editable}
|
||||
<ModernButton
|
||||
icon={IconEdit}
|
||||
label={lovePlg.string.EditOffice}
|
||||
size={'small'}
|
||||
on:click={() => dispatch('configure')}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Header>
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller padding={'1rem'} bottomPadding={'4rem'} horizontal>
|
||||
<FloorGrid bind:floorContainer {rows} preview>
|
||||
{#each rooms as room}
|
||||
<RoomPreview {room} info={getInfo(room._id, $infos)} />
|
||||
{/each}
|
||||
</FloorGrid>
|
||||
</Scroller>
|
||||
</div>
|
||||
{#if $currentRoom}
|
||||
<ControlBar room={$currentRoom} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,371 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
Breadcrumb,
|
||||
ButtonIcon,
|
||||
ModernButton,
|
||||
Header,
|
||||
IconAdd,
|
||||
Scroller,
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { Floor, GRID_WIDTH, Room, getFreeSpace } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { floors, lockedRoom } from '../stores'
|
||||
import { FloorSize, RGBAColor, ResizeInitParams, RoomSide, shadowError, shadowNormal } from '../types'
|
||||
import { calculateFloorSize } from '../utils'
|
||||
import AddRoomPopup from './AddRoomPopup.svelte'
|
||||
import FloorGrid from './FloorGrid.svelte'
|
||||
import RoomConfigure from './RoomConfigure.svelte'
|
||||
import lovePlg from '../plugin'
|
||||
import { Contact } from '@hcengineering/contact'
|
||||
|
||||
export let rooms: Room[] = []
|
||||
export let floor: Ref<Floor>
|
||||
export let visibleNav: boolean
|
||||
export let excludedPersons: Ref<Contact>[] = []
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let divScroll: HTMLElement
|
||||
let resizeInitParams: ResizeInitParams | undefined = undefined
|
||||
let floorContainer: HTMLDivElement
|
||||
let floorRect: DOMRect
|
||||
const floorSize: FloorSize = {
|
||||
cols: GRID_WIDTH + 2,
|
||||
rows: 5,
|
||||
width: 0,
|
||||
height: 0,
|
||||
cellSize: 4,
|
||||
cellTop: 2.5,
|
||||
cellRound: 0.75
|
||||
}
|
||||
const roomsConf: RoomConfigure[] = []
|
||||
let block: boolean = false
|
||||
let lockedID: number = -1
|
||||
let locked: { room: Room, size: DOMRect, map: boolean[][], side?: RoomSide } | undefined = undefined
|
||||
let dragged: { x: number, y: number, offsetX: number, offsetY: number } | undefined = undefined
|
||||
let dragRoom: RoomConfigure
|
||||
let dragShadow: boolean = false
|
||||
let oldX: number = -1
|
||||
let oldY: number = -1
|
||||
|
||||
let cursor: string = 'default'
|
||||
$: document.body.style.cursor = cursor
|
||||
|
||||
async function updateRoom (id: Ref<Room>): Promise<void> {
|
||||
if (locked !== undefined && resizeInitParams !== undefined) {
|
||||
const room = rooms.find((r) => r._id === id)
|
||||
if (room === undefined) {
|
||||
return
|
||||
}
|
||||
const update: DocumentUpdate<Room> = {}
|
||||
if (room.x !== resizeInitParams.x) {
|
||||
update.x = room.x
|
||||
}
|
||||
if (room.y !== resizeInitParams.y) {
|
||||
update.y = room.y
|
||||
}
|
||||
if (room.width !== resizeInitParams.width) {
|
||||
update.width = room.width
|
||||
}
|
||||
if (room.height !== resizeInitParams.height) {
|
||||
update.height = room.height
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
await client.update(room, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addRoom (e: MouseEvent): void {
|
||||
showPopup(AddRoomPopup, { floor }, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
$: selectedFloor = $floors.filter((fl) => fl._id === floor)[0]
|
||||
|
||||
const setShadowColor = (color: RGBAColor): void => {
|
||||
const { r, g, b, a } = color
|
||||
if (dragged && dragRoom) dragRoom.setShadowColor(r, g, b, a)
|
||||
else if (roomsConf[lockedID]) roomsConf[lockedID].setShadowColor(r, g, b, a)
|
||||
}
|
||||
|
||||
function startDragRoom (room: Room, size: DOMRect, n: number): void {
|
||||
if (room === undefined || size === undefined) return
|
||||
const map: boolean[][] = getFreeSpace(rooms, room, true)
|
||||
locked = { room, size, map }
|
||||
dragShadow = false
|
||||
lockedID = n
|
||||
divScroll.addEventListener('mousemove', dragMouseMove)
|
||||
window.addEventListener('mouseup', docMouseUp)
|
||||
}
|
||||
|
||||
function dragMouseMove (e: MouseEvent): void {
|
||||
if ($lockedRoom === '' || block || dragged === undefined || locked === undefined) return
|
||||
block = true
|
||||
if (!dragShadow && dragged && dragRoom) {
|
||||
setShadowColor(shadowNormal)
|
||||
dragRoom.setShadow(0, 0, 2, 2)
|
||||
dragShadow = true
|
||||
}
|
||||
dragged.x = e.clientX - floorRect.x - dragged.offsetX + floorSize.cellRound
|
||||
dragged.y = e.clientY - floorRect.y - dragged.offsetY + floorSize.cellRound
|
||||
let newX: number = Math.round((dragged.x - floorSize.cellRound) / floorSize.cellSize)
|
||||
let newY: number = Math.round((dragged.y - floorSize.cellRound) / floorSize.cellSize)
|
||||
newX = newX < 1 ? 0 : newX + locked.room.width > GRID_WIDTH ? GRID_WIDTH - locked.room.width : newX - 1
|
||||
newY = newY < 1 ? 0 : newY - 1
|
||||
if (oldX !== newX || oldY !== newY) {
|
||||
let checkFree: boolean = true
|
||||
for (let y = newY; y < newY + locked.room.height; y++) {
|
||||
if (locked.map[y] === undefined) locked.map[y] = new Array(GRID_WIDTH).fill(true)
|
||||
for (let x = newX; x < newX + locked.room.width; x++) {
|
||||
if (locked.map[y][x] !== undefined && !locked.map[y][x]) checkFree = false
|
||||
}
|
||||
}
|
||||
if (checkFree) {
|
||||
locked.room.x = newX
|
||||
locked.room.y = newY
|
||||
setShadowColor(shadowNormal)
|
||||
} else {
|
||||
setShadowColor(shadowError)
|
||||
}
|
||||
oldX = newX
|
||||
oldY = newY
|
||||
}
|
||||
block = false
|
||||
}
|
||||
|
||||
function startResizeRoom (room: Room, size: DOMRect, side: RoomSide, n: number): void {
|
||||
if (room === undefined || size === undefined || side === undefined) return
|
||||
const map: boolean[][] = getFreeSpace(rooms, room)
|
||||
locked = { room, size, map, side }
|
||||
lockedID = n
|
||||
window.addEventListener('mousemove', resizeMouseMove)
|
||||
window.addEventListener('mouseup', docMouseUp)
|
||||
}
|
||||
|
||||
function resizeMouseMove (e: MouseEvent): void {
|
||||
if ($lockedRoom === '' || locked?.room === undefined || locked.side === undefined || block) return
|
||||
block = true
|
||||
const error: RoomSide = { top: false, bottom: false, left: false, right: false }
|
||||
const room: Room = locked.room
|
||||
locked.size = roomsConf[lockedID].getRect()
|
||||
|
||||
if (locked.side.bottom || locked.side.top) {
|
||||
let newHeight: number = locked.side.bottom
|
||||
? Math.round((e.clientY - locked.size.y - floorSize.cellRound) / floorSize.cellSize)
|
||||
: Math.round((locked.size.y - e.clientY + locked.size.height - floorSize.cellTop) / floorSize.cellSize)
|
||||
if (newHeight < 1) newHeight = 1
|
||||
let newY: number = locked.side.bottom ? room.y : room.y + room.height - newHeight
|
||||
if (newY < 0) {
|
||||
newHeight += newY
|
||||
newY = 0
|
||||
}
|
||||
let freeSpace: boolean = !(room.y === 0 && locked.side.top)
|
||||
if (freeSpace) {
|
||||
const startY: number = locked.side.bottom ? room.y + room.height : newY
|
||||
const endY: number = locked.side.bottom ? room.y + newHeight : room.y
|
||||
for (let y = startY; y < endY; y++) {
|
||||
if (y > 0 && locked.map[y] === undefined) locked.map[y] = new Array(GRID_WIDTH).fill(true)
|
||||
for (let x = room.x; x < room.x + room.width; x++) {
|
||||
if (locked.map[y][x] !== undefined && !locked.map[y][x]) freeSpace = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (locked.side.bottom) {
|
||||
if (e.clientY < locked.size.y + locked.size.height - 6) {
|
||||
error.bottom = locked.room.height === 1
|
||||
} else if (
|
||||
e.clientY >= locked.size.y + locked.size.height - 6 &&
|
||||
e.clientY < locked.size.y + locked.size.height + 6
|
||||
) {
|
||||
error.bottom = false
|
||||
} else if (e.clientY >= locked.size.y + locked.size.height + 6) error.bottom = !freeSpace
|
||||
} else {
|
||||
if (e.clientY > locked.size.y - floorSize.cellTop + 6) {
|
||||
error.top = locked.room.height === 1
|
||||
} else if (
|
||||
e.clientY <= locked.size.y - floorSize.cellTop + 6 &&
|
||||
e.clientY > locked.size.y - floorSize.cellTop - 6
|
||||
) {
|
||||
error.bottom = false
|
||||
} else if (e.clientY <= locked.size.y - floorSize.cellTop - 6) error.bottom = !freeSpace
|
||||
}
|
||||
if ((freeSpace && room.height < newHeight) || room.height > newHeight) {
|
||||
rooms[lockedID].height = newHeight
|
||||
if (locked.side.top) rooms[lockedID].y = newY
|
||||
locked.room = rooms[lockedID]
|
||||
locked.map = getFreeSpace(rooms, locked.room)
|
||||
}
|
||||
}
|
||||
|
||||
if (locked.side.left || locked.side.right) {
|
||||
let newWidth: number = locked.side.right
|
||||
? Math.round((e.clientX - locked.size.x - floorSize.cellRound) / floorSize.cellSize)
|
||||
: Math.round((locked.size.x - e.clientX + locked.size.width - floorSize.cellRound) / floorSize.cellSize)
|
||||
if (newWidth < 1) newWidth = 1
|
||||
let newX: number = locked.side.right ? room.x : room.x + room.width - newWidth
|
||||
if (newX < 0) {
|
||||
newWidth += newX
|
||||
newX = 0
|
||||
}
|
||||
if (newX + newWidth > GRID_WIDTH) newWidth = GRID_WIDTH - newX
|
||||
let freeSpace: boolean = !(
|
||||
(room.x === 0 && locked.side.left) ||
|
||||
(room.x + room.width === GRID_WIDTH && locked.side.right)
|
||||
)
|
||||
if (freeSpace) {
|
||||
const startX: number = locked.side.right ? room.x + room.width : newX
|
||||
const endX: number = locked.side.right ? room.x + newWidth : room.x
|
||||
for (let y = room.y; y < room.y + room.height; y++) {
|
||||
for (let x = startX; x < endX; x++) {
|
||||
if (locked.map[y][x] !== undefined && !locked.map[y][x]) freeSpace = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (locked.side.right) {
|
||||
if (e.clientX < locked.size.x + locked.size.width + floorSize.cellRound - 6) {
|
||||
error.right = locked.room.width === 1
|
||||
} else if (
|
||||
e.clientX >= locked.size.x + locked.size.width + floorSize.cellRound - 6 &&
|
||||
e.clientX < locked.size.x + locked.size.width + floorSize.cellRound + 6
|
||||
) {
|
||||
error.right = false
|
||||
} else if (e.clientX >= locked.size.x + locked.size.width + floorSize.cellRound + 6) error.right = !freeSpace
|
||||
} else {
|
||||
if (e.clientX > locked.size.x - floorSize.cellRound + 6) {
|
||||
error.left = locked.room.width === 1
|
||||
} else if (
|
||||
e.clientX <= locked.size.x - floorSize.cellRound + 6 &&
|
||||
e.clientX > locked.size.x - floorSize.cellRound - 6
|
||||
) {
|
||||
error.bottom = false
|
||||
} else if (e.clientX <= locked.size.x - floorSize.cellRound - 6) error.bottom = !freeSpace
|
||||
}
|
||||
if ((freeSpace && room.width < newWidth) || room.width > newWidth) {
|
||||
rooms[lockedID].width = newWidth
|
||||
if (locked.side.left) rooms[lockedID].x = newX
|
||||
locked.room = rooms[lockedID]
|
||||
locked.map = getFreeSpace(rooms, locked.room)
|
||||
}
|
||||
}
|
||||
|
||||
setShadowColor(error.top || error.bottom || error.left || error.right ? shadowError : shadowNormal)
|
||||
block = false
|
||||
}
|
||||
|
||||
function docMouseUp (e: MouseEvent): void {
|
||||
if (locked) updateRoom(locked.room._id)
|
||||
if (dragged !== undefined) {
|
||||
divScroll.removeEventListener('mousemove', dragMouseMove)
|
||||
dragged = undefined
|
||||
dragRoom.clearShadow()
|
||||
} else {
|
||||
window.removeEventListener('mousemove', resizeMouseMove)
|
||||
roomsConf[lockedID].clearShadow()
|
||||
}
|
||||
window.removeEventListener('mouseup', docMouseUp)
|
||||
cursor = 'default'
|
||||
lockedRoom.set('')
|
||||
lockedID = -1
|
||||
}
|
||||
$: rows = calculateFloorSize(rooms) + 2
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent">
|
||||
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
|
||||
<Breadcrumb title={selectedFloor?.name ?? ''} size={'large'} isCurrent />
|
||||
<svelte:fragment slot="actions">
|
||||
<ButtonIcon icon={IconAdd} size={'small'} on:click={addRoom} />
|
||||
<div class="hulyHeader-divider short" />
|
||||
<ModernButton
|
||||
label={lovePlg.string.FinalizeEditing}
|
||||
kind={'primary'}
|
||||
size={'small'}
|
||||
on:click={() => dispatch('configure')}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Header>
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller bind:divScroll padding={'1rem'} bottomPadding={'1rem'} horizontal>
|
||||
<FloorGrid
|
||||
bind:floorContainer
|
||||
{rows}
|
||||
on:resize={(event) => {
|
||||
if (event.detail === undefined) return
|
||||
const { width, height } = event.detail
|
||||
floorSize.width = width
|
||||
floorSize.height = height
|
||||
floorSize.cellSize = width / (GRID_WIDTH + 2)
|
||||
floorSize.cellTop = (floorSize.cellSize / 3) * 1.6
|
||||
floorSize.cellRound = floorSize.cellSize / 5
|
||||
floorSize.rows = calculateFloorSize(rooms) + 2
|
||||
}}
|
||||
>
|
||||
{#each rooms as room, i}
|
||||
<RoomConfigure
|
||||
bind:this={roomsConf[i]}
|
||||
room={lockedID === i && dragged !== undefined && (locked?.room.x !== room.x || locked?.room.y !== room.y)
|
||||
? { ...room, x: locked?.room.x ?? room.x, y: locked?.room.y ?? room.y }
|
||||
: room}
|
||||
placed={lockedID === i && dragged !== undefined && locked?.room.x === room.x && locked?.room.y === room.y}
|
||||
cellSize={floorSize.cellSize}
|
||||
{excludedPersons}
|
||||
on:cursor={(event) => {
|
||||
if (event.detail) cursor = event.detail
|
||||
}}
|
||||
on:resize={(event) => {
|
||||
if (event.detail === undefined) return
|
||||
const { room, size, side } = event.detail
|
||||
resizeInitParams = { x: room.x, y: room.y, width: room.width, height: room.height }
|
||||
floorRect = floorContainer.getBoundingClientRect()
|
||||
startResizeRoom(room, size, side, i)
|
||||
}}
|
||||
on:move={(event) => {
|
||||
if (event.detail === undefined) return
|
||||
const { room, size, offset } = event.detail
|
||||
floorRect = floorContainer.getBoundingClientRect()
|
||||
dragged = {
|
||||
x: size.x - floorRect.x + floorSize.cellRound,
|
||||
y: size.y - floorRect.y + floorSize.cellRound,
|
||||
offsetX: offset.x,
|
||||
offsetY: offset.y
|
||||
}
|
||||
resizeInitParams = { x: room.x, y: room.y, width: room.width, height: room.height }
|
||||
startDragRoom(room, size, i)
|
||||
}}
|
||||
on:updated={(event) => {
|
||||
if (event.detail !== undefined && locked) locked.size = event.detail
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</FloorGrid>
|
||||
{#if lockedID !== -1 && dragged !== undefined && locked !== undefined}
|
||||
<RoomConfigure
|
||||
bind:this={dragRoom}
|
||||
room={locked.room}
|
||||
cellSize={floorSize.cellSize}
|
||||
top={dragged.y}
|
||||
left={dragged.x}
|
||||
/>
|
||||
{/if}
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { resizeObserver } from '@hcengineering/ui'
|
||||
import { GRID_WIDTH } from '@hcengineering/love'
|
||||
|
||||
export let floorContainer: HTMLDivElement
|
||||
export let marginInline: string = 'auto'
|
||||
export let preview: boolean = false
|
||||
export let rows: number = 5
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if preview}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={floorContainer}
|
||||
class="floorGrid"
|
||||
style:grid-template-columns={`repeat(${GRID_WIDTH + 2}, 1fr)`}
|
||||
style:grid-template-rows={rows ? `repeat(${rows}, 1fr)` : undefined}
|
||||
style:max-width={`${(GRID_WIDTH + 2) * 4}rem`}
|
||||
style:margin-inline={marginInline}
|
||||
on:mouseover
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={floorContainer}
|
||||
use:resizeObserver={(element) => {
|
||||
dispatch('resize', { width: element.clientWidth, height: element.clientHeight })
|
||||
}}
|
||||
class="floorGrid"
|
||||
style:grid-template-columns={`repeat(${GRID_WIDTH + 2}, 1fr)`}
|
||||
style:grid-template-rows={rows ? `repeat(${rows}, 1fr)` : undefined}
|
||||
style:max-width={`${(GRID_WIDTH + 2) * 4}rem`}
|
||||
style:margin-inline={marginInline}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.floorGrid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(17, 1fr);
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 1fr;
|
||||
place-items: stretch;
|
||||
place-content: center;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import ui, { Button, IconChevronLeft, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
import { Floor, Room } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { floors, rooms } from '../stores'
|
||||
import FloorPreview from './FloorPreview.svelte'
|
||||
import IconLayers from './icons/Layers.svelte'
|
||||
|
||||
export let selectedFloor: Floor | undefined
|
||||
|
||||
function getRooms (rooms: Room[], floor: Ref<Floor>): Room[] {
|
||||
return rooms.filter((p) => p.floor === floor)
|
||||
}
|
||||
|
||||
let floorsSelector: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function changeMode (): void {
|
||||
floorsSelector = !floorsSelector
|
||||
}
|
||||
|
||||
function selectFloor (_id: Ref<Floor>): void {
|
||||
dispatch('close', _id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4">
|
||||
{#if floorsSelector}
|
||||
{#each $floors as floor, i}
|
||||
<Button
|
||||
kind={'ghost'}
|
||||
size={'large'}
|
||||
on:click={() => {
|
||||
selectFloor(floor._id)
|
||||
}}
|
||||
justify={'left'}
|
||||
label={getEmbeddedLabel(floor.name)}
|
||||
/>
|
||||
{#if i !== $floors.length - 1}<div class="divider" />{/if}
|
||||
{/each}
|
||||
<div class="flex-row-center flex-reverse mt-4 w-full">
|
||||
<ModernButton on:click={changeMode} icon={IconChevronLeft} label={ui.string.Back} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if selectedFloor}
|
||||
<Scroller>
|
||||
<FloorPreview
|
||||
floor={selectedFloor}
|
||||
showRoomName
|
||||
rooms={getRooms($rooms, selectedFloor._id)}
|
||||
selected
|
||||
isOpen
|
||||
disabled
|
||||
cropped
|
||||
size={'small'}
|
||||
kind={'no-border'}
|
||||
background={'var(--theme-popup-color)'}
|
||||
/>
|
||||
</Scroller>
|
||||
{/if}
|
||||
{#if $floors.length > 1}
|
||||
<div class="flex-row-center flex-reverse flex-no-shrink w-full mt-4">
|
||||
<ModernButton on:click={changeMode} icon={IconLayers} label={love.string.ChangeFloor} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
width: 36rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
AccordionItem,
|
||||
ButtonIcon,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconMoreH,
|
||||
IconSettings,
|
||||
Label,
|
||||
SelectPopup,
|
||||
eventToHTMLElement,
|
||||
showPopup,
|
||||
type SelectPopupValueType
|
||||
} from '@hcengineering/ui'
|
||||
import { Floor, ParticipantInfo, Room } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../plugin'
|
||||
import { infos } from '../stores'
|
||||
import { calculateFloorSize } from '../utils'
|
||||
import EditFloorPopup from './EditFloorPopup.svelte'
|
||||
import FloorGrid from './FloorGrid.svelte'
|
||||
import RoomPreview from './RoomPreview.svelte'
|
||||
|
||||
export let floor: Floor
|
||||
export let configurable: boolean = false
|
||||
export let rooms: Room[] = []
|
||||
export let selected: boolean
|
||||
export let disabled: boolean = false
|
||||
export let isOpen: boolean = false
|
||||
export let cropped: boolean = false
|
||||
export let showRoomName: boolean = false
|
||||
export let size: 'small' | 'medium' | 'large' = 'large'
|
||||
export let background: string | undefined = undefined
|
||||
export let kind: 'default' | 'second' | 'no-border' = 'default'
|
||||
export let configure: boolean = false
|
||||
|
||||
const me = getCurrentAccount()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let floorContainer: HTMLDivElement
|
||||
let hovered: number = -1
|
||||
|
||||
function getInfo (room: Ref<Room>, info: ParticipantInfo[]): ParticipantInfo[] {
|
||||
return info.filter((p) => p.room === room)
|
||||
}
|
||||
|
||||
let roomName: IntlString | undefined = undefined
|
||||
|
||||
function hover (e: CustomEvent<any>, n: number): void {
|
||||
roomName = e.detail.name
|
||||
hovered = n
|
||||
}
|
||||
|
||||
$: editable = hasAccountRole(me, AccountRole.Maintainer)
|
||||
$: rows = calculateFloorSize(rooms) - (cropped ? 1 : 0)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function remove (): Promise<void> {
|
||||
await client.remove(floor)
|
||||
}
|
||||
|
||||
function renameFloor (): void {
|
||||
showPopup(EditFloorPopup, { id: floor._id }, 'top', () => {
|
||||
pressed = false
|
||||
})
|
||||
}
|
||||
|
||||
let pressed: boolean = false
|
||||
const clickMore = (e: MouseEvent): void => {
|
||||
pressed = true
|
||||
const value: SelectPopupValueType[] = [
|
||||
{
|
||||
id: 'configure',
|
||||
icon: IconSettings,
|
||||
label: configure ? plugin.string.FinalizeEditing : plugin.string.EditOffice
|
||||
},
|
||||
{ id: 'rename', icon: IconEdit, label: plugin.string.RenameAFloor }
|
||||
]
|
||||
showPopup(SelectPopup, { value }, eventToHTMLElement(e), (result) => {
|
||||
if (result === 'configure') {
|
||||
dispatch('configure', floor)
|
||||
pressed = false
|
||||
} else if (result === 'rename') renameFloor()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<AccordionItem
|
||||
id={`floor-${floor._id}`}
|
||||
title={floor.name}
|
||||
{size}
|
||||
{background}
|
||||
{isOpen}
|
||||
selectable
|
||||
{selected}
|
||||
{disabled}
|
||||
{kind}
|
||||
categoryHeader
|
||||
on:select
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if showRoomName && roomName !== undefined}
|
||||
<span class="content-color overflow-label"><Label label={roomName} /></span>
|
||||
{/if}
|
||||
{#if configurable && editable}
|
||||
{#if rooms.length === 0}
|
||||
<div class="mr-2">
|
||||
<ButtonIcon icon={IconDelete} kind={'negative'} size={'small'} on:click={remove} />
|
||||
</div>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconMoreH} kind={'tertiary'} size={'small'} {pressed} on:click={clickMore} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<FloorGrid bind:floorContainer {rows} preview on:mouseover={() => (roomName = undefined)}>
|
||||
{#each rooms as room, i}
|
||||
<RoomPreview
|
||||
{room}
|
||||
info={getInfo(room._id, $infos)}
|
||||
preview
|
||||
on:hover={(e) => {
|
||||
hover(e, i)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</FloorGrid>
|
||||
</AccordionItem>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import {
|
||||
ButtonIcon,
|
||||
IconAdd,
|
||||
Label,
|
||||
Scroller,
|
||||
Separator,
|
||||
defineSeparators,
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { Floor as FloorType, Room } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { floors, rooms } from '../stores'
|
||||
import { loveSeparators } from '../types'
|
||||
import EditFloorPopup from './EditFloorPopup.svelte'
|
||||
import FloorPreview from './FloorPreview.svelte'
|
||||
|
||||
export let visibleNav: boolean
|
||||
export let navFloat: boolean = false
|
||||
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
|
||||
export let floor: Ref<FloorType>
|
||||
export let configure: boolean
|
||||
|
||||
const me = getCurrentAccount()
|
||||
|
||||
function getRooms (rooms: Room[], floor: Ref<FloorType>): Room[] {
|
||||
return rooms.filter((p) => p.floor === floor)
|
||||
}
|
||||
|
||||
function addFloor (e: MouseEvent): void {
|
||||
showPopup(EditFloorPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
let editable: boolean = false
|
||||
$: editable = hasAccountRole(me, AccountRole.Maintainer)
|
||||
|
||||
defineSeparators('love', loveSeparators)
|
||||
</script>
|
||||
|
||||
{#if visibleNav}
|
||||
<div
|
||||
class="antiPanel-navigator {appsDirection === 'horizontal'
|
||||
? 'portrait'
|
||||
: 'landscape'} border-left will-change-opacity"
|
||||
>
|
||||
<div class="antiPanel-wrap__content hulyNavPanel-container">
|
||||
<div class="hulyNavPanel-header small" class:withButton={editable}>
|
||||
<span class="overflow-label">
|
||||
<Label label={love.string.Floors} />
|
||||
</span>
|
||||
{#if editable}
|
||||
<ButtonIcon icon={IconAdd} kind={'primary'} size={'small'} on:click={addFloor} />
|
||||
{/if}
|
||||
</div>
|
||||
<Scroller fade={{ multipler: { top: 4, bottom: 0 } }} shrink>
|
||||
{#each $floors as _floor}
|
||||
<FloorPreview
|
||||
showRoomName
|
||||
floor={_floor}
|
||||
configurable
|
||||
configure={configure && floor === _floor._id}
|
||||
rooms={getRooms($rooms, _floor._id)}
|
||||
selected={floor === _floor._id}
|
||||
background={'var(--theme-navpanel-color)'}
|
||||
on:configure={() => {
|
||||
if (floor === _floor._id) {
|
||||
configure = !configure
|
||||
} else {
|
||||
floor = _floor._id
|
||||
configure = true
|
||||
}
|
||||
}}
|
||||
on:select={() => {
|
||||
floor = _floor._id
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</Scroller>
|
||||
</div>
|
||||
<Separator name={'love'} float={navFloat ? 'navigator' : true} index={0} color={'transparent'} />
|
||||
</div>
|
||||
<Separator name={'love'} float={navFloat} index={0} color={'transparent'} separatorSize={0} short />
|
||||
{/if}
|
||||
@@ -0,0 +1,62 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Floor as FloorType, Office, Room, isOffice } from '@hcengineering/love'
|
||||
import { activeFloor, floors, rooms } from '../stores'
|
||||
import Floor from './Floor.svelte'
|
||||
import FloorConfigure from './FloorConfigure.svelte'
|
||||
import Floors from './Floors.svelte'
|
||||
import { Contact, Person } from '@hcengineering/contact'
|
||||
|
||||
export let visibleNav: boolean
|
||||
export let navFloat: boolean = false
|
||||
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
|
||||
|
||||
function getRooms (rooms: Room[], floor: Ref<FloorType>): Room[] {
|
||||
return rooms.filter((p) => p.floor === floor)
|
||||
}
|
||||
|
||||
let selectedFloor = $activeFloor === '' ? $floors[0]?._id : $activeFloor
|
||||
|
||||
let configure: boolean = false
|
||||
|
||||
let excludedPersons: Ref<Contact>[] = []
|
||||
$: excludedPersons = $rooms
|
||||
.filter((p) => isOffice(p) && p.person !== null)
|
||||
.map((p) => (p as Office).person) as Ref<Person>[]
|
||||
</script>
|
||||
|
||||
<Floors bind:visibleNav {navFloat} {appsDirection} bind:floor={selectedFloor} bind:configure />
|
||||
<div class="antiPanel-component filledNav">
|
||||
{#if configure}
|
||||
<FloorConfigure
|
||||
rooms={getRooms($rooms, selectedFloor)}
|
||||
floor={selectedFloor}
|
||||
{visibleNav}
|
||||
{excludedPersons}
|
||||
on:change={(event) => (visibleNav = event.detail)}
|
||||
on:configure={() => (configure = false)}
|
||||
/>
|
||||
{:else}
|
||||
<Floor
|
||||
rooms={getRooms($rooms, selectedFloor)}
|
||||
floor={selectedFloor}
|
||||
{visibleNav}
|
||||
on:change={(event) => (visibleNav = event.detail)}
|
||||
on:configure={() => (configure = true)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,90 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { Invite, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { infos, myInfo, rooms } from '../stores'
|
||||
import { connectRoom, getFreePlace } from '../utils'
|
||||
|
||||
export let invite: Invite
|
||||
|
||||
$: person = $personByIdStore.get(invite.from)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function accept (): Promise<void> {
|
||||
const room = $rooms.find((p) => p._id === invite.room)
|
||||
if (room === undefined) return
|
||||
const myPerson = $personByIdStore.get(invite.target)
|
||||
if (myPerson === undefined) return
|
||||
if ($myInfo === undefined) return
|
||||
await client.update(invite, { status: RequestStatus.Approved })
|
||||
const place = getFreePlace(
|
||||
room,
|
||||
$infos.filter((p) => p.room === room?._id)
|
||||
)
|
||||
await connectRoom(place.x, place.y, $myInfo, myPerson, room)
|
||||
}
|
||||
|
||||
async function decline (): Promise<void> {
|
||||
await client.update(invite, { status: RequestStatus.Rejected })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup flex-col-center">
|
||||
{#if person}
|
||||
<div class="p-4 flex-col-center flex-gap-4">
|
||||
<Avatar {person} size={'large'} name={person.name} />
|
||||
<span class="title">
|
||||
<Label label={love.string.InvitingYou} params={{ name: formatName(person.name) }} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-between w-full buttons">
|
||||
<div class="p-1 w-full">
|
||||
<Button label={love.string.Accept} width={'100%'} on:click={accept} />
|
||||
</div>
|
||||
<div class="p-1 w-full">
|
||||
<Button label={love.string.Decline} width={'100%'} on:click={decline} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
padding-top: 1rem;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding-top: 0.25rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--caption-color);
|
||||
font-size: 700;
|
||||
}
|
||||
|
||||
.roomTitle {
|
||||
color: var(--caption-color);
|
||||
font-size: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { RoomType } from '@hcengineering/love'
|
||||
import { currentRoom } from '../stores'
|
||||
import { screenSharing } from '../utils'
|
||||
import Hall from './Hall.svelte'
|
||||
import RoomComponent from './Room.svelte'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
export let navFloat: boolean = false
|
||||
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
|
||||
|
||||
const saved = localStorage.getItem('love-visibleNav')
|
||||
if (saved !== undefined) visibleNav = saved === 'true'
|
||||
$: localStorage.setItem('love-visibleNav', JSON.stringify(visibleNav))
|
||||
</script>
|
||||
|
||||
<div class="hulyPanels-container" class:left-divider={$screenSharing || $currentRoom?.type === RoomType.Video}>
|
||||
{#if ($currentRoom !== undefined && $screenSharing) || $currentRoom?.type === RoomType.Video}
|
||||
<RoomComponent withVideo={$currentRoom.type === RoomType.Video} room={$currentRoom._id} />
|
||||
{:else}
|
||||
<Hall bind:visibleNav {navFloat} {appsDirection} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { getEmbeddedLabel, translate } from '@hcengineering/platform'
|
||||
import { DropdownLabels, DropdownTextItem, Label, Toggle } from '@hcengineering/ui'
|
||||
import { Room } from 'livekit-client'
|
||||
import love from '../plugin'
|
||||
import { getActive, krispProcessor, lk, selectedMicId, selectedSpeakerId } from '../utils'
|
||||
import { DevicesPreference } from '@hcengineering/love'
|
||||
import { getCurrentAccount, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { myPreferences } from '../stores'
|
||||
import { isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
|
||||
|
||||
void Room.getLocalDevices().then(async (devices) => {
|
||||
devices.forEach((device) => {
|
||||
if (device.kind === 'audiooutput') {
|
||||
speakers.push({ label: device.label, id: device.deviceId })
|
||||
} else if (device.kind === 'audioinput') {
|
||||
mics.push({ label: device.label, id: device.deviceId })
|
||||
}
|
||||
})
|
||||
if (speakers.length === 0) {
|
||||
speakers.push({ label: await translate(love.string.DefaultDevice, {}), id: 'default' })
|
||||
}
|
||||
if (mics.length === 0) {
|
||||
mics.push({ label: await translate(love.string.DefaultDevice, {}), id: 'default' })
|
||||
}
|
||||
speakers = speakers
|
||||
mics = mics
|
||||
})
|
||||
|
||||
let speakers: DropdownTextItem[] = []
|
||||
let mics: DropdownTextItem[] = []
|
||||
|
||||
$: activeSpeaker = getActive(speakers, lk.getActiveDevice('audiooutput'), localStorage.getItem(selectedSpeakerId))
|
||||
$: activeMic = getActive(mics, lk.getActiveDevice('audioinput'), localStorage.getItem(selectedMicId))
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function saveNoiseCancellationPreference (
|
||||
myPreferences: DevicesPreference | undefined,
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
if (myPreferences !== undefined) {
|
||||
await client.update(myPreferences, { noiseCancellation: value })
|
||||
} else {
|
||||
const space = getCurrentAccount()._id as string as Ref<Space>
|
||||
await client.createDoc(love.class.DevicesPreference, space, {
|
||||
attachedTo: space,
|
||||
noiseCancellation: value,
|
||||
camEnabled: true,
|
||||
micEnabled: true,
|
||||
blurRadius: 0
|
||||
})
|
||||
}
|
||||
await krispProcessor.setEnabled(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4 grid">
|
||||
<Label label={love.string.Speaker} />
|
||||
<DropdownLabels
|
||||
selected={activeSpeaker?.id}
|
||||
items={speakers}
|
||||
enableSearch={false}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
disabled={speakers.length === 0}
|
||||
on:selected={async (item) => {
|
||||
if (item.detail != null && item.detail !== 'default') {
|
||||
await lk.switchActiveDevice('audiooutput', item.detail)
|
||||
localStorage.setItem(selectedSpeakerId, item.detail)
|
||||
activeSpeaker = speakers.find((p) => p.id === item.detail) ?? speakers[0]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label label={love.string.Microphone} />
|
||||
<DropdownLabels
|
||||
items={mics}
|
||||
placeholder={love.string.Microphone}
|
||||
selected={activeMic?.id}
|
||||
enableSearch={false}
|
||||
justify={'left'}
|
||||
disabled={mics.length === 0}
|
||||
width={'100%'}
|
||||
on:selected={async (item) => {
|
||||
if (item.detail != null && item.detail !== 'default') {
|
||||
await lk.switchActiveDevice('audioinput', item.detail)
|
||||
localStorage.setItem(selectedMicId, item.detail)
|
||||
activeMic = mics.find((p) => p.id === item.detail) ?? mics[0]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if isKrispNoiseFilterSupported()}
|
||||
<Label label={love.string.NoiseCancellation} />
|
||||
<Toggle
|
||||
on={$myPreferences?.noiseCancellation ?? true}
|
||||
on:change={(e) => {
|
||||
saveNoiseCancellationPreference($myPreferences, e.detail)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
row-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Person, formatName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Icon, Loading } from '@hcengineering/ui'
|
||||
import love from '../plugin'
|
||||
|
||||
export let _id: string
|
||||
export let name: string
|
||||
export let muted: boolean
|
||||
export let mirror: boolean
|
||||
export let connecting: boolean = false
|
||||
export let small: boolean = false
|
||||
|
||||
let parent: HTMLDivElement
|
||||
let activeTrack: boolean = false
|
||||
|
||||
export function appendChild (track: HTMLMediaElement): void {
|
||||
const video = parent.querySelector('.video')
|
||||
if (video != null) {
|
||||
video.remove()
|
||||
}
|
||||
|
||||
track.classList.add('video')
|
||||
parent.appendChild(track)
|
||||
activeTrack = true
|
||||
}
|
||||
|
||||
export function setTrackMuted (value: boolean): void {
|
||||
const video = parent.querySelector('video')
|
||||
if (video != null) {
|
||||
if (value) {
|
||||
video.classList.add('hidden')
|
||||
} else {
|
||||
video.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
activeTrack = !value
|
||||
}
|
||||
|
||||
let labelWidth: number
|
||||
let parentWidth: number
|
||||
$: filled = labelWidth === parentWidth && labelWidth > 0
|
||||
$: user = $personByIdStore.get(_id as Ref<Person>)
|
||||
</script>
|
||||
|
||||
<div id={_id} class="parent" class:small bind:clientWidth={parentWidth}>
|
||||
<div class="label" class:filled bind:clientWidth={labelWidth}>
|
||||
<span class="overflow-label">{formatName(name)}</span>
|
||||
{#if connecting}
|
||||
<div class="loading">
|
||||
<Loading size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="icon" class:muted>
|
||||
<Icon size="small" icon={love.icon.MicDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
<div bind:this={parent} class="cover" class:mirror={mirror && activeTrack}>
|
||||
<div class="ava" class:hidden={activeTrack}>
|
||||
<Avatar size={'full'} {name} person={user} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.video) {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.hidden) {
|
||||
display: none;
|
||||
}
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
aspect-ratio: 1280/720;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.ava {
|
||||
height: 25%;
|
||||
overflow: hidden;
|
||||
border-radius: 20%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
}
|
||||
.parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: max-content;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
max-width: 100%;
|
||||
padding: 0.25rem 0.5rem 0.5rem 1rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0 0.25rem 0 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
z-index: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
text-shadow: 0 0 0.25rem black;
|
||||
}
|
||||
&.filled {
|
||||
padding: 0.25rem 1rem 0.5rem 1rem;
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
}
|
||||
}
|
||||
&.small .label {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
|
||||
|
||||
&.filled {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
&:not(.muted) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PersonAccount, formatName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { JoinRequest, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { myInfo, myOffice } from '../stores'
|
||||
import { connectRoom, isConnected } from '../utils'
|
||||
|
||||
export let request: JoinRequest
|
||||
|
||||
$: person = $personByIdStore.get(request.person)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function accept (): Promise<void> {
|
||||
if (request.room === $myOffice?._id && !$isConnected) {
|
||||
const me = (getCurrentAccount() as PersonAccount).person
|
||||
const person = $personByIdStore.get(me)
|
||||
if (person === undefined) return
|
||||
await connectRoom(0, 0, $myInfo, person, $myOffice)
|
||||
await client.update(request, { status: RequestStatus.Approved })
|
||||
}
|
||||
}
|
||||
|
||||
async function decline (): Promise<void> {
|
||||
await client.update(request, { status: RequestStatus.Rejected })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup flex-col-center">
|
||||
{#if person}
|
||||
<div class="p-4 flex-col-center flex-gap-4">
|
||||
<Avatar {person} size={'large'} name={person.name} />
|
||||
<span class="title">
|
||||
<Label label={love.string.IsKnocking} params={{ name: formatName(person.name) }} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-between w-full buttons">
|
||||
<div class="p-1 w-full">
|
||||
<Button label={love.string.Accept} width={'100%'} on:click={accept} />
|
||||
</div>
|
||||
<div class="p-1 w-full">
|
||||
<Button label={love.string.Decline} width={'100%'} on:click={decline} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
padding-top: 1rem;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding-top: 0.25rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--caption-color);
|
||||
font-size: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { JoinRequest, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { rooms } from '../stores'
|
||||
import { getRoomLabel } from '../utils'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
|
||||
export let request: JoinRequest
|
||||
|
||||
$: room = $rooms.find((p) => p._id === request.room)
|
||||
|
||||
async function cancel () {
|
||||
if (request.status === RequestStatus.Pending) {
|
||||
const client = getClient()
|
||||
await client.remove(request)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup flex-col-center">
|
||||
<div class="mb-4 flex-col-center flex-gap-2">
|
||||
<Label label={love.string.KnockingTo} params={{ name: room?.name }} />
|
||||
<span class="title">
|
||||
{#if room}
|
||||
<Label label={getRoomLabel(room, $personByIdStore)} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-row-center p-1 w-full">
|
||||
<Button label={love.string.Cancel} width={'100%'} on:click={cancel} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiPopup {
|
||||
padding-top: 1rem;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--caption-color);
|
||||
font-size: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Label, Loading, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track,
|
||||
TrackPublication
|
||||
} from 'livekit-client'
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, infos } from '../stores'
|
||||
import { awaitConnect, isConnected, isCurrentInstanceConnected, lk, screenSharing } from '../utils'
|
||||
import ControlBar from './ControlBar.svelte'
|
||||
import ParticipantView from './ParticipantView.svelte'
|
||||
|
||||
export let withVideo: boolean
|
||||
export let room: Ref<TypeRoom>
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
name: string
|
||||
connecting: boolean
|
||||
muted: boolean
|
||||
mirror: boolean
|
||||
}
|
||||
|
||||
let participants: ParticipantData[] = []
|
||||
const participantElements: ParticipantView[] = []
|
||||
let screen: HTMLVideoElement
|
||||
|
||||
const remToPx = (rem: number): number => rem * $deviceInfo.fontSize
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source === Track.Source.ScreenShare) {
|
||||
track.attach(screen)
|
||||
} else {
|
||||
const element = track.attach()
|
||||
attachTrack(element, participant)
|
||||
}
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source !== Track.Source.ScreenShare) {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
} else {
|
||||
track.detach(screen)
|
||||
}
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLocalTrack (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
void attachTrack(element, participant)
|
||||
}
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTrack (element: HTMLMediaElement, participant: Participant): Promise<void> {
|
||||
let index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index === -1) {
|
||||
index = participants.push({
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false
|
||||
})
|
||||
}
|
||||
participants = participants
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
await tick()
|
||||
index = participants.findIndex((p) => p._id === participant.identity)
|
||||
const el = participantElements[index]
|
||||
if (el !== undefined) {
|
||||
el.appendChild(element)
|
||||
return
|
||||
}
|
||||
}
|
||||
console.error('Failed to attach track after 10 attempts')
|
||||
Analytics.handleError(new Error(`Failed to attach track after 10 attempts, participant: ${participant.identity}`))
|
||||
}
|
||||
|
||||
function attachParticipant (participant: Participant): void {
|
||||
const current = participants.find((p) => p._id === participant.identity)
|
||||
if (current !== undefined) {
|
||||
current.connecting = false
|
||||
current.muted = !participant.isMicrophoneEnabled
|
||||
current.mirror = participant.isLocal
|
||||
participants = participants
|
||||
return
|
||||
}
|
||||
const value: ParticipantData = {
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false
|
||||
}
|
||||
participants.push(value)
|
||||
participants = participants
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
}
|
||||
|
||||
function handleParticipantDisconnected (participant: RemoteParticipant): void {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
}
|
||||
|
||||
function muteHandler (publication: TrackPublication, participant: Participant): void {
|
||||
if (publication.kind === Track.Kind.Video) {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
return
|
||||
}
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participantElements[index].setTrackMuted(publication.isMuted)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let loading: boolean = false
|
||||
let configured: boolean = false
|
||||
|
||||
function handleLocalTrackUnsubscribed (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication?.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.detach(screen)
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
} else {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
loading = true
|
||||
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
|
||||
if (wsURL === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
configured = true
|
||||
|
||||
await awaitConnect()
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
attachParticipant(participant)
|
||||
for (const publication of participant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachParticipant(lk.localParticipant)
|
||||
for (const publication of lk.localParticipant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, lk.localParticipant)
|
||||
}
|
||||
}
|
||||
}
|
||||
lk.on(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.on(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnsubscribed)
|
||||
loading = false
|
||||
})
|
||||
|
||||
let gridStyle = ''
|
||||
let columns: number = 0
|
||||
let rows: number = 0
|
||||
let roomWidth: number
|
||||
let roomHeight: number
|
||||
|
||||
onDestroy(
|
||||
infos.subscribe((data) => {
|
||||
for (const info of data) {
|
||||
if (info.room !== room) continue
|
||||
const current = participants.find((p) => p._id === info.person)
|
||||
if (current !== undefined) continue
|
||||
const value: ParticipantData = {
|
||||
_id: info.person,
|
||||
name: info.name,
|
||||
muted: true,
|
||||
mirror: false,
|
||||
connecting: true
|
||||
}
|
||||
participants.push(value)
|
||||
}
|
||||
participants = participants
|
||||
updateStyle(participants.length, $screenSharing)
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.off(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.off(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.off(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.off(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnsubscribed)
|
||||
})
|
||||
|
||||
function updateStyle (count: number, screenSharing: boolean): void {
|
||||
columns = screenSharing ? 1 : Math.min(Math.ceil(Math.sqrt(count)), 8)
|
||||
rows = Math.ceil(count / columns)
|
||||
gridStyle = `grid-template-columns: repeat(${columns}, 1fr); aspect-ratio: ${columns * 1280}/${rows * 720};`
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col-center w-full h-full">
|
||||
{#if $isConnected && !$isCurrentInstanceConnected}
|
||||
<div class="flex justify-center error h-full w-full clear-mins">
|
||||
<Label label={love.string.AnotherWindowError} />
|
||||
</div>
|
||||
{:else if !configured}
|
||||
<div class="flex justify-center error h-full w-full clear-mins">
|
||||
<Label label={love.string.ServiceNotConfigured} />
|
||||
</div>
|
||||
{:else if loading}
|
||||
<Loading />
|
||||
{/if}
|
||||
<div class="room-container" class:sharing={$screenSharing} class:many={columns > 3} class:hidden={loading}>
|
||||
<div class="screenContainer">
|
||||
<video class="screen" bind:this={screen}></video>
|
||||
</div>
|
||||
{#if withVideo}
|
||||
<div
|
||||
bind:clientWidth={roomWidth}
|
||||
bind:clientHeight={roomHeight}
|
||||
class="videoGrid"
|
||||
style={$screenSharing ? '' : gridStyle}
|
||||
class:scroll-m-0={$screenSharing}
|
||||
>
|
||||
{#each participants as participant, i (participant._id)}
|
||||
<ParticipantView
|
||||
bind:this={participantElements[i]}
|
||||
{...participant}
|
||||
small={$screenSharing ||
|
||||
(!$screenSharing &&
|
||||
((columns > 1 && (roomWidth - 16 * (columns - 1)) / columns < 300) ||
|
||||
(rows > 1 && (roomHeight - 16 * (rows - 1)) / rows < 168)))}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $currentRoom}
|
||||
<ControlBar room={$currentRoom} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.error {
|
||||
font-size: 500;
|
||||
font-size: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.room-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
.screenContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
.screen {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
&:not(.sharing) {
|
||||
gap: 0;
|
||||
|
||||
.videoGrid {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.screenContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.sharing {
|
||||
gap: 1rem;
|
||||
|
||||
.videoGrid {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0 0.5rem;
|
||||
width: 15rem;
|
||||
min-width: 15rem;
|
||||
min-height: 0;
|
||||
max-width: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.many {
|
||||
padding: 0.5rem;
|
||||
|
||||
&:not(.sharing) .videoGrid,
|
||||
&.sharing {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { RadioGroup, RadioItem } from '@hcengineering/ui'
|
||||
import { Room, RoomAccess, isOffice, roomAccessLabel } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let room: Room
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function setAccess (access: RoomAccess): Promise<void> {
|
||||
await client.update(room, { access })
|
||||
dispatch('close', access)
|
||||
}
|
||||
|
||||
const items: RadioItem[] = isOffice(room)
|
||||
? [
|
||||
{
|
||||
value: RoomAccess.Knock,
|
||||
labelIntl: roomAccessLabel[RoomAccess.Knock],
|
||||
action: async () => {
|
||||
await setAccess(RoomAccess.Knock)
|
||||
}
|
||||
},
|
||||
{
|
||||
value: RoomAccess.DND,
|
||||
labelIntl: roomAccessLabel[RoomAccess.DND],
|
||||
action: async () => {
|
||||
await setAccess(RoomAccess.DND)
|
||||
}
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: RoomAccess.Open,
|
||||
labelIntl: roomAccessLabel[RoomAccess.Open],
|
||||
action: async () => {
|
||||
await setAccess(RoomAccess.Open)
|
||||
}
|
||||
},
|
||||
{
|
||||
value: RoomAccess.Knock,
|
||||
labelIntl: roomAccessLabel[RoomAccess.Knock],
|
||||
action: async () => {
|
||||
await setAccess(RoomAccess.Knock)
|
||||
}
|
||||
},
|
||||
{
|
||||
value: RoomAccess.DND,
|
||||
labelIntl: roomAccessLabel[RoomAccess.DND],
|
||||
action: async () => {
|
||||
await setAccess(RoomAccess.DND)
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4">
|
||||
<RadioGroup {items} gap={'large'} selected={room.access} />
|
||||
</div>
|
||||
@@ -0,0 +1,243 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Contact, Person } from '@hcengineering/contact'
|
||||
import { AssigneeBox, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ActionIcon, EditBox, Icon, IconDelete, resizeObserver } from '@hcengineering/ui'
|
||||
import { Room, RoomAccess, RoomType, isOffice } from '@hcengineering/love'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import love from '../plugin'
|
||||
import { infos, lockedRoom } from '../stores'
|
||||
import { RoomSide, shadowNormal } from '../types'
|
||||
import { getRoomLabel } from '../utils'
|
||||
|
||||
export let room: Room
|
||||
export let cellSize: number
|
||||
export let top: number | undefined = undefined
|
||||
export let left: number | undefined = undefined
|
||||
export let placed: boolean = false
|
||||
export let excludedPersons: Ref<Contact>[] = []
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const SHADOW_OFFSET = 3
|
||||
const SHADOW_BLUR = 2
|
||||
const SHADOW_SPREAD = -1
|
||||
|
||||
async function updateName (): Promise<void> {
|
||||
await client.update(room, { name: room.name })
|
||||
}
|
||||
|
||||
async function changePerson (e: CustomEvent<Ref<Person> | null>): Promise<void> {
|
||||
if (isOffice(room)) {
|
||||
const value = e.detail
|
||||
const prevValue = room.person
|
||||
await client.update(room, { person: value })
|
||||
if (prevValue != null) {
|
||||
const currentInfo = $infos.find((p) => p.room === room._id && p.person === prevValue)
|
||||
if (currentInfo !== undefined) {
|
||||
await client.update(currentInfo, { room: love.ids.Reception })
|
||||
}
|
||||
}
|
||||
if (value != null) {
|
||||
const currentInfo = $infos.find((p) => p.room === love.ids.Reception && p.person === value)
|
||||
if (currentInfo !== undefined) {
|
||||
await client.update(currentInfo, { room: room._id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove (): Promise<void> {
|
||||
await client.remove(room)
|
||||
}
|
||||
|
||||
$: removable = $infos.filter((i) => i.room === room._id).length === 0
|
||||
$: dispatch('cursor', cursor)
|
||||
$: zoomOut = cellSize < 40
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let container: HTMLDivElement
|
||||
let cursor: string = 'default'
|
||||
let dragStyle: string | undefined = undefined
|
||||
const roomSide: RoomSide = { top: false, bottom: false, left: false, right: false }
|
||||
let roomRect: DOMRect
|
||||
|
||||
const shadow = tweened({ x: 0, y: 0, r: 0, s: 0 }, { duration: 150, easing: cubicOut })
|
||||
const shadowColor = tweened(shadowNormal, { duration: 300, easing: cubicOut })
|
||||
|
||||
export function setShadow (x: number, y: number, r: number, s?: number) {
|
||||
shadow.set({ x, y, r, s: s ?? 0 })
|
||||
}
|
||||
export function setShadowColor (r: number, g: number, b: number, a: number) {
|
||||
shadowColor.set({ r, g, b, a })
|
||||
}
|
||||
export function clearShadow () {
|
||||
shadow.set({ x: 0, y: 0, r: 1, s: 0 })
|
||||
shadowColor.set({ ...shadowNormal, a: 0 })
|
||||
}
|
||||
export function getRect (): DOMRect {
|
||||
return container ? container.getBoundingClientRect() : new DOMRect()
|
||||
}
|
||||
function checkLeave () {
|
||||
if ($lockedRoom !== '') return
|
||||
cursor = 'default'
|
||||
clearShadow()
|
||||
}
|
||||
|
||||
function mouseDown (e: MouseEvent): void {
|
||||
if (container === undefined || $lockedRoom !== '') return
|
||||
roomRect = container.getBoundingClientRect()
|
||||
if (roomSide.top || roomSide.bottom || roomSide.left || roomSide.right) {
|
||||
dispatch('resize', { room, size: roomRect, side: roomSide })
|
||||
} else if (cursor === 'all-scroll') {
|
||||
dispatch('move', {
|
||||
room,
|
||||
size: roomRect,
|
||||
offset: {
|
||||
x: e.clientX - roomRect.left,
|
||||
y: e.clientY - roomRect.top
|
||||
}
|
||||
})
|
||||
}
|
||||
lockedRoom.set(room._id)
|
||||
}
|
||||
|
||||
function changeCursor (e: MouseEvent): void {
|
||||
if ($lockedRoom !== '') return
|
||||
roomRect = container.getBoundingClientRect()
|
||||
const offsetX = e.clientX - roomRect.x + cellSize / 5
|
||||
const offsetY = e.clientY - roomRect.y + (cellSize / 3) * 1.6
|
||||
roomSide.top = offsetY <= 12
|
||||
roomSide.left = offsetX <= 12
|
||||
roomSide.bottom = offsetY >= roomRect.height + (cellSize / 3) * 1.6 + cellSize / 5 + -12
|
||||
roomSide.right = offsetX >= roomRect.width + (cellSize / 5) * 2 - 12
|
||||
|
||||
if (roomSide.left) {
|
||||
if (roomSide.top) cursor = 'nwse-resize'
|
||||
else if (roomSide.bottom) cursor = 'nesw-resize'
|
||||
else cursor = 'ew-resize'
|
||||
} else if (roomSide.right) {
|
||||
if (roomSide.top) cursor = 'nesw-resize'
|
||||
else if (roomSide.bottom) cursor = 'nwse-resize'
|
||||
else cursor = 'ew-resize'
|
||||
} else if (roomSide.top || roomSide.bottom) cursor = 'ns-resize'
|
||||
else cursor = 'all-scroll'
|
||||
|
||||
shadow.set({
|
||||
x: roomSide.left ? -SHADOW_OFFSET : roomSide.right ? SHADOW_OFFSET : 0,
|
||||
y: roomSide.top ? -SHADOW_OFFSET : roomSide.bottom ? SHADOW_OFFSET : 0,
|
||||
r: roomSide.top || roomSide.bottom || roomSide.left || roomSide.right ? SHADOW_BLUR : 0,
|
||||
s: SHADOW_SPREAD
|
||||
})
|
||||
shadowColor.set(shadowNormal)
|
||||
}
|
||||
|
||||
$: showButtons = room.access === RoomAccess.DND || room.type === RoomType.Video || removable
|
||||
$: updateStyle(top, left, room, roomRect)
|
||||
|
||||
const updateStyle = (t: number | undefined, l: number | undefined, r: Room, rect: DOMRect): void => {
|
||||
dragStyle =
|
||||
t && l && r && rect
|
||||
? `top: ${top}px; left: ${left}px; width: ${room.width * cellSize}px; height: ${room.height * cellSize};`
|
||||
: undefined
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (container) roomRect = container.getBoundingClientRect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={container}
|
||||
use:resizeObserver={() => {
|
||||
roomRect = container.getBoundingClientRect()
|
||||
if ($lockedRoom === room._id) dispatch('updated', roomRect)
|
||||
}}
|
||||
class="floorGrid-configureRoom"
|
||||
class:dragged={top !== undefined || left !== undefined}
|
||||
class:placed
|
||||
style={dragStyle}
|
||||
style:--huly-floor-roomWidth={room.width}
|
||||
style:--huly-floor-roomHeight={room.height}
|
||||
style:--huly-floor-roomShadow={`var(--theme-popup-shadow), ${$shadow.x}px ${$shadow.y}px ${$shadow.r}px ${$shadow.s}px rgba(${$shadowColor.r}, ${$shadowColor.g}, ${$shadowColor.b}, ${$shadowColor.a})`}
|
||||
style:top={top ? `${top}px` : undefined}
|
||||
style:left={left ? `${left}px` : undefined}
|
||||
style:grid-column={`${room.x + 2} / span ${room.width}`}
|
||||
style:grid-row={`${room.y + 2} / span ${room.height}`}
|
||||
style:grid-template-columns={`repeat(${room.width}, 1fr)`}
|
||||
style:grid-template-rows={`repeat(${room.height}, 1fr)`}
|
||||
style:aspect-ratio={`${room.width} / ${room.height}`}
|
||||
style:pointer-events={$lockedRoom !== '' ? 'none' : 'all'}
|
||||
on:mousemove={changeCursor}
|
||||
on:mouseleave={checkLeave}
|
||||
on:mousedown={mouseDown}
|
||||
>
|
||||
{#each new Array(room.height) as _, rY}
|
||||
{#each new Array(room.width) as _, rX}
|
||||
<div
|
||||
class="floorGrid-configureRoom__field"
|
||||
class:null={!(isOffice(room) && rX === 0 && rY === 0)}
|
||||
on:mousedown={(e) => {
|
||||
if (isOffice(room) && rX === 0 && rY === 0) e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{#if isOffice(room) && rX === 0 && rY === 0}
|
||||
<AssigneeBox
|
||||
_class={contact.class.Person}
|
||||
excluded={excludedPersons}
|
||||
shouldShowName={false}
|
||||
showNavigate={false}
|
||||
width={'100%'}
|
||||
label={contact.string.Person}
|
||||
value={room.person}
|
||||
avatarSize={'full'}
|
||||
on:change={changePerson}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
<div class="floorGrid-configureRoom__header">
|
||||
<EditBox
|
||||
bind:value={room.name}
|
||||
on:change={updateName}
|
||||
placeholder={getRoomLabel(room, $personByIdStore)}
|
||||
kind={'editbox'}
|
||||
/>
|
||||
{#if showButtons}
|
||||
<div
|
||||
class="flex-row-center flex-no-shrink h-full {zoomOut ? 'flex-gap-1' : 'flex-gap-2'}"
|
||||
on:mousedown|stopPropagation
|
||||
>
|
||||
{#if room.access === RoomAccess.DND}
|
||||
<Icon icon={love.icon.DND} size={zoomOut ? 'x-small' : 'small'} />
|
||||
{/if}
|
||||
{#if room.type === RoomType.Video}
|
||||
<Icon icon={love.icon.CamEnabled} size={zoomOut ? 'x-small' : 'small'} />
|
||||
{/if}
|
||||
{#if removable}
|
||||
<ActionIcon icon={IconDelete} size={zoomOut ? 'x-small' : 'small'} action={remove} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,283 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { personByIdStore, UserInfo } from '@hcengineering/contact-resources'
|
||||
import { IdMap, getCurrentAccount } from '@hcengineering/core'
|
||||
import ui, {
|
||||
ModernButton,
|
||||
SplitButton,
|
||||
IconArrowLeft,
|
||||
IconUpOutline,
|
||||
Label,
|
||||
eventToHTMLElement,
|
||||
getCurrentLocation,
|
||||
location,
|
||||
navigate,
|
||||
showPopup,
|
||||
Scroller
|
||||
} from '@hcengineering/ui'
|
||||
import {
|
||||
ParticipantInfo,
|
||||
Room,
|
||||
RoomType,
|
||||
isOffice,
|
||||
loveId,
|
||||
roomAccessIcon,
|
||||
roomAccessLabel
|
||||
} from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, infos, invites, myInfo, myOffice, myRequests } from '../stores'
|
||||
import {
|
||||
getRoomName,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicEnabled,
|
||||
isSharingEnabled,
|
||||
leaveRoom,
|
||||
screenSharing,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare,
|
||||
tryConnect
|
||||
} from '../utils'
|
||||
import CamSettingPopup from './CamSettingPopup.svelte'
|
||||
import MicSettingPopup from './MicSettingPopup.svelte'
|
||||
import RoomAccessPopup from './RoomAccessPopup.svelte'
|
||||
|
||||
export let room: Room
|
||||
|
||||
function getPerson (info: ParticipantInfo | undefined, employees: IdMap<Person>): Person | undefined {
|
||||
if (info !== undefined) {
|
||||
return employees.get(info.person)
|
||||
}
|
||||
}
|
||||
|
||||
let joined: boolean = false
|
||||
$: joined = $myInfo?.room === room._id
|
||||
|
||||
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
|
||||
let info: ParticipantInfo[] = []
|
||||
$: info = $infos.filter((p) => p.room === room._id)
|
||||
|
||||
let allowCam: boolean = false
|
||||
const allowShare: boolean = true
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function changeMute (): Promise<void> {
|
||||
await setMic(!$isMicEnabled)
|
||||
}
|
||||
|
||||
async function changeCam (): Promise<void> {
|
||||
await setCam(!$isCameraEnabled)
|
||||
}
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
await setShare(!$isSharingEnabled)
|
||||
}
|
||||
|
||||
async function leave (): Promise<void> {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
async function connect (): Promise<void> {
|
||||
await tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites)
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function back (): void {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = loveId
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
function micSettings (e: MouseEvent): void {
|
||||
showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
function camSettings (e: MouseEvent): void {
|
||||
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
function setAccess (e: MouseEvent): void {
|
||||
showPopup(RoomAccessPopup, { room }, eventToHTMLElement(e), (res) => {
|
||||
if (res !== undefined) {
|
||||
room.access = res
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const me = (getCurrentAccount() as PersonAccount).person
|
||||
$: myRoom = isOffice(room) && room.person === me
|
||||
</script>
|
||||
|
||||
<div class="antiPopup room-popup">
|
||||
<div class="room-label"><Label label={love.string.Room} /></div>
|
||||
<div class="title overflow-label">
|
||||
{getRoomName(room, $personByIdStore)}
|
||||
</div>
|
||||
<div class="room-popup__content">
|
||||
<Scroller padding={'0.5rem'} stickedScrollBars>
|
||||
<div class="room-popup__content-grid">
|
||||
{#each info as inf}
|
||||
{@const person = getPerson(inf, $personByIdStore)}
|
||||
{#if person}
|
||||
<div class="person"><UserInfo value={person} size={'medium'} showStatus={false} /></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
{#if joined && $isConnected}
|
||||
<div class="room-btns" class:no-video={!allowCam}>
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
showTooltip={{ label: $isMicEnabled ? love.string.Mute : love.string.UnMute }}
|
||||
action={changeMute}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={micSettings}
|
||||
separate
|
||||
/>
|
||||
{#if allowCam}
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
showTooltip={{ label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo }}
|
||||
disabled={!$isConnected}
|
||||
action={changeCam}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={camSettings}
|
||||
separate
|
||||
/>
|
||||
{/if}
|
||||
{#if allowShare}
|
||||
<ModernButton
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
label={$isSharingEnabled ? love.string.StopShare : love.string.Share}
|
||||
tooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
on:click={changeShare}
|
||||
/>
|
||||
{/if}
|
||||
<ModernButton
|
||||
icon={roomAccessIcon[room.access]}
|
||||
label={roomAccessLabel[room.access]}
|
||||
tooltip={{ label: love.string.ChangeAccess }}
|
||||
kind={'secondary'}
|
||||
size={'large'}
|
||||
disabled={isOffice(room) && room.person !== me}
|
||||
on:click={setAccess}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if (joined && !myRoom) || (!allowLeave && $myRequests.length === 0 && !myRoom)}
|
||||
<div class="btns flex-row-center flex-reverse flex-no-shrink w-full flex-gap-2">
|
||||
{#if allowLeave}
|
||||
<ModernButton
|
||||
label={love.string.LeaveRoom}
|
||||
icon={love.icon.LeaveRoom}
|
||||
size={'large'}
|
||||
kind={'negative'}
|
||||
on:click={leave}
|
||||
/>
|
||||
{:else if $myRequests.length === 0 && !myRoom}
|
||||
<ModernButton
|
||||
icon={love.icon.EnterRoom}
|
||||
label={love.string.EnterRoom}
|
||||
size={'large'}
|
||||
autoFocus
|
||||
on:click={connect}
|
||||
/>
|
||||
{/if}
|
||||
{#if $location.path[2] !== loveId}
|
||||
<ModernButton icon={IconArrowLeft} label={ui.string.Back} kind={'secondary'} size={'large'} on:click={back} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.room-popup {
|
||||
padding: var(--spacing-2);
|
||||
min-width: 35rem;
|
||||
max-width: 46rem;
|
||||
max-height: 40rem;
|
||||
|
||||
.room-label {
|
||||
margin-top: var(--spacing-0_5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-size: 1.5rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.room-popup__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--theme-popup-divider);
|
||||
border-radius: var(--medium-BorderRadius);
|
||||
|
||||
&-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1) var(--spacing-2);
|
||||
|
||||
.person {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--spacing-1) 0 var(--spacing-0_5);
|
||||
gap: var(--spacing-1);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
& + .btns {
|
||||
padding-top: var(--spacing-1_5);
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
padding-top: var(--spacing-2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { IdMap, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import { ParticipantInfo, Room, RoomAccess, RoomType } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { invites, myInfo, myRequests } from '../stores'
|
||||
import { getRoomLabel, invite, tryConnect } from '../utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let room: Room
|
||||
export let info: ParticipantInfo[]
|
||||
export let preview: boolean = false
|
||||
export let hovered: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const me = getCurrentAccount() as PersonAccount
|
||||
const meName = $personByIdStore.get(me.person)?.name
|
||||
const meAvatar = $personByIdStore.get(me.person)
|
||||
|
||||
let container: HTMLDivElement
|
||||
let hoveredRoomX: number | undefined = undefined
|
||||
let hoveredRoomY: number | undefined = undefined
|
||||
|
||||
$: disabled = room._class === love.class.Office && info.length === 0
|
||||
|
||||
function getPerson (info: ParticipantInfo | undefined, employees: IdMap<Person>): Person | undefined {
|
||||
if (info !== undefined) {
|
||||
return employees.get(info.person)
|
||||
}
|
||||
}
|
||||
|
||||
function getPersonInfo (y: number, x: number, info: ParticipantInfo[]): ParticipantInfo | undefined {
|
||||
return info.find((p) => p.x === x && p.y === y)
|
||||
}
|
||||
|
||||
function mouseEnter (): void {
|
||||
dispatch('hover', { name: getRoomLabel(room, $personByIdStore) })
|
||||
}
|
||||
|
||||
function clickHandler (x: number, y: number, person: Person | undefined): void {
|
||||
if (person !== undefined) {
|
||||
if (room._id === $myInfo?.room) return
|
||||
invite(person._id, $myInfo?.room)
|
||||
} else {
|
||||
tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites, { x, y })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
bind:this={container}
|
||||
class="floorGrid-room"
|
||||
class:preview
|
||||
class:hovered
|
||||
class:disabled
|
||||
class:myOffice={$myInfo?.room === room._id}
|
||||
style:--huly-floor-roomWidth={room.width}
|
||||
style:--huly-floor-roomHeight={room.height}
|
||||
style:grid-column={`${room.x + 2} / span ${room.width}`}
|
||||
style:grid-row={`${room.y + 2} / span ${room.height}`}
|
||||
style:grid-template-columns={`repeat(${room.width}, 1fr)`}
|
||||
style:grid-template-rows={`repeat(${room.height}, 1fr)`}
|
||||
style:aspect-ratio={`${room.width} / ${room.height}`}
|
||||
on:mouseenter|stopPropagation={mouseEnter}
|
||||
>
|
||||
{#each new Array(room.height) as _, y}
|
||||
{#each new Array(room.width) as _, x}
|
||||
{@const personInfo = getPersonInfo(y, x, info)}
|
||||
{@const person = getPerson(personInfo, $personByIdStore)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="floorGrid-room__field"
|
||||
class:hovered={hoveredRoomX === x && hoveredRoomY === y}
|
||||
class:person={personInfo || person || $myInfo?.room === room._id}
|
||||
on:mouseenter={() => {
|
||||
if (!(personInfo || person) && !disabled && $myInfo?.room !== room._id) {
|
||||
hoveredRoomX = x
|
||||
hoveredRoomY = y
|
||||
}
|
||||
}}
|
||||
on:mouseout={() => {
|
||||
hoveredRoomX = undefined
|
||||
hoveredRoomY = undefined
|
||||
}}
|
||||
on:click={() => {
|
||||
clickHandler(x, y, person)
|
||||
}}
|
||||
>
|
||||
{#if personInfo}
|
||||
<Avatar name={person?.name ?? personInfo.name} {person} size={'full'} />
|
||||
{:else if hoveredRoomX === x && hoveredRoomY === y}
|
||||
<Avatar name={meName} person={meAvatar} size={'full'} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if !preview}
|
||||
<div class="floorGrid-room__header">
|
||||
<span class="overflow-label text-md flex-grow">
|
||||
<Label label={getRoomLabel(room, $personByIdStore)} />
|
||||
</span>
|
||||
{#if room.access === RoomAccess.DND || room.type === RoomType.Video}
|
||||
<div class="flex-row-center flex-no-shrink h-full flex-gap-2">
|
||||
{#if room.access === RoomAccess.DND}
|
||||
<Icon icon={love.icon.DND} size={'small'} />
|
||||
{/if}
|
||||
{#if room.type === RoomType.Video}
|
||||
<Icon icon={love.icon.CamEnabled} size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { getCurrentAccount, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Breadcrumb, Header, Label, Toggle } from '@hcengineering/ui'
|
||||
import { DevicesPreference } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { myPreferences } from '../stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { krispProcessor } from '../utils'
|
||||
import { isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function saveMicPreference (myPreferences: DevicesPreference | undefined, value: boolean): Promise<void> {
|
||||
if (myPreferences !== undefined) {
|
||||
await client.update(myPreferences, { micEnabled: !value })
|
||||
} else {
|
||||
const space = getCurrentAccount()._id as string as Ref<Space>
|
||||
await client.createDoc(love.class.DevicesPreference, space, {
|
||||
attachedTo: space,
|
||||
noiseCancellation: true,
|
||||
micEnabled: !value,
|
||||
camEnabled: true,
|
||||
blurRadius: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCamPreference (myPreferences: DevicesPreference | undefined, value: boolean): Promise<void> {
|
||||
if (myPreferences !== undefined) {
|
||||
await client.update(myPreferences, { camEnabled: !value })
|
||||
} else {
|
||||
const space = getCurrentAccount()._id as string as Ref<Space>
|
||||
await client.createDoc(love.class.DevicesPreference, space, {
|
||||
attachedTo: space,
|
||||
noiseCancellation: true,
|
||||
camEnabled: !value,
|
||||
micEnabled: true,
|
||||
blurRadius: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNoiseCancellationPreference (
|
||||
myPreferences: DevicesPreference | undefined,
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
if (myPreferences !== undefined) {
|
||||
await client.update(myPreferences, { noiseCancellation: value })
|
||||
} else {
|
||||
const space = getCurrentAccount()._id as string as Ref<Space>
|
||||
await client.createDoc(love.class.DevicesPreference, space, {
|
||||
attachedTo: space,
|
||||
noiseCancellation: value,
|
||||
camEnabled: true,
|
||||
micEnabled: true,
|
||||
blurRadius: 0
|
||||
})
|
||||
}
|
||||
await krispProcessor.setEnabled(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent">
|
||||
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
|
||||
<Breadcrumb icon={love.icon.Love} label={love.string.Settings} size={'large'} isCurrent />
|
||||
</Header>
|
||||
<div class="flex-row-stretch flex-grow p-10">
|
||||
<div class="flex-grow flex-col flex-gap-4">
|
||||
<div class="flex-row-center flex-gap-4">
|
||||
<Label label={love.string.StartWithMutedMic} />
|
||||
<Toggle
|
||||
on={!$myPreferences?.micEnabled ?? false}
|
||||
on:change={(e) => {
|
||||
saveMicPreference($myPreferences, e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-4">
|
||||
<Label label={love.string.StartWithoutVideo} />
|
||||
<Toggle
|
||||
on={!$myPreferences?.camEnabled ?? false}
|
||||
on:change={(e) => {
|
||||
saveCamPreference($myPreferences, e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-4">
|
||||
<Label label={love.string.NoiseCancellation} />
|
||||
<Toggle
|
||||
disabled={!isKrispNoiseFilterSupported()}
|
||||
on={isKrispNoiseFilterSupported() && ($myPreferences?.noiseCancellation ?? true)}
|
||||
showTooltip={!isKrispNoiseFilterSupported()
|
||||
? { label: love.string.NoiseCancellationNotSupported }
|
||||
: undefined}
|
||||
on:change={(e) => {
|
||||
saveNoiseCancellationPreference($myPreferences, e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,404 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ActionIcon, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { RoomType, Room as TypeRoom } from '@hcengineering/love'
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
RoomEvent,
|
||||
Track,
|
||||
TrackPublication
|
||||
} from 'livekit-client'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, infos, myInfo, myOffice } from '../stores'
|
||||
import {
|
||||
awaitConnect,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicEnabled,
|
||||
isSharingEnabled,
|
||||
leaveRoom,
|
||||
lk,
|
||||
screenSharing,
|
||||
setCam,
|
||||
setMic,
|
||||
setShare
|
||||
} from '../utils'
|
||||
import ParticipantView from './ParticipantView.svelte'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { MessageBox } from '@hcengineering/presentation'
|
||||
|
||||
export let isDock: boolean = false
|
||||
export let room: Ref<TypeRoom>
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
name: string
|
||||
muted: boolean
|
||||
mirror: boolean
|
||||
connecting: boolean
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let participants: ParticipantData[] = []
|
||||
const participantElements: ParticipantView[] = []
|
||||
let screen: HTMLVideoElement
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source === Track.Source.ScreenShare) {
|
||||
track.attach(screen)
|
||||
} else {
|
||||
const element = track.attach()
|
||||
attachTrack(element, participant)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
if (track.source !== Track.Source.ScreenShare) {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleLocalTrack (publication: LocalTrackPublication, participant: LocalParticipant): void {
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
void attachTrack(element, participant)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTrack (element: HTMLMediaElement, participant: Participant): Promise<void> {
|
||||
let index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index === -1) {
|
||||
index = participants.push({
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false
|
||||
})
|
||||
}
|
||||
participants = participants
|
||||
participantElements.length = participants.length
|
||||
await tick()
|
||||
participantElements[index].appendChild(element)
|
||||
}
|
||||
|
||||
function attachParticipant (participant: Participant): void {
|
||||
const current = participants.find((p) => p._id === participant.identity)
|
||||
if (current !== undefined) {
|
||||
current.connecting = false
|
||||
current.muted = !participant.isMicrophoneEnabled
|
||||
current.mirror = participant.isLocal
|
||||
participants = participants
|
||||
return
|
||||
}
|
||||
const value: ParticipantData = {
|
||||
_id: participant.identity,
|
||||
name: participant.name ?? '',
|
||||
muted: !participant.isMicrophoneEnabled,
|
||||
mirror: participant.isLocal,
|
||||
connecting: false
|
||||
}
|
||||
participants.push(value)
|
||||
participants = participants
|
||||
}
|
||||
|
||||
function handleParticipantDisconnected (participant: RemoteParticipant): void {
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
|
||||
function muteHandler (publication: TrackPublication, participant: Participant): void {
|
||||
if (publication.kind === Track.Kind.Video) {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
return
|
||||
}
|
||||
const index = participants.findIndex((p) => p._id === participant.identity)
|
||||
if (index !== -1) {
|
||||
participantElements[index].setTrackMuted(publication.isMuted)
|
||||
}
|
||||
} else {
|
||||
const part = participants.find((p) => p._id === participant.identity)
|
||||
if (part !== undefined) {
|
||||
part.muted = publication.isMuted
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await awaitConnect()
|
||||
for (const participant of lk.remoteParticipants.values()) {
|
||||
attachParticipant(participant)
|
||||
for (const publication of participant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachParticipant(lk.localParticipant)
|
||||
for (const publication of lk.localParticipant.trackPublications.values()) {
|
||||
if (publication.track !== undefined && publication.track.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
publication.track.attach(screen)
|
||||
} else {
|
||||
const element = publication.track.attach()
|
||||
await attachTrack(element, lk.localParticipant)
|
||||
}
|
||||
}
|
||||
}
|
||||
lk.on(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.on(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackUnmuted, muteHandler)
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
})
|
||||
|
||||
onDestroy(
|
||||
infos.subscribe((data) => {
|
||||
for (const info of data) {
|
||||
if (info.room !== room) continue
|
||||
const current = participants.find((p) => p._id === info.person)
|
||||
if (current !== undefined) continue
|
||||
const value: ParticipantData = {
|
||||
_id: info.person,
|
||||
name: info.name,
|
||||
muted: true,
|
||||
mirror: false,
|
||||
connecting: true
|
||||
}
|
||||
participants.push(value)
|
||||
}
|
||||
participants = participants
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
lk.off(RoomEvent.ParticipantConnected, attachParticipant)
|
||||
lk.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.off(RoomEvent.LocalTrackPublished, handleLocalTrack)
|
||||
lk.off(RoomEvent.TrackMuted, muteHandler)
|
||||
lk.off(RoomEvent.TrackUnmuted, muteHandler)
|
||||
})
|
||||
|
||||
let divScroll: HTMLElement
|
||||
let allowCam: boolean = false
|
||||
let allowLeave: boolean = false
|
||||
|
||||
$: allowCam = $currentRoom?.type === RoomType.Video
|
||||
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
|
||||
async function changeMute (): Promise<void> {
|
||||
if (!$isConnected) return
|
||||
await setMic(!$isMicEnabled)
|
||||
}
|
||||
|
||||
async function changeCam (): Promise<void> {
|
||||
if (!$isConnected || !allowCam) return
|
||||
await setCam(!$isCameraEnabled)
|
||||
}
|
||||
|
||||
export function canClose (): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
async function leave (): Promise<void> {
|
||||
showPopup(MessageBox, {
|
||||
label: love.string.LeaveRoom,
|
||||
message: love.string.LeaveRoomConfirmation,
|
||||
action: async () => {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
if (!$isConnected) return
|
||||
await setShare(!$isSharingEnabled)
|
||||
}
|
||||
|
||||
$: dispatchFit($isSharingEnabled)
|
||||
$: dispatchFit(participants.length > 0)
|
||||
const dispatchFit = (_?: boolean): void => {
|
||||
setTimeout(() => {
|
||||
if (divScroll) {
|
||||
const notFit: number = divScroll.scrollHeight - divScroll.clientHeight
|
||||
dispatch('changeContent', { notFit })
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup videoPopup-container" class:isDock>
|
||||
<div class="header">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
size={'small'}
|
||||
action={changeMute}
|
||||
disabled={!$isConnected}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Cam : $isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
size={'small'}
|
||||
action={changeCam}
|
||||
disabled={!$isConnected || !allowCam}
|
||||
/>
|
||||
{#if $isConnected}
|
||||
<ActionIcon
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
label={$isSharingEnabled ? love.string.StopShare : love.string.Share}
|
||||
disabled={$screenSharing && !$isSharingEnabled}
|
||||
size={'small'}
|
||||
action={changeShare}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ActionIcon
|
||||
icon={view.icon.Pin}
|
||||
label={isDock ? view.string.Unpin : view.string.Pin}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
dispatch('dock')
|
||||
}}
|
||||
/>
|
||||
{#if allowLeave}
|
||||
<ActionIcon
|
||||
icon={love.icon.LeaveRoom}
|
||||
iconProps={{ color: '#FF6711' }}
|
||||
label={love.string.LeaveRoom}
|
||||
size={'small'}
|
||||
action={leave}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="screenContainer" class:hidden={!$screenSharing || $isSharingEnabled}>
|
||||
<video class="screen" bind:this={screen}></video>
|
||||
</div>
|
||||
<Scroller bind:divScroll noStretch padding={'0 .5rem'} gap={'flex-gap-2'} onResize={dispatchFit} stickedScrollBars>
|
||||
{#each participants as participant, i (participant._id)}
|
||||
<div class="video">
|
||||
<ParticipantView bind:this={participantElements[i]} {...participant} small />
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
||||
<div class="antiNav-space" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.videoPopup-container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 15rem;
|
||||
user-select: none;
|
||||
|
||||
&:not(.isDock) {
|
||||
border-radius: var(--large-BorderRadius);
|
||||
}
|
||||
&.isDock {
|
||||
background-color: var(--theme-statusbar-color);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
margin: 0.5rem 1rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.screenContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin: 0 0.5rem 0.5rem;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
.screen {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { rootBarExtensions } from '@hcengineering/ui'
|
||||
import { RemoteParticipant, RemoteTrack, RemoteTrackPublication, RoomEvent, Track } from 'livekit-client'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { disconnect, isCurrentInstanceConnected, lk } from '../utils'
|
||||
|
||||
let parentElement: HTMLDivElement
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
const element = track.attach()
|
||||
element.id = publication.trackSid
|
||||
parentElement.appendChild(element)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
const element = document.getElementById(publication.trackSid)
|
||||
if (element != null) {
|
||||
parentElement.removeChild(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
rootBarExtensions.update((cur) => {
|
||||
if (cur.find((p) => p[1] === love.component.ControlExt) === undefined) {
|
||||
cur.push(['left', love.component.ControlExt])
|
||||
}
|
||||
return cur
|
||||
})
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
})
|
||||
|
||||
onDestroy(async () => {
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
if ($isCurrentInstanceConnected) {
|
||||
await disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={parentElement}></div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M16 24.0001C15.8345 24.0001 15.6715 23.9589 15.5259 23.8801L3.40621 17.3541C2.92006 17.0923 2.73823 16.486 3.00011 15.9999C3.26191 15.514 3.86801 15.3322 4.35401 15.5939L16 21.8644L27.6456 15.5941C28.1318 15.3323 28.7381 15.5142 29 16.0004C29.2618 16.4866 29.0799 17.0931 28.5937 17.3549L16.4741 23.8808C16.3284 23.9593 16.1655 24.0003 16 24.0001Z"
|
||||
/>
|
||||
<path
|
||||
d="M16 30.0001C15.8345 30.0001 15.6715 29.9589 15.5259 29.8801L3.40621 23.3541C2.92006 23.0923 2.73823 22.486 3.00011 21.9999C3.26191 21.514 3.86801 21.3322 4.35401 21.5939L16 27.8644L27.6456 21.5941C28.1318 21.3323 28.7381 21.5142 29 22.0004C29.2618 22.4866 29.0799 23.0931 28.5937 23.3549L16.4741 29.8808C16.3284 29.9593 16.1655 30.0003 16 30.0001Z"
|
||||
/>
|
||||
<path
|
||||
d="M16 18.0001C15.8345 18.0001 15.6715 17.9589 15.5259 17.8801L2.5259 10.8801C2.36698 10.7945 2.23419 10.6674 2.14164 10.5124C2.04909 10.3574 2.00023 10.1803 2.00023 9.99976C2.00023 9.81923 2.04909 9.64208 2.14164 9.48708C2.23419 9.33209 2.36698 9.20503 2.5259 9.11941L15.5259 2.11941C15.6716 2.04079 15.8345 1.99963 16 1.99963C16.1655 1.99963 16.3284 2.04079 16.4741 2.11941L29.4741 9.11941C29.633 9.20503 29.7658 9.33209 29.8584 9.48708C29.9509 9.64208 29.9998 9.81923 29.9998 9.99976C29.9998 10.1803 29.9509 10.3574 29.8584 10.5124C29.7658 10.6674 29.633 10.7945 29.4741 10.8801L16.4741 17.8801C16.3285 17.9589 16.1655 18.0001 16 18.0001ZM5.1094 10.0001L16 15.8644L26.8906 10.0001L16 4.13591L5.1094 10.0001Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import ControlExt from './components/ControlExt.svelte'
|
||||
import Main from './components/Main.svelte'
|
||||
import Settings from './components/Settings.svelte'
|
||||
import WorkbenchExtension from './components/WorkbenchExtension.svelte'
|
||||
import { toggleMic, toggleVideo } from './utils'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
Main,
|
||||
ControlExt,
|
||||
Settings,
|
||||
WorkbenchExtension
|
||||
},
|
||||
actionImpl: {
|
||||
ToggleMic: toggleMic,
|
||||
ToggleVideo: toggleVideo
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { mergeIds, type IntlString } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import love, { loveId } from '@hcengineering/love'
|
||||
|
||||
export default mergeIds(loveId, love, {
|
||||
component: {
|
||||
ControlExt: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
LeaveRoom: '' as IntlString,
|
||||
LeaveRoomConfirmation: '' as IntlString,
|
||||
Mute: '' as IntlString,
|
||||
UnMute: '' as IntlString,
|
||||
Share: '' as IntlString,
|
||||
StopShare: '' as IntlString,
|
||||
StartVideo: '' as IntlString,
|
||||
StopVideo: '' as IntlString,
|
||||
Floors: '' as IntlString,
|
||||
Floor: '' as IntlString,
|
||||
MyOffice: '' as IntlString,
|
||||
EditOffice: '' as IntlString,
|
||||
FinalizeEditing: '' as IntlString,
|
||||
ChangeFloor: '' as IntlString,
|
||||
Accept: '' as IntlString,
|
||||
Decline: '' as IntlString,
|
||||
ChangeAccess: '' as IntlString,
|
||||
AddAFloor: '' as IntlString,
|
||||
RenameAFloor: '' as IntlString,
|
||||
KnockingTo: '' as IntlString,
|
||||
Cancel: '' as IntlString,
|
||||
EnterRoom: '' as IntlString,
|
||||
Configure: '' as IntlString,
|
||||
MeetingRoom: '' as IntlString,
|
||||
TeamRoom: '' as IntlString,
|
||||
AnotherWindowError: '' as IntlString,
|
||||
Speaker: '' as IntlString,
|
||||
Microphone: '' as IntlString,
|
||||
Camera: '' as IntlString,
|
||||
Settings: '' as IntlString,
|
||||
LoveDescription: '' as IntlString,
|
||||
DefaultDevice: '' as IntlString,
|
||||
StartWithMutedMic: '' as IntlString,
|
||||
StartWithoutVideo: '' as IntlString,
|
||||
YouInivite: '' as IntlString,
|
||||
NoiseCancellation: '' as IntlString,
|
||||
NoiseCancellationNotSupported: '' as IntlString,
|
||||
Blur: '' as IntlString,
|
||||
BlurRadius: '' as IntlString,
|
||||
BlurTooltip: '' as IntlString,
|
||||
GuestLink: '' as IntlString,
|
||||
CopyGuestLink: '' as IntlString,
|
||||
Record: '' as IntlString,
|
||||
StopRecord: '' as IntlString,
|
||||
ServiceNotConfigured: '' as IntlString
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import { type Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import { getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
RequestStatus,
|
||||
type DevicesPreference,
|
||||
type Floor,
|
||||
type Invite,
|
||||
type JoinRequest,
|
||||
type Office,
|
||||
type ParticipantInfo,
|
||||
type Room
|
||||
} from '@hcengineering/love'
|
||||
import { derived, writable } from 'svelte/store'
|
||||
import love from './plugin'
|
||||
|
||||
export const rooms = writable<Room[]>([])
|
||||
export const myOffice = derived(rooms, (val) => {
|
||||
const me = getCurrentAccount()
|
||||
const personId = (me as PersonAccount).person
|
||||
return val.find((p) => (p as Office).person === personId) as Office | undefined
|
||||
})
|
||||
export const infos = writable<ParticipantInfo[]>([])
|
||||
export const myInfo = derived(infos, (val) => {
|
||||
const me = getCurrentAccount()
|
||||
const personId = (me as PersonAccount).person
|
||||
return val.find((p) => p.person === personId)
|
||||
})
|
||||
export const currentRoom = derived([rooms, myInfo], ([rooms, myInfo]) => {
|
||||
return myInfo !== undefined ? rooms.find((p) => p._id === myInfo.room) : undefined
|
||||
})
|
||||
export const floors = writable<Floor[]>([])
|
||||
export const activeFloor = derived([rooms, myInfo, myOffice], ([rooms, myInfo, myOffice]) => {
|
||||
let res: Ref<Floor> | undefined
|
||||
if (myInfo !== undefined) {
|
||||
res = rooms.find((p) => p._id === myInfo.room)?.floor
|
||||
}
|
||||
if (res === undefined && myOffice !== undefined) {
|
||||
res = rooms.find((p) => p._id === myOffice._id)?.floor
|
||||
}
|
||||
return res ?? love.ids.MainFloor
|
||||
})
|
||||
export const myRequests = writable<JoinRequest[]>([])
|
||||
export const invites = writable<Invite[]>([])
|
||||
export const myInvites = derived([invites, myInfo], ([val, info]) => {
|
||||
const me = getCurrentAccount()
|
||||
const personId = (me as PersonAccount).person
|
||||
return val.filter((p) => p.target === personId && info?.room !== p.room)
|
||||
})
|
||||
export const activeInvites = derived(invites, (val) => {
|
||||
const me = getCurrentAccount()
|
||||
const personId = (me as PersonAccount).person
|
||||
return val.filter((p) => p.from === personId)
|
||||
})
|
||||
|
||||
export const myPreferences = writable<DevicesPreference | undefined>()
|
||||
export let $myPreferences: DevicesPreference | undefined
|
||||
|
||||
function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] {
|
||||
const map = new Map<Ref<Person>, ParticipantInfo>()
|
||||
for (const val of value) {
|
||||
map.set(val.person, val)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
function fillStores (): void {
|
||||
const client = getClient()
|
||||
if (client !== undefined) {
|
||||
const query = createQuery(true)
|
||||
query.query(love.class.Room, {}, (res) => {
|
||||
rooms.set(res)
|
||||
})
|
||||
const statusQuery = createQuery(true)
|
||||
statusQuery.query(love.class.ParticipantInfo, {}, (res) => {
|
||||
infos.set(filterParticipantInfo(res))
|
||||
})
|
||||
const floorsQuery = createQuery(true)
|
||||
floorsQuery.query(love.class.Floor, {}, (res) => {
|
||||
floors.set(res)
|
||||
})
|
||||
const requestsQuery = createQuery(true)
|
||||
requestsQuery.query(
|
||||
love.class.JoinRequest,
|
||||
{ person: (getCurrentAccount() as PersonAccount).person, status: RequestStatus.Pending },
|
||||
(res) => {
|
||||
myRequests.set(res)
|
||||
}
|
||||
)
|
||||
const preferencesQuery = createQuery(true)
|
||||
preferencesQuery.query(love.class.DevicesPreference, {}, (res) => {
|
||||
myPreferences.set(res[0])
|
||||
$myPreferences = res[0]
|
||||
})
|
||||
const invitesQuery = createQuery(true)
|
||||
invitesQuery.query(love.class.Invite, { status: RequestStatus.Pending }, (res) => {
|
||||
invites.set(res)
|
||||
})
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
fillStores()
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
fillStores()
|
||||
|
||||
export const lockedRoom = writable<string>('')
|
||||
@@ -0,0 +1,43 @@
|
||||
import { type DefSeparators } from '@hcengineering/ui'
|
||||
|
||||
export interface ResizeInitParams {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface FloorSize {
|
||||
cols: number
|
||||
rows: number
|
||||
width: number
|
||||
height: number
|
||||
cellSize: number
|
||||
cellTop: number
|
||||
cellRound: number
|
||||
}
|
||||
export interface RoomSide {
|
||||
top: boolean
|
||||
bottom: boolean
|
||||
left: boolean
|
||||
right: boolean
|
||||
}
|
||||
|
||||
export interface Slot {
|
||||
_id: string
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface RGBAColor {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
a: number
|
||||
}
|
||||
export const shadowNormal: RGBAColor = { r: 81, g: 144, b: 236, a: 1 }
|
||||
export const shadowError: RGBAColor = { r: 249, g: 110, b: 80, a: 1 }
|
||||
|
||||
export const loveSeparators: DefSeparators = [{ minSize: 17.5, size: 25, maxSize: 30, float: 'navigator' }, null]
|
||||
@@ -0,0 +1,710 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import contact, { getName, type Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import { concatLink, getCurrentAccount, type IdMap, type Ref, type Space } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, navigate, type DropdownTextItem } from '@hcengineering/ui'
|
||||
import { KrispNoiseFilter, isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
|
||||
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
|
||||
import {
|
||||
RequestStatus,
|
||||
RoomAccess,
|
||||
RoomType,
|
||||
isOffice,
|
||||
loveId,
|
||||
type Invite,
|
||||
type JoinRequest,
|
||||
type Office,
|
||||
type ParticipantInfo,
|
||||
type Room
|
||||
} from '@hcengineering/love'
|
||||
import {
|
||||
ConnectionState,
|
||||
Room as LKRoom,
|
||||
LocalAudioTrack,
|
||||
LocalVideoTrack,
|
||||
RoomEvent,
|
||||
Track,
|
||||
type AudioCaptureOptions,
|
||||
type LocalTrackPublication,
|
||||
type RemoteParticipant,
|
||||
type RemoteTrack,
|
||||
type RemoteTrackPublication,
|
||||
type VideoCaptureOptions
|
||||
} from 'livekit-client'
|
||||
import { get, writable } from 'svelte/store'
|
||||
import { sendMessage } from './broadcast'
|
||||
import love from './plugin'
|
||||
import { $myPreferences, currentRoom } from './stores'
|
||||
|
||||
export const selectedCamId = 'selectedDevice_cam'
|
||||
export const selectedMicId = 'selectedDevice_mic'
|
||||
export const selectedSpeakerId = 'selectedDevice_speaker'
|
||||
|
||||
export async function getToken (
|
||||
roomName: string,
|
||||
roomId: Ref<Room>,
|
||||
userId: string,
|
||||
participantName: string
|
||||
): Promise<string> {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
const res = await fetch(concatLink(endpoint, '/getToken'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName: getTokenRoomName(roomName, roomId), _id: userId, participantName })
|
||||
})
|
||||
return await res.text()
|
||||
}
|
||||
|
||||
function getTokenRoomName (roomName: string, roomId: Ref<Room>): string {
|
||||
const loc = getCurrentLocation()
|
||||
return `${loc.path[1]}_${roomName}_${roomId}`
|
||||
}
|
||||
|
||||
export const lk: LKRoom = new LKRoom({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
publishDefaults: {
|
||||
videoCodec: 'vp9',
|
||||
screenShareEncoding: {
|
||||
maxBitrate: 7_000_000,
|
||||
maxFramerate: 15,
|
||||
priority: 'high'
|
||||
}
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
autoGainControl: true,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: localStorage.getItem(selectedSpeakerId) ?? undefined
|
||||
},
|
||||
videoCaptureDefaults: {
|
||||
facingMode: 'user',
|
||||
resolution: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function prepare (): Promise<void> {
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
if (wsURL !== undefined) {
|
||||
await lk.prepareConnection(wsURL)
|
||||
}
|
||||
}
|
||||
|
||||
void prepare()
|
||||
|
||||
export const isConnected = writable<boolean>(false)
|
||||
export const isCurrentInstanceConnected = writable<boolean>(false)
|
||||
export let $isCurrentInstanceConnected: boolean = false
|
||||
isCurrentInstanceConnected.subscribe((value) => {
|
||||
$isCurrentInstanceConnected = value
|
||||
})
|
||||
export const screenSharing = writable<boolean>(false)
|
||||
export const isRecording = writable<boolean>(false)
|
||||
export const isRecordingAvailable = writable<boolean | undefined>(undefined)
|
||||
export const isMicEnabled = writable<boolean>(false)
|
||||
export const isCameraEnabled = writable<boolean>(false)
|
||||
export const isSharingEnabled = writable<boolean>(false)
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, (pub, part) => {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
} else {
|
||||
isCameraEnabled.set(true)
|
||||
sendMessage({ type: 'cam', value: true })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
isMicEnabled.set(!pub.track?.isMuted)
|
||||
sendMessage({ type: 'mic', value: !pub.track?.isMuted })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, (pub, part) => {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else {
|
||||
isCameraEnabled.set(false)
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
isMicEnabled.set(false)
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.TrackMuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else {
|
||||
isCameraEnabled.set(false)
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
isMicEnabled.set(false)
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnmuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
} else {
|
||||
isCameraEnabled.set(true)
|
||||
sendMessage({ type: 'cam', value: true })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
isMicEnabled.set(true)
|
||||
sendMessage({ type: 'mic', value: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const krispProcessor = KrispNoiseFilter()
|
||||
export let blurProcessor: ProcessorWrapper<BackgroundOptions> | undefined
|
||||
let localVideo: LocalVideoTrack | undefined
|
||||
|
||||
try {
|
||||
blurProcessor = BackgroundBlur()
|
||||
} catch (err) {
|
||||
console.log("Can't set blur processor", err)
|
||||
}
|
||||
|
||||
async function setKrispProcessor (pub: LocalTrackPublication): Promise<void> {
|
||||
if (pub.track instanceof LocalAudioTrack) {
|
||||
if (!isKrispNoiseFilterSupported()) {
|
||||
console.warn('enhanced noise filter is currently not supported on this browser')
|
||||
return
|
||||
}
|
||||
try {
|
||||
// once instantiated the filter will begin initializing and will download additional resources
|
||||
console.log('enabling LiveKit enhanced noise filter')
|
||||
await pub.track.setProcessor(krispProcessor)
|
||||
await krispProcessor.setEnabled($myPreferences?.noiseCancellation ?? true)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setBlurProcessor (pub: LocalTrackPublication): Promise<void> {
|
||||
if (pub.track instanceof LocalVideoTrack) {
|
||||
if (blurProcessor !== undefined) {
|
||||
localVideo = pub.track
|
||||
const radius = $myPreferences?.blurRadius ?? 0.1
|
||||
if (radius >= 0.5) {
|
||||
try {
|
||||
await blurProcessor.updateTransformerOptions({ blurRadius: radius })
|
||||
await pub.track.setProcessor(blurProcessor)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateBlurRadius (value: number): Promise<void> {
|
||||
const client = getClient()
|
||||
if ($myPreferences !== undefined) {
|
||||
await client.update($myPreferences, { blurRadius: value })
|
||||
} else {
|
||||
const space = getCurrentAccount()._id as string as Ref<Space>
|
||||
await client.createDoc(love.class.DevicesPreference, space, {
|
||||
attachedTo: space,
|
||||
noiseCancellation: true,
|
||||
camEnabled: true,
|
||||
micEnabled: true,
|
||||
blurRadius: value
|
||||
})
|
||||
}
|
||||
try {
|
||||
if (blurProcessor !== undefined && localVideo !== undefined) {
|
||||
if (value < 0.5) {
|
||||
await localVideo.stopProcessor()
|
||||
} else {
|
||||
const current = localVideo.getProcessor()
|
||||
if (current !== undefined) {
|
||||
await blurProcessor.updateTransformerOptions({ blurRadius: value })
|
||||
} else {
|
||||
await blurProcessor.updateTransformerOptions({ blurRadius: value })
|
||||
await localVideo.setProcessor(blurProcessor)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
lk.on(RoomEvent.LocalTrackPublished, (pub) => {
|
||||
if (pub.track?.kind === Track.Kind.Video && pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
sendMessage({ type: 'share', value: true })
|
||||
}
|
||||
|
||||
if (pub.source === Track.Source.Microphone) {
|
||||
void setKrispProcessor(pub)
|
||||
}
|
||||
|
||||
if (pub.source === Track.Source.Camera) {
|
||||
void setBlurProcessor(pub)
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, (pub) => {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'share', value: false })
|
||||
} else if (pub.track.source === Track.Source.Camera) {
|
||||
if (localVideo !== undefined) {
|
||||
localVideo = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.RecordingStatusChanged, (evt) => {
|
||||
isRecording.set(evt)
|
||||
})
|
||||
lk.on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
||||
try {
|
||||
const data = JSON.parse(metadata)
|
||||
if (data.recording !== undefined) {
|
||||
isRecording.set(data.recording)
|
||||
}
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.Connected, () => {
|
||||
isConnected.set(true)
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
isCurrentInstanceConnected.set(true)
|
||||
isRecording.set(lk.isRecording)
|
||||
})
|
||||
lk.on(RoomEvent.Disconnected, () => {
|
||||
isConnected.set(false)
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
isCurrentInstanceConnected.set(false)
|
||||
})
|
||||
|
||||
export async function connect (name: string, room: Room, _id: string): Promise<void> {
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
if (wsURL === undefined) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const token = await getToken(room.name, room._id, _id, name)
|
||||
await lk.connect(wsURL, token)
|
||||
sendMessage({ type: 'connect', value: true })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function awaitConnect (): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (lk.state === ConnectionState.Connected) {
|
||||
resolve()
|
||||
}
|
||||
lk.once(RoomEvent.Connected, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function disconnect (): Promise<void> {
|
||||
const me = lk.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
await lk.disconnect()
|
||||
screenSharing.set(false)
|
||||
isMicEnabled.set(false)
|
||||
isCameraEnabled.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
sendMessage({ type: 'mic', value: false })
|
||||
sendMessage({ type: 'cam', value: false })
|
||||
sendMessage({ type: 'share', value: false })
|
||||
sendMessage({ type: 'connect', value: false })
|
||||
}
|
||||
|
||||
export async function leaveRoom (ownInfo: ParticipantInfo | undefined, ownOffice: Office | undefined): Promise<void> {
|
||||
const me = lk.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
if (ownInfo !== undefined) {
|
||||
const client = getClient()
|
||||
if (ownOffice !== undefined && ownInfo.room !== ownOffice._id) {
|
||||
await client.update(ownInfo, { room: ownOffice._id, x: 0, y: 0 })
|
||||
} else if (ownOffice === undefined) {
|
||||
await client.update(ownInfo, { room: love.ids.Reception, x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
await disconnect()
|
||||
}
|
||||
|
||||
export async function setCam (value: boolean): Promise<void> {
|
||||
if (value && get(currentRoom)?.type !== RoomType.Video) return
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
const opt: VideoCaptureOptions = {}
|
||||
const selectedDevice = localStorage.getItem(selectedCamId)
|
||||
if (selectedDevice !== null) {
|
||||
const devices = await LKRoom.getLocalDevices('videoinput')
|
||||
const available = devices.find((p) => p.deviceId === selectedDevice)
|
||||
if (available !== undefined) {
|
||||
opt.deviceId = available.deviceId
|
||||
}
|
||||
}
|
||||
await lk.localParticipant.setCameraEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_cam', value })
|
||||
}
|
||||
}
|
||||
|
||||
export async function setMic (value: boolean): Promise<void> {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
const opt: AudioCaptureOptions = {}
|
||||
const selectedDevice = localStorage.getItem(selectedMicId)
|
||||
if (selectedDevice !== null) {
|
||||
const devices = await LKRoom.getLocalDevices('audioinput')
|
||||
const available = devices.find((p) => p.deviceId === selectedDevice)
|
||||
if (available !== undefined) {
|
||||
opt.deviceId = available.deviceId
|
||||
}
|
||||
}
|
||||
await lk.localParticipant.setMicrophoneEnabled(value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_mic', value })
|
||||
}
|
||||
}
|
||||
|
||||
export async function setShare (value: boolean): Promise<void> {
|
||||
if ($isCurrentInstanceConnected) {
|
||||
try {
|
||||
await lk.localParticipant.setScreenShareEnabled(value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_share', value })
|
||||
}
|
||||
}
|
||||
|
||||
export function getRoomName (room: Room, personByIdStore: IdMap<Person>): string {
|
||||
if (isOffice(room) && room.person !== null && room.name === '') {
|
||||
const employee = personByIdStore.get(room.person)
|
||||
if (employee !== undefined) {
|
||||
const client = getClient()
|
||||
return getName(client.getHierarchy(), employee)
|
||||
}
|
||||
}
|
||||
return room.name
|
||||
}
|
||||
|
||||
export function getRoomLabel (room: Room, personByIdStore: IdMap<Person>): IntlString {
|
||||
const name = getRoomName(room, personByIdStore)
|
||||
if (name !== '') return getEmbeddedLabel(name)
|
||||
return isOffice(room) ? love.string.Office : love.string.Room
|
||||
}
|
||||
|
||||
async function moveToRoom (
|
||||
x: number,
|
||||
y: number,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
currentPerson: Person,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
if (currentInfo !== undefined) {
|
||||
await client.diffUpdate(currentInfo, {
|
||||
x,
|
||||
y,
|
||||
room: room._id
|
||||
})
|
||||
} else {
|
||||
await client.createDoc(love.class.ParticipantInfo, love.space.Rooms, {
|
||||
x,
|
||||
y,
|
||||
room: room._id,
|
||||
person: currentPerson._id,
|
||||
name: currentPerson.name
|
||||
})
|
||||
}
|
||||
const loc = getCurrentLocation()
|
||||
if (room.type === RoomType.Video && loc.path[2] !== loveId) {
|
||||
loc.path[2] = loveId
|
||||
loc.path.length = 3
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectLK (currentPerson: Person, room: Room): Promise<void> {
|
||||
await connect(currentPerson.name, room, currentPerson._id)
|
||||
await Promise.all([
|
||||
setMic($myPreferences?.micEnabled ?? lk.remoteParticipants.size < 16),
|
||||
setCam(room.type === RoomType.Video && ($myPreferences?.camEnabled ?? true))
|
||||
])
|
||||
}
|
||||
|
||||
export async function connectRoom (
|
||||
x: number,
|
||||
y: number,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
currentPerson: Person,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
await disconnect()
|
||||
await moveToRoom(x, y, currentInfo, currentPerson, room)
|
||||
await connectLK(currentPerson, room)
|
||||
}
|
||||
|
||||
export const joinRequest: Ref<JoinRequest> | undefined = undefined
|
||||
const requestsQuery = createQuery(true)
|
||||
|
||||
export function getFreePlace (room: Room, info: ParticipantInfo[]): { x: number, y: number } {
|
||||
const me = getCurrentAccount()
|
||||
let y = 0
|
||||
while (true) {
|
||||
for (let x = 0; x < room.width; x++) {
|
||||
if (info.find((p) => p.x === x && p.y === y) === undefined) {
|
||||
if (x === 0 && y === 0 && isOffice(room)) {
|
||||
if (room.person === (me as PersonAccount).person) {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
} else {
|
||||
return { x, y }
|
||||
}
|
||||
}
|
||||
}
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateFloorSize (_rooms: Room[], preview?: boolean): number {
|
||||
let fH: number = 5
|
||||
_rooms.forEach((room) => {
|
||||
if (room.y + room.height + 2 > fH) fH = room.y + room.height + 2
|
||||
})
|
||||
return fH
|
||||
}
|
||||
|
||||
function checkPlace (room: Room, info: ParticipantInfo[], x: number, y: number): boolean {
|
||||
return !isOffice(room) && info.find((p) => p.x === x && p.y === y) === undefined
|
||||
}
|
||||
|
||||
export async function tryConnect (
|
||||
personByIdStore: IdMap<Person>,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
room: Room,
|
||||
info: ParticipantInfo[],
|
||||
currentRequests: JoinRequest[],
|
||||
currentInvites: Invite[],
|
||||
place?: { x: number, y: number }
|
||||
): Promise<void> {
|
||||
const me = getCurrentAccount()
|
||||
const currentPerson = personByIdStore.get((me as PersonAccount).person)
|
||||
if (currentPerson === undefined) return
|
||||
const client = getClient()
|
||||
if (!client.getHierarchy().hasMixin(currentPerson, contact.mixin.Employee)) return
|
||||
if (room._id === currentInfo?.room) return
|
||||
if (room.access === RoomAccess.DND) return
|
||||
const thisRoomRequest = currentRequests.find((p) => p.room === room._id)
|
||||
if (thisRoomRequest !== undefined) return
|
||||
for (const req of currentRequests) {
|
||||
await client.remove(req)
|
||||
}
|
||||
if (place !== undefined && !checkPlace(room, info, place.x, place.y)) {
|
||||
place = undefined
|
||||
}
|
||||
if (place === undefined) {
|
||||
place = getFreePlace(room, info)
|
||||
}
|
||||
const x: number = place.x
|
||||
const y: number = place.y
|
||||
if (isOffice(room)) {
|
||||
if (room.person === null) return
|
||||
// we should check that office owner in office
|
||||
const owner = room.person
|
||||
if (owner === currentPerson._id) {
|
||||
// it's our office if it's empty let's disconnect
|
||||
if (info.length === 0) {
|
||||
await leaveRoom(currentInfo, room)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const ownerInfo = info.find((p) => p.person === owner)
|
||||
if (ownerInfo?.room !== room._id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const invite of currentInvites) {
|
||||
await client.update(invite, { status: invite.room === room._id ? RequestStatus.Approved : RequestStatus.Rejected })
|
||||
}
|
||||
if (room.access === RoomAccess.Knock && (!isOffice(room) || room.person !== currentPerson._id)) {
|
||||
const _id = await client.createDoc(love.class.JoinRequest, love.space.Rooms, {
|
||||
person: currentPerson._id,
|
||||
room: room._id,
|
||||
status: RequestStatus.Pending
|
||||
})
|
||||
requestsQuery.query(love.class.JoinRequest, { person: (me as PersonAccount).person, _id }, (res) => {
|
||||
const req = res[0]
|
||||
if (req.status === RequestStatus.Pending) return
|
||||
requestsQuery.unsubscribe()
|
||||
if (req.status === RequestStatus.Approved) {
|
||||
void connectRoom(x, y, currentInfo, currentPerson, room)
|
||||
}
|
||||
})
|
||||
// we should send request to room owner if it ouffice and all participants if not
|
||||
} else {
|
||||
await connectRoom(x, y, currentInfo, currentPerson, room)
|
||||
}
|
||||
}
|
||||
|
||||
export async function invite (person: Ref<Person>, room: Ref<Room> | undefined): Promise<void> {
|
||||
if (room === undefined || room === love.ids.Reception) return
|
||||
const client = getClient()
|
||||
const me = getCurrentAccount()
|
||||
await client.createDoc(love.class.Invite, love.space.Rooms, {
|
||||
target: person,
|
||||
room,
|
||||
status: RequestStatus.Pending,
|
||||
from: (me as PersonAccount).person
|
||||
})
|
||||
}
|
||||
|
||||
export function getActive (
|
||||
devices: DropdownTextItem[],
|
||||
activeId: string | undefined,
|
||||
prevId?: string | null
|
||||
): DropdownTextItem {
|
||||
if (activeId !== undefined) {
|
||||
const res = devices.find((p) => p.id === activeId)
|
||||
if (res !== undefined) return res
|
||||
}
|
||||
if (prevId != null) {
|
||||
const res = devices.find((p) => p.id === prevId)
|
||||
if (res !== undefined) return res
|
||||
}
|
||||
return devices[0]
|
||||
}
|
||||
|
||||
export async function toggleMic (): Promise<void> {
|
||||
await setMic(!get(isMicEnabled))
|
||||
}
|
||||
|
||||
export async function toggleVideo (): Promise<void> {
|
||||
await setCam(!get(isCameraEnabled))
|
||||
}
|
||||
|
||||
export async function record (room: Room): Promise<void> {
|
||||
try {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
if (token === undefined) {
|
||||
throw new Error('Token not found')
|
||||
}
|
||||
const roomName = getTokenRoomName(room.name, room._id)
|
||||
if (lk.isRecording) {
|
||||
await fetch(concatLink(endpoint, '/stopRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name })
|
||||
})
|
||||
} else {
|
||||
await fetch(concatLink(endpoint, '/startRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name })
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkRecordAvailable (): Promise<void> {
|
||||
try {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
|
||||
const res = await fetch(concatLink(endpoint, '/checkRecordAvailable'))
|
||||
const result = await res.json()
|
||||
isRecordingAvailable.set(result)
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
void checkRecordAvailable()
|
||||
@@ -0,0 +1,5 @@
|
||||
const sveltePreprocess = require('svelte-preprocess')
|
||||
|
||||
module.exports = {
|
||||
preprocess: sveltePreprocess()
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@hcengineering/love",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Uberflow Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"prettier-plugin-svelte": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/preference": "^0.6.9",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/drive": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/view": "^0.6.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { Drive } from '@hcengineering/drive'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform'
|
||||
import { Preference } from '@hcengineering/preference'
|
||||
import { Action } from '@hcengineering/view'
|
||||
|
||||
export const loveId = 'love' as Plugin
|
||||
|
||||
export const GRID_WIDTH = 15
|
||||
|
||||
export enum RoomAccess {
|
||||
Open,
|
||||
Knock,
|
||||
DND
|
||||
}
|
||||
|
||||
export enum RoomType {
|
||||
Video,
|
||||
Audio,
|
||||
Reception
|
||||
}
|
||||
|
||||
export interface Floor extends Doc {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Room extends Doc {
|
||||
name: string
|
||||
type: RoomType
|
||||
access: RoomAccess
|
||||
floor: Ref<Floor>
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Office extends Room {
|
||||
person: Ref<Person> | null
|
||||
}
|
||||
|
||||
// transient data for status
|
||||
export interface ParticipantInfo extends Doc {
|
||||
// isActive: boolean (disabled until server connection to check it for all active rooms)
|
||||
person: Ref<Person>
|
||||
name: string
|
||||
room: Ref<Room>
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface RoomInfo extends Doc {
|
||||
persons: Ref<Person>[]
|
||||
room: Ref<Room>
|
||||
isOffice: boolean
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected
|
||||
}
|
||||
|
||||
export interface JoinRequest extends Doc {
|
||||
person: Ref<Person>
|
||||
room: Ref<Room>
|
||||
status: RequestStatus
|
||||
}
|
||||
|
||||
export interface Invite extends Doc {
|
||||
from: Ref<Person>
|
||||
target: Ref<Person>
|
||||
room: Ref<Room>
|
||||
status: RequestStatus
|
||||
}
|
||||
|
||||
export interface DevicesPreference extends Preference {
|
||||
micEnabled: boolean
|
||||
noiseCancellation: boolean
|
||||
blurRadius: number
|
||||
camEnabled: boolean
|
||||
}
|
||||
|
||||
export * from './utils'
|
||||
|
||||
const love = plugin(loveId, {
|
||||
class: {
|
||||
Room: '' as Ref<Class<Room>>,
|
||||
Floor: '' as Ref<Class<Floor>>,
|
||||
Office: '' as Ref<Class<Office>>,
|
||||
ParticipantInfo: '' as Ref<Class<ParticipantInfo>>,
|
||||
JoinRequest: '' as Ref<Class<JoinRequest>>,
|
||||
DevicesPreference: '' as Ref<Class<DevicesPreference>>,
|
||||
RoomInfo: '' as Ref<Class<RoomInfo>>,
|
||||
Invite: '' as Ref<Class<Invite>>
|
||||
},
|
||||
action: {
|
||||
ToggleMic: '' as Ref<Action>,
|
||||
ToggleVideo: '' as Ref<Action>
|
||||
},
|
||||
string: {
|
||||
Office: '' as IntlString,
|
||||
Room: '' as IntlString,
|
||||
IsKnocking: '' as IntlString,
|
||||
KnockingLabel: '' as IntlString,
|
||||
InivitingLabel: '' as IntlString,
|
||||
InvitingYou: '' as IntlString,
|
||||
RoomType: '' as IntlString,
|
||||
Knock: '' as IntlString,
|
||||
Open: '' as IntlString,
|
||||
DND: '' as IntlString
|
||||
},
|
||||
ids: {
|
||||
MainFloor: '' as Ref<Floor>,
|
||||
Reception: '' as Ref<Room>,
|
||||
InviteNotification: '' as Ref<NotificationType>,
|
||||
KnockNotification: '' as Ref<NotificationType>
|
||||
},
|
||||
icon: {
|
||||
Love: '' as Asset,
|
||||
LeaveRoom: '' as Asset,
|
||||
EnterRoom: '' as Asset,
|
||||
Mic: '' as Asset,
|
||||
MicEnabled: '' as Asset,
|
||||
MicDisabled: '' as Asset,
|
||||
Cam: '' as Asset,
|
||||
CamEnabled: '' as Asset,
|
||||
CamDisabled: '' as Asset,
|
||||
SharingEnabled: '' as Asset,
|
||||
SharingDisabled: '' as Asset,
|
||||
Open: '' as Asset,
|
||||
Knock: '' as Asset,
|
||||
DND: '' as Asset,
|
||||
Record: '' as Asset,
|
||||
StopRecord: '' as Asset
|
||||
},
|
||||
metadata: {
|
||||
WebSocketURL: '' as Metadata<string>,
|
||||
ServiceEnpdoint: '' as Metadata<string>
|
||||
},
|
||||
space: {
|
||||
Rooms: '' as Ref<Space>,
|
||||
Drive: '' as Ref<Drive>
|
||||
}
|
||||
})
|
||||
|
||||
export const roomAccessIcon = {
|
||||
[RoomAccess.Open]: love.icon.Open,
|
||||
[RoomAccess.Knock]: love.icon.Knock,
|
||||
[RoomAccess.DND]: love.icon.DND
|
||||
}
|
||||
|
||||
export const roomAccessLabel = {
|
||||
[RoomAccess.Open]: love.string.Open,
|
||||
[RoomAccess.Knock]: love.string.Knock,
|
||||
[RoomAccess.DND]: love.string.DND
|
||||
}
|
||||
|
||||
export default love
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { Data, Ref } from '@hcengineering/core'
|
||||
import love, { Office, Room, RoomAccess, RoomType, GRID_WIDTH } from '.'
|
||||
|
||||
interface Slot {
|
||||
_id?: Ref<Room>
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export function isOffice (room: Data<Room>): room is Office {
|
||||
return (room as Office).person !== undefined
|
||||
}
|
||||
|
||||
export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Office>[] {
|
||||
const res: Data<Room | Office>[] = []
|
||||
// create 12 offices
|
||||
for (let index = 0; index < 12; index++) {
|
||||
const office: Data<Office> = {
|
||||
name: '',
|
||||
type: RoomType.Audio,
|
||||
access: RoomAccess.Knock,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 2,
|
||||
height: 1,
|
||||
x: (index % 2) * 3,
|
||||
y: index - (index % 2),
|
||||
person: employees[index] ?? null
|
||||
}
|
||||
res.push(office)
|
||||
}
|
||||
res.push({
|
||||
name: 'All hands',
|
||||
type: RoomType.Video,
|
||||
access: RoomAccess.Open,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 9,
|
||||
height: 3,
|
||||
x: 6,
|
||||
y: 0
|
||||
})
|
||||
res.push({
|
||||
name: 'Meeting Room 1',
|
||||
type: RoomType.Video,
|
||||
access: RoomAccess.Open,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 4,
|
||||
height: 3,
|
||||
x: 6,
|
||||
y: 4
|
||||
})
|
||||
res.push({
|
||||
name: 'Meeting Room 2',
|
||||
type: RoomType.Video,
|
||||
access: RoomAccess.Open,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 4,
|
||||
height: 3,
|
||||
x: 11,
|
||||
y: 4
|
||||
})
|
||||
res.push({
|
||||
name: 'Voice Room 1',
|
||||
type: RoomType.Audio,
|
||||
access: RoomAccess.Open,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 4,
|
||||
height: 3,
|
||||
x: 6,
|
||||
y: 8
|
||||
})
|
||||
res.push({
|
||||
name: 'Voice Room 2',
|
||||
type: RoomType.Audio,
|
||||
access: RoomAccess.Open,
|
||||
floor: love.ids.MainFloor,
|
||||
width: 4,
|
||||
height: 3,
|
||||
x: 11,
|
||||
y: 8
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const cropMaxWidth = (width: number): number => {
|
||||
return width > GRID_WIDTH ? GRID_WIDTH : width
|
||||
}
|
||||
|
||||
export function getFreeSpace (rooms: Slot[], exclude?: Slot, completeExclusion?: boolean): boolean[][] {
|
||||
const sorted = [...rooms].sort((a, b) => a.y - b.y)
|
||||
const map: boolean[][] = [new Array(GRID_WIDTH).fill(true)]
|
||||
|
||||
for (const room of sorted) {
|
||||
const excluded: boolean = exclude?._id === room._id
|
||||
for (
|
||||
let y = room.y === 0 ? 0 : excluded ? room.y : room.y - 1;
|
||||
y < room.y + room.height + (excluded ? 0 : 1);
|
||||
y++
|
||||
) {
|
||||
if (map[y] === undefined) {
|
||||
map[y] = new Array(GRID_WIDTH).fill(true)
|
||||
}
|
||||
for (
|
||||
let x = room.x === 0 ? 0 : excluded ? room.x : room.x - 1;
|
||||
x <
|
||||
(room.x + room.width - 1 < GRID_WIDTH
|
||||
? excluded
|
||||
? cropMaxWidth(room.x + room.width)
|
||||
: cropMaxWidth(room.x + room.width + 1)
|
||||
: GRID_WIDTH - 1);
|
||||
x++
|
||||
) {
|
||||
map[y][x] = completeExclusion === true && excluded
|
||||
}
|
||||
}
|
||||
}
|
||||
map.push(new Array(GRID_WIDTH).fill(true))
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
export function getFreePosition (
|
||||
rooms: Slot[],
|
||||
width: number,
|
||||
height: number
|
||||
): {
|
||||
x: number
|
||||
y: number
|
||||
} {
|
||||
const map: boolean[][] = getFreeSpace(rooms)
|
||||
|
||||
for (let y = 0; y <= map.length; y++) {
|
||||
if (map[y] === undefined) {
|
||||
map[y] = new Array(GRID_WIDTH).fill(true)
|
||||
}
|
||||
for (let x = 0; x < map[y].length; x++) {
|
||||
if (map[y][x]) {
|
||||
let matched = true
|
||||
for (let yIndex = 0; yIndex < height; yIndex++) {
|
||||
if (map[y + yIndex] === undefined) {
|
||||
map[y + yIndex] = new Array(GRID_WIDTH).fill(true)
|
||||
}
|
||||
for (let xIndex = 0; xIndex < width; xIndex++) {
|
||||
if (!map[y + yIndex][x + xIndex]) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return {
|
||||
x,
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIntersection (rooms: Slot[], width: number, height: number, x: number, y: number): boolean {
|
||||
for (const room of rooms) {
|
||||
if (x <= room.x + room.width && x + width >= room.x && y <= room.y + room.height && y + height >= room.y) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,10 @@
|
||||
"@hcengineering/request": "^0.6.6",
|
||||
"@hcengineering/support": "^0.6.1",
|
||||
"@hcengineering/document": "^0.6.0",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"@hcengineering/love-assets": "^0.6.0",
|
||||
"@hcengineering/server-love": "^0.6.0",
|
||||
"@hcengineering/server-love-resources": "^0.6.0",
|
||||
"@hcengineering/controlled-documents": "^0.1.0",
|
||||
"@hcengineering/products": "~0.1.0",
|
||||
"@hcengineering/training": "^0.1.0"
|
||||
|
||||
@@ -88,6 +88,7 @@ import { serverTimeId } from '@hcengineering/server-time'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
import { serverTrackerId } from '@hcengineering/server-tracker'
|
||||
import { serverViewId } from '@hcengineering/server-view'
|
||||
import { serverLoveId } from '@hcengineering/server-love'
|
||||
import { serverTrainingId } from '@hcengineering/server-training'
|
||||
import { serverDocumentsId } from '@hcengineering/server-controlled-documents'
|
||||
import {
|
||||
@@ -125,6 +126,7 @@ import { templatesId } from '@hcengineering/templates'
|
||||
import { trackerId } from '@hcengineering/tracker'
|
||||
import { viewId } from '@hcengineering/view'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { loveId } from '@hcengineering/love'
|
||||
import { documentsId } from '@hcengineering/controlled-documents'
|
||||
import { productsId } from '@hcengineering/products'
|
||||
import { trainingId } from '@hcengineering/training'
|
||||
@@ -159,6 +161,7 @@ import templatesEn from '@hcengineering/templates-assets/lang/en.json'
|
||||
import trackerEn from '@hcengineering/tracker-assets/lang/en.json'
|
||||
import viewEn from '@hcengineering/view-assets/lang/en.json'
|
||||
import workbenchEn from '@hcengineering/workbench-assets/lang/en.json'
|
||||
import loveEn from '@hcengineering/love-assets/lang/en.json'
|
||||
import documentsEn from '@hcengineering/controlled-documents-assets/lang/en.json'
|
||||
import productsEn from '@hcengineering/products-assets/lang/en.json'
|
||||
import trainingEn from '@hcengineering/training-assets/lang/en.json'
|
||||
@@ -192,6 +195,7 @@ addStringsLoader(hrId, async (lang: string) => hrEn)
|
||||
addStringsLoader(bitrixId, async (lang: string) => bitrixEn)
|
||||
addStringsLoader(requestId, async (lang: string) => requestEn)
|
||||
addStringsLoader(documentId, async (lang: string) => documentEn)
|
||||
addStringsLoader(loveId, async (lang: string) => loveEn)
|
||||
addStringsLoader(driveId, async (lang: string) => driveEn)
|
||||
addStringsLoader(documentsId, async (lang: string) => documentsEn)
|
||||
addStringsLoader(productsId, async (lang: string) => productsEn)
|
||||
@@ -237,6 +241,7 @@ export function start (
|
||||
addLocation(serverViewId, () => import('@hcengineering/server-view-resources'))
|
||||
addLocation(serverHrId, () => import('@hcengineering/server-hr-resources'))
|
||||
addLocation(serverActivityId, () => import('@hcengineering/server-activity-resources'))
|
||||
addLocation(serverLoveId, () => import('@hcengineering/server-love-resources'))
|
||||
addLocation(serverGuestId, () => import('@hcengineering/server-guest-resources'))
|
||||
addLocation(openAIId, () => Promise.resolve({ default: openAIPluginImpl }))
|
||||
addLocation(serverDocumentId, () => import('@hcengineering/server-document-resources'))
|
||||
|
||||
@@ -1657,6 +1657,41 @@
|
||||
"projectFolder": "models/server-document",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/love",
|
||||
"projectFolder": "plugins/love",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/love-assets",
|
||||
"projectFolder": "plugins/love-assets",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/love-resources",
|
||||
"projectFolder": "plugins/love-resources",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/model-love",
|
||||
"projectFolder": "models/love",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/server-love",
|
||||
"projectFolder": "server-plugins/love",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/server-love-resources",
|
||||
"projectFolder": "server-plugins/love-resources",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/model-server-love",
|
||||
"projectFolder": "models/server-love",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/drive",
|
||||
"projectFolder": "plugins/drive",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1 @@
|
||||
tsdoc-metadata.json
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@hcengineering/server-love-resources",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"prettier-plugin-svelte": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/workbench": "^0.6.9",
|
||||
"@hcengineering/server-notification-resources": "^0.6.0",
|
||||
"@hcengineering/love": "^0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import contact, { Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Account,
|
||||
Ref,
|
||||
Tx,
|
||||
TxCUD,
|
||||
TxCreateDoc,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxUpdateDoc,
|
||||
UserStatus
|
||||
} from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources'
|
||||
import love, {
|
||||
Invite,
|
||||
JoinRequest,
|
||||
ParticipantInfo,
|
||||
RequestStatus,
|
||||
RoomAccess,
|
||||
RoomInfo,
|
||||
isOffice,
|
||||
loveId
|
||||
} from '@hcengineering/love'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee>
|
||||
if (actualTx._class !== core.class.TxMixin) return []
|
||||
if (actualTx.mixin !== contact.mixin.Employee) return []
|
||||
const val = actualTx.attributes.active
|
||||
if (val === undefined) return []
|
||||
if (val) {
|
||||
const freeRoom = (await control.findAll(love.class.Office, { person: null }))[0]
|
||||
if (freeRoom !== undefined) {
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc(freeRoom._class, freeRoom.space, freeRoom._id, {
|
||||
person: actualTx.objectId
|
||||
})
|
||||
]
|
||||
}
|
||||
} else {
|
||||
const room = (await control.findAll(love.class.Office, { person: actualTx.objectId }))[0]
|
||||
if (room !== undefined) {
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc(room._class, room.space, room._id, {
|
||||
person: null
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Promise<Tx[]> {
|
||||
const account = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: acc as Ref<PersonAccount> })[0]
|
||||
if (account === undefined) return []
|
||||
const personId = account.person
|
||||
const person = (await control.findAll(contact.class.Person, { _id: personId }))[0]
|
||||
const room = (await control.findAll(love.class.Office, { person: personId }))[0]
|
||||
const tx = control.txFactory.createTxCreateDoc(love.class.ParticipantInfo, love.space.Rooms, {
|
||||
person: personId,
|
||||
name: person !== undefined ? getName(control.hierarchy, person) : account.email,
|
||||
room: room?._id ?? love.ids.Reception,
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const ptx = control.txFactory.createTxApplyIf(
|
||||
love.space.Rooms,
|
||||
personId,
|
||||
[],
|
||||
[
|
||||
{
|
||||
_class: love.class.ParticipantInfo,
|
||||
query: { person }
|
||||
}
|
||||
],
|
||||
[tx]
|
||||
)
|
||||
await control.apply([ptx], true)
|
||||
return []
|
||||
}
|
||||
|
||||
async function removeUserInfo (acc: Ref<Account>, control: TriggerControl): Promise<Tx[]> {
|
||||
const account = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: acc as Ref<PersonAccount> })[0]
|
||||
if (account === undefined) return []
|
||||
const person = account.person
|
||||
const infos = await control.findAll(love.class.ParticipantInfo, { person })
|
||||
for (const info of infos) {
|
||||
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)], true)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnUserStatus (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<UserStatus>
|
||||
if (actualTx.objectClass !== core.class.UserStatus) return []
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const createTx = actualTx as TxCreateDoc<UserStatus>
|
||||
const status = TxProcessor.createDoc2Doc(createTx)
|
||||
return await createUserInfo(status.user, control)
|
||||
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = actualTx as TxUpdateDoc<UserStatus>
|
||||
const val = updateTx.operations.online
|
||||
if (val === undefined) return []
|
||||
const status = (await control.findAll(core.class.UserStatus, { _id: updateTx.objectId }))[0]
|
||||
if (status !== undefined) {
|
||||
if (val) {
|
||||
return await createUserInfo(status.user, control)
|
||||
} else {
|
||||
return await removeUserInfo(status.user, control)
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function roomJoinHandler (info: ParticipantInfo, control: TriggerControl, roomInfos: RoomInfo[]): Promise<Tx[]> {
|
||||
const roomInfo = roomInfos.find((ri) => ri.room === info.room)
|
||||
if (roomInfo !== undefined) {
|
||||
roomInfo.persons.push(info.person)
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc(love.class.RoomInfo, love.space.Rooms, roomInfo._id, {
|
||||
persons: Array.from(new Set([...roomInfo.persons, info.person]))
|
||||
})
|
||||
]
|
||||
} else {
|
||||
const room = (await control.findAll(love.class.Room, { _id: info.room }))[0]
|
||||
if (room === undefined) return []
|
||||
return [
|
||||
control.txFactory.createTxCreateDoc(love.class.RoomInfo, love.space.Rooms, {
|
||||
persons: [info.person],
|
||||
room: info.room,
|
||||
isOffice: isOffice(room)
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaultRoomAccess (info: ParticipantInfo, roomInfos: RoomInfo[], control: TriggerControl): Tx[] {
|
||||
const res: Tx[] = []
|
||||
const oldRoomInfo = roomInfos.find((ri) => ri.persons.includes(info.person))
|
||||
if (oldRoomInfo !== undefined) {
|
||||
oldRoomInfo.persons = oldRoomInfo.persons.filter((p) => p !== info.person)
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(love.class.RoomInfo, love.space.Rooms, oldRoomInfo._id, {
|
||||
persons: oldRoomInfo.persons
|
||||
})
|
||||
)
|
||||
if (oldRoomInfo.persons.length === 0) {
|
||||
const resetAccessTx = control.txFactory.createTxUpdateDoc(
|
||||
oldRoomInfo.isOffice ? love.class.Office : love.class.Room,
|
||||
love.space.Rooms,
|
||||
oldRoomInfo.room,
|
||||
{
|
||||
access: oldRoomInfo.isOffice ? RoomAccess.Knock : RoomAccess.Open
|
||||
}
|
||||
)
|
||||
res.push(resetAccessTx)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export async function OnParticipantInfo (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const roomInfos = await control.queryFind(love.class.RoomInfo, {})
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ParticipantInfo>
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<ParticipantInfo>)
|
||||
return await roomJoinHandler(info, control, roomInfos)
|
||||
}
|
||||
if (actualTx._class === core.class.TxRemoveDoc) {
|
||||
const removedInfo = control.removedMap.get(actualTx.objectId) as ParticipantInfo
|
||||
if (removedInfo === undefined) return []
|
||||
return setDefaultRoomAccess(removedInfo, roomInfos, control)
|
||||
}
|
||||
if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const newRoom = (actualTx as TxUpdateDoc<ParticipantInfo>).operations.room
|
||||
if (newRoom === undefined) return []
|
||||
const info = (await control.findAll(love.class.ParticipantInfo, { _id: actualTx.objectId }, { limit: 1 }))[0]
|
||||
if (info === undefined) return []
|
||||
const res: Tx[] = []
|
||||
res.push(...setDefaultRoomAccess(info, roomInfos, control))
|
||||
res.push(...(await roomJoinHandler(info, control, roomInfos)))
|
||||
return res
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<JoinRequest>
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const request = TxProcessor.createDoc2Doc(actualTx)
|
||||
if (request.status === RequestStatus.Pending) {
|
||||
const roomInfo = (await control.findAll(love.class.RoomInfo, { room: request.room }))[0]
|
||||
if (roomInfo !== undefined) {
|
||||
const res: Tx[] = []
|
||||
const from = (await control.findAll(contact.class.Person, { _id: request.person }))[0]
|
||||
if (from === undefined) return []
|
||||
for (const user of roomInfo.persons) {
|
||||
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user })
|
||||
if (userAcc === undefined) continue
|
||||
if (
|
||||
await isAllowed(
|
||||
control,
|
||||
userAcc._id,
|
||||
love.ids.KnockNotification,
|
||||
notification.providers.BrowserNotification
|
||||
)
|
||||
) {
|
||||
const path = [workbenchId, control.workspace.workspaceUrl, loveId]
|
||||
const title = await translate(love.string.KnockingLabel, {})
|
||||
const body = await translate(love.string.IsKnocking, { name: formatName(from.name) })
|
||||
await createPushNotification(control, userAcc._id, title, body, request._id, from, path)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<Invite>
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const invite = TxProcessor.createDoc2Doc(actualTx)
|
||||
if (invite.status === RequestStatus.Pending) {
|
||||
const target = (await control.findAll(contact.class.Person, { _id: invite.target }))[0]
|
||||
if (target === undefined) return []
|
||||
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id })
|
||||
if (userAcc === undefined) return []
|
||||
const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0]
|
||||
if (
|
||||
await isAllowed(control, userAcc._id, love.ids.InviteNotification, notification.providers.BrowserNotification)
|
||||
) {
|
||||
const path = [workbenchId, control.workspace.workspaceUrl, loveId]
|
||||
const title = await translate(love.string.InivitingLabel, {})
|
||||
const body =
|
||||
from !== undefined
|
||||
? await translate(love.string.InvitingYou, { name: formatName(from.name) })
|
||||
: await translate(love.string.InivitingLabel, {})
|
||||
await createPushNotification(control, userAcc._id, title, body, invite._id, from, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnEmployee,
|
||||
OnUserStatus,
|
||||
OnParticipantInfo,
|
||||
OnKnock,
|
||||
OnInvite
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@hcengineering/server-love",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@types/node": "~20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"prettier-plugin-svelte": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/server-core": "^0.6.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { TriggerFunc } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const serverLoveId = 'server-love' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export default plugin(serverLoveId, {
|
||||
trigger: {
|
||||
OnEmployee: '' as Resource<TriggerFunc>,
|
||||
OnUserStatus: '' as Resource<TriggerFunc>,
|
||||
OnParticipantInfo: '' as Resource<TriggerFunc>,
|
||||
OnInvite: '' as Resource<TriggerFunc>,
|
||||
OnKnock: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user