diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index d1b6be8885..7f5c3d57a1 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -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 diff --git a/dev/prod/package.json b/dev/prod/package.json index 74f38eab43..562f416405 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -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", diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index fd47cac156..136cd66c3d 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -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) diff --git a/models/all/package.json b/models/all/package.json index 2c2c8bfc5d..119d5127d7 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -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", diff --git a/models/all/src/index.ts b/models/all/src/index.ts index 10e1999b7b..3098b2f93f 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -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], diff --git a/models/all/src/migration.ts b/models/all/src/migration.ts index 77af10c5d0..9983d5aae6 100644 --- a/models/all/src/migration.ts +++ b/models/all/src/migration.ts @@ -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 diff --git a/models/love/.eslintrc.js b/models/love/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/models/love/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/models/love/.npmignore b/models/love/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/models/love/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/models/love/config/rig.json b/models/love/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/models/love/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/models/love/package.json b/models/love/package.json new file mode 100644 index 0000000000..b4a7bc984a --- /dev/null +++ b/models/love/package.json @@ -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" + } +} diff --git a/models/love/src/index.ts b/models/love/src/index.ts new file mode 100644 index 0000000000..217fb15cea --- /dev/null +++ b/models/love/src/index.ts @@ -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 + + 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 | 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 + + @Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room')) + room!: Ref + + 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 + + @Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room')) + room!: Ref + + 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 + + @Prop(TypeRef(contact.class.Person), getEmbeddedLabel('Target')) + target!: Ref + + @Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room')) + room!: Ref + + 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[] + room!: Ref + 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 + ) +} diff --git a/models/love/src/migration.ts b/models/love/src/migration.ts new file mode 100644 index 0000000000..e854b2a67f --- /dev/null +++ b/models/love/src/migration.ts @@ -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 { + 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 { + 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 { + 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, + width: 100, + height: 0, + x: 0, + y: 0 + }, + love.ids.Reception + ) +} + +export const loveOperation: MigrateOperation = { + async migrate (client: MigrationClient): Promise {}, + async upgrade (client: MigrationUpgradeClient): Promise { + 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 + ) + } +} diff --git a/models/love/src/plugin.ts b/models/love/src/plugin.ts new file mode 100644 index 0000000000..205a602b89 --- /dev/null +++ b/models/love/src/plugin.ts @@ -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 + }, + category: { + Office: '' as Ref + }, + actionImpl: { + ToggleMic: '' as ViewAction, + ToggleVideo: '' as ViewAction + }, + ids: { + Settings: '' as Ref, + LoveNotificationGroup: '' as Ref + } +}) diff --git a/models/love/tsconfig.json b/models/love/tsconfig.json new file mode 100644 index 0000000000..9765b6086d --- /dev/null +++ b/models/love/tsconfig.json @@ -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" + } +} \ No newline at end of file diff --git a/models/server-love/.eslintrc.js b/models/server-love/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/models/server-love/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/models/server-love/.npmignore b/models/server-love/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/models/server-love/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/models/server-love/config/rig.json b/models/server-love/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/models/server-love/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/models/server-love/package.json b/models/server-love/package.json new file mode 100644 index 0000000000..340478a186 --- /dev/null +++ b/models/server-love/package.json @@ -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" + } +} diff --git a/models/server-love/src/index.ts b/models/server-love/src/index.ts new file mode 100644 index 0000000000..384d288d98 --- /dev/null +++ b/models/server-love/src/index.ts @@ -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 + } + }) +} diff --git a/models/server-love/tsconfig.json b/models/server-love/tsconfig.json new file mode 100644 index 0000000000..9765b6086d --- /dev/null +++ b/models/server-love/tsconfig.json @@ -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" + } +} \ No newline at end of file diff --git a/plugins/love-assets/.eslintrc.js b/plugins/love-assets/.eslintrc.js new file mode 100644 index 0000000000..e73094bc7e --- /dev/null +++ b/plugins/love-assets/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/love-assets/assets/icons.svg b/plugins/love-assets/assets/icons.svg new file mode 100644 index 0000000000..57f410da0e --- /dev/null +++ b/plugins/love-assets/assets/icons.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/love-assets/config/rig.json b/plugins/love-assets/config/rig.json new file mode 100644 index 0000000000..b75800b9b7 --- /dev/null +++ b/plugins/love-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/plugins/love-assets/jest.config.js b/plugins/love-assets/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/love-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/love-assets/lang/en.json b/plugins/love-assets/lang/en.json new file mode 100644 index 0000000000..a50f4bdb49 --- /dev/null +++ b/plugins/love-assets/lang/en.json @@ -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" + } +} diff --git a/plugins/love-assets/lang/es.json b/plugins/love-assets/lang/es.json new file mode 100644 index 0000000000..b4f3328288 --- /dev/null +++ b/plugins/love-assets/lang/es.json @@ -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" + } +} diff --git a/plugins/love-assets/lang/pt.json b/plugins/love-assets/lang/pt.json new file mode 100644 index 0000000000..402c36e916 --- /dev/null +++ b/plugins/love-assets/lang/pt.json @@ -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" + } +} diff --git a/plugins/love-assets/lang/ru.json b/plugins/love-assets/lang/ru.json new file mode 100644 index 0000000000..a3d452d54e --- /dev/null +++ b/plugins/love-assets/lang/ru.json @@ -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": "Сервис не настроен" + } +} diff --git a/plugins/love-assets/package.json b/plugins/love-assets/package.json new file mode 100644 index 0000000000..99c5c02d03 --- /dev/null +++ b/plugins/love-assets/package.json @@ -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" + } +} diff --git a/plugins/love-assets/src/__tests__/lang.test.ts b/plugins/love-assets/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..cf96c6dcf7 --- /dev/null +++ b/plugins/love-assets/src/__tests__/lang.test.ts @@ -0,0 +1,6 @@ +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equale', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) diff --git a/plugins/love-assets/src/index.ts b/plugins/love-assets/src/index.ts new file mode 100644 index 0000000000..37803b9f8d --- /dev/null +++ b/plugins/love-assets/src/index.ts @@ -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` +}) diff --git a/plugins/love-assets/tsconfig.json b/plugins/love-assets/tsconfig.json new file mode 100644 index 0000000000..4449c73536 --- /dev/null +++ b/plugins/love-assets/tsconfig.json @@ -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" + } +} \ No newline at end of file diff --git a/plugins/love-resources/.eslintrc.js b/plugins/love-resources/.eslintrc.js new file mode 100644 index 0000000000..bb8fd7450d --- /dev/null +++ b/plugins/love-resources/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'], + parserOptions: { tsconfigRootDir: __dirname } +} diff --git a/plugins/love-resources/.prettierrc b/plugins/love-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/plugins/love-resources/.prettierrc @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/plugins/love-resources/config/rig.json b/plugins/love-resources/config/rig.json new file mode 100644 index 0000000000..bcad6f7c33 --- /dev/null +++ b/plugins/love-resources/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "ui" +} diff --git a/plugins/love-resources/jest.config.js b/plugins/love-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/love-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/love-resources/package.json b/plugins/love-resources/package.json new file mode 100644 index 0000000000..407c4eb78b --- /dev/null +++ b/plugins/love-resources/package.json @@ -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" + } +} diff --git a/plugins/love-resources/postcss.config.js b/plugins/love-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/plugins/love-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/plugins/love-resources/src/broadcast.ts b/plugins/love-resources/src/broadcast.ts new file mode 100644 index 0000000000..5fb8de91db --- /dev/null +++ b/plugins/love-resources/src/broadcast.ts @@ -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) => { + 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' }) diff --git a/plugins/love-resources/src/components/ActiveInvitesPopup.svelte b/plugins/love-resources/src/components/ActiveInvitesPopup.svelte new file mode 100644 index 0000000000..bfec5913d1 --- /dev/null +++ b/plugins/love-resources/src/components/ActiveInvitesPopup.svelte @@ -0,0 +1,61 @@ + + + +
+ + +
+ +
+
+
+
+ + diff --git a/plugins/love-resources/src/components/AddRoomPopup.svelte b/plugins/love-resources/src/components/AddRoomPopup.svelte new file mode 100644 index 0000000000..509bbd97af --- /dev/null +++ b/plugins/love-resources/src/components/AddRoomPopup.svelte @@ -0,0 +1,84 @@ + + +
+ {#each items as item} +
+
+ {/each} +
+ + diff --git a/plugins/love-resources/src/components/CamSettingPopup.svelte b/plugins/love-resources/src/components/CamSettingPopup.svelte new file mode 100644 index 0000000000..3c7bc61581 --- /dev/null +++ b/plugins/love-resources/src/components/CamSettingPopup.svelte @@ -0,0 +1,78 @@ + + +
+
+ + diff --git a/plugins/love-resources/src/components/ControlBar.svelte b/plugins/love-resources/src/components/ControlBar.svelte new file mode 100644 index 0000000000..bdbd28414c --- /dev/null +++ b/plugins/love-resources/src/components/ControlBar.svelte @@ -0,0 +1,198 @@ + + + +
+ {#if room._id !== love.ids.Reception} + + {/if} + {#if $isConnected} + + {#if allowCam} + + {/if} + {#if allowShare} + + {/if} + {#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isRecordingAvailable} + record(room)} + /> + {/if} + {/if} +
+ {#if hasAccountRole(getCurrentAccount(), AccountRole.User)} + + {/if} + {#if allowLeave} + + {/if} +
+
+ + diff --git a/plugins/love-resources/src/components/ControlExt.svelte b/plugins/love-resources/src/components/ControlExt.svelte new file mode 100644 index 0000000000..7e41ac7ed3 --- /dev/null +++ b/plugins/love-resources/src/components/ControlExt.svelte @@ -0,0 +1,488 @@ + + + +
+ + +
+ {#if selectedFloor} +
+ {#if activeRooms.length > 0} +
+ {#each activeRooms as active} + + +
r._id === active._id)} + on:click={(ev) => { + openRoom(ev, active) + }} + > +
{getRoomName(active, $personByIdStore)}
+
+ {#each active.participants as participant} +
+ +
+ {/each} +
+
+ {/each} + {/if} + {#if reception && receptionParticipants.length > 0} +
+
+
{getRoomName(reception, $personByIdStore)}
+
+ {#each receptionParticipants as participant (participant._id)} +
{ + if (myInfo) { + invite(participant.person, $myInfo?.room) + } + }} + > + +
+ {/each} +
+
+ {/if} +
+ + diff --git a/plugins/love-resources/src/components/EditFloorPopup.svelte b/plugins/love-resources/src/components/EditFloorPopup.svelte new file mode 100644 index 0000000000..91709714d1 --- /dev/null +++ b/plugins/love-resources/src/components/EditFloorPopup.svelte @@ -0,0 +1,42 @@ + + + { + if (id === undefined) createFloor() + else updateFloor() + }} + okLabel={id === undefined ? love.string.AddAFloor : love.string.RenameAFloor} + canSave={name.trim().length > 0} + on:close={() => dispatch('close')} +> + + diff --git a/plugins/love-resources/src/components/Floor.svelte b/plugins/love-resources/src/components/Floor.svelte new file mode 100644 index 0000000000..39cee6a490 --- /dev/null +++ b/plugins/love-resources/src/components/Floor.svelte @@ -0,0 +1,73 @@ + + + +
+
dispatch('change', event.detail)}> + + + {#if editable} + dispatch('configure')} + /> + {/if} + +
+
+ + + {#each rooms as room} + + {/each} + + +
+ {#if $currentRoom} + + {/if} +
diff --git a/plugins/love-resources/src/components/FloorConfigure.svelte b/plugins/love-resources/src/components/FloorConfigure.svelte new file mode 100644 index 0000000000..3a604e9b4a --- /dev/null +++ b/plugins/love-resources/src/components/FloorConfigure.svelte @@ -0,0 +1,371 @@ + + + +
+
dispatch('change', event.detail)}> + + + +
+ dispatch('configure')} + /> + +
+
+ + { + 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} + { + 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} + + {#if lockedID !== -1 && dragged !== undefined && locked !== undefined} + + {/if} + +
+
diff --git a/plugins/love-resources/src/components/FloorGrid.svelte b/plugins/love-resources/src/components/FloorGrid.svelte new file mode 100644 index 0000000000..cbcd5e3232 --- /dev/null +++ b/plugins/love-resources/src/components/FloorGrid.svelte @@ -0,0 +1,70 @@ + + + +{#if preview} + + +
+ +
+{:else} +
{ + 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} + > + +
+{/if} + + diff --git a/plugins/love-resources/src/components/FloorPopup.svelte b/plugins/love-resources/src/components/FloorPopup.svelte new file mode 100644 index 0000000000..1f5aefceb3 --- /dev/null +++ b/plugins/love-resources/src/components/FloorPopup.svelte @@ -0,0 +1,98 @@ + + + +
+ {#if floorsSelector} + {#each $floors as floor, i} +
+
+
+
+
+ + diff --git a/plugins/love-resources/src/components/Main.svelte b/plugins/love-resources/src/components/Main.svelte new file mode 100644 index 0000000000..9e97c901f9 --- /dev/null +++ b/plugins/love-resources/src/components/Main.svelte @@ -0,0 +1,37 @@ + + + +
+ {#if ($currentRoom !== undefined && $screenSharing) || $currentRoom?.type === RoomType.Video} + + {:else} + + {/if} +
diff --git a/plugins/love-resources/src/components/MicSettingPopup.svelte b/plugins/love-resources/src/components/MicSettingPopup.svelte new file mode 100644 index 0000000000..f7774677e2 --- /dev/null +++ b/plugins/love-resources/src/components/MicSettingPopup.svelte @@ -0,0 +1,112 @@ + + +
+
+ + diff --git a/plugins/love-resources/src/components/ParticipantView.svelte b/plugins/love-resources/src/components/ParticipantView.svelte new file mode 100644 index 0000000000..154d69c78a --- /dev/null +++ b/plugins/love-resources/src/components/ParticipantView.svelte @@ -0,0 +1,159 @@ + + + +
+
+ {formatName(name)} + {#if connecting} +
+ +
+ {/if} +
+ +
+
+
+
+ +
+
+
+ + diff --git a/plugins/love-resources/src/components/RequestPopup.svelte b/plugins/love-resources/src/components/RequestPopup.svelte new file mode 100644 index 0000000000..52710f720f --- /dev/null +++ b/plugins/love-resources/src/components/RequestPopup.svelte @@ -0,0 +1,81 @@ + + + +
+ {#if person} +
+ + + +
+ {/if} +
+
+
+
+
+
+
+ + diff --git a/plugins/love-resources/src/components/RequestingPopup.svelte b/plugins/love-resources/src/components/RequestingPopup.svelte new file mode 100644 index 0000000000..b7b3aabc6a --- /dev/null +++ b/plugins/love-resources/src/components/RequestingPopup.svelte @@ -0,0 +1,60 @@ + + + +
+
+
+
+
+
+ + diff --git a/plugins/love-resources/src/components/Room.svelte b/plugins/love-resources/src/components/Room.svelte new file mode 100644 index 0000000000..a19d9619f9 --- /dev/null +++ b/plugins/love-resources/src/components/Room.svelte @@ -0,0 +1,418 @@ + + + +
+ {#if $isConnected && !$isCurrentInstanceConnected} +
+
+ {:else if !configured} +
+
+ {:else if loading} + + {/if} +
3} class:hidden={loading}> +
+ +
+ {#if withVideo} +
+ {#each participants as participant, i (participant._id)} + 1 && (roomWidth - 16 * (columns - 1)) / columns < 300) || + (rows > 1 && (roomHeight - 16 * (rows - 1)) / rows < 168)))} + /> + {/each} +
+ {/if} +
+ {#if $currentRoom} + + {/if} +
+ + diff --git a/plugins/love-resources/src/components/RoomAccessPopup.svelte b/plugins/love-resources/src/components/RoomAccessPopup.svelte new file mode 100644 index 0000000000..723445d238 --- /dev/null +++ b/plugins/love-resources/src/components/RoomAccessPopup.svelte @@ -0,0 +1,61 @@ + + +
+ +
diff --git a/plugins/love-resources/src/components/RoomConfigure.svelte b/plugins/love-resources/src/components/RoomConfigure.svelte new file mode 100644 index 0000000000..a229504b01 --- /dev/null +++ b/plugins/love-resources/src/components/RoomConfigure.svelte @@ -0,0 +1,243 @@ + + + + +
{ + 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} +
{ + if (isOffice(room) && rX === 0 && rY === 0) e.stopPropagation() + }} + > + {#if isOffice(room) && rX === 0 && rY === 0} + + {/if} +
+ {/each} + {/each} +
+ + {#if showButtons} +
+ {#if room.access === RoomAccess.DND} + + {/if} + {#if room.type === RoomType.Video} + + {/if} + {#if removable} + + {/if} +
+ {/if} +
+
diff --git a/plugins/love-resources/src/components/RoomPopup.svelte b/plugins/love-resources/src/components/RoomPopup.svelte new file mode 100644 index 0000000000..e88e939c3a --- /dev/null +++ b/plugins/love-resources/src/components/RoomPopup.svelte @@ -0,0 +1,283 @@ + + + +
+
+
+ {getRoomName(room, $personByIdStore)} +
+
+ +
+ {#each info as inf} + {@const person = getPerson(inf, $personByIdStore)} + {#if person} +
+ {/if} + {/each} +
+
+
+ {#if joined && $isConnected} +
+ + {#if allowCam} + + {/if} + {#if allowShare} + + {/if} + +
+ {/if} + {#if (joined && !myRoom) || (!allowLeave && $myRequests.length === 0 && !myRoom)} +
+ {#if allowLeave} + + {:else if $myRequests.length === 0 && !myRoom} + + {/if} + {#if $location.path[2] !== loveId} + + {/if} +
+ {/if} +
+ + diff --git a/plugins/love-resources/src/components/RoomPreview.svelte b/plugins/love-resources/src/components/RoomPreview.svelte new file mode 100644 index 0000000000..95b5430a51 --- /dev/null +++ b/plugins/love-resources/src/components/RoomPreview.svelte @@ -0,0 +1,134 @@ + + + + + +
+ {#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)} + +
{ + 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} + + {:else if hoveredRoomX === x && hoveredRoomY === y} + + {/if} +
+ {/each} + {/each} + + {#if !preview} +
+ + + {#if room.access === RoomAccess.DND || room.type === RoomType.Video} +
+ {#if room.access === RoomAccess.DND} + + {/if} + {#if room.type === RoomType.Video} + + {/if} +
+ {/if} +
+ {/if} +
diff --git a/plugins/love-resources/src/components/Settings.svelte b/plugins/love-resources/src/components/Settings.svelte new file mode 100644 index 0000000000..c16883e0cc --- /dev/null +++ b/plugins/love-resources/src/components/Settings.svelte @@ -0,0 +1,107 @@ + + +
+
dispatch('change', event.detail)}> + +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/love-resources/src/components/VideoPopup.svelte b/plugins/love-resources/src/components/VideoPopup.svelte new file mode 100644 index 0000000000..35e420fef5 --- /dev/null +++ b/plugins/love-resources/src/components/VideoPopup.svelte @@ -0,0 +1,404 @@ + + + +
+
+
+ + + {#if $isConnected} + + {/if} +
+
+ { + dispatch('dock') + }} + /> + {#if allowLeave} + + {/if} +
+
+
+ +
+ + {#each participants as participant, i (participant._id)} +
+ +
+ {/each} +
+
+
+ + diff --git a/plugins/love-resources/src/components/WorkbenchExtension.svelte b/plugins/love-resources/src/components/WorkbenchExtension.svelte new file mode 100644 index 0000000000..542ec894ad --- /dev/null +++ b/plugins/love-resources/src/components/WorkbenchExtension.svelte @@ -0,0 +1,55 @@ + + +
diff --git a/plugins/love-resources/src/components/icons/Layers.svelte b/plugins/love-resources/src/components/icons/Layers.svelte new file mode 100644 index 0000000000..443258ba13 --- /dev/null +++ b/plugins/love-resources/src/components/icons/Layers.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/plugins/love-resources/src/index.ts b/plugins/love-resources/src/index.ts new file mode 100644 index 0000000000..c21edbb675 --- /dev/null +++ b/plugins/love-resources/src/index.ts @@ -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 => ({ + component: { + Main, + ControlExt, + Settings, + WorkbenchExtension + }, + actionImpl: { + ToggleMic: toggleMic, + ToggleVideo: toggleVideo + } +}) diff --git a/plugins/love-resources/src/plugin.ts b/plugins/love-resources/src/plugin.ts new file mode 100644 index 0000000000..e1cfcf09d3 --- /dev/null +++ b/plugins/love-resources/src/plugin.ts @@ -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 + } +}) diff --git a/plugins/love-resources/src/stores.ts b/plugins/love-resources/src/stores.ts new file mode 100644 index 0000000000..ac5ad80273 --- /dev/null +++ b/plugins/love-resources/src/stores.ts @@ -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([]) +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([]) +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([]) +export const activeFloor = derived([rooms, myInfo, myOffice], ([rooms, myInfo, myOffice]) => { + let res: Ref | 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([]) +export const invites = writable([]) +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() +export let $myPreferences: DevicesPreference | undefined + +function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] { + const map = new Map, 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('') diff --git a/plugins/love-resources/src/types.ts b/plugins/love-resources/src/types.ts new file mode 100644 index 0000000000..272c905a20 --- /dev/null +++ b/plugins/love-resources/src/types.ts @@ -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] diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts new file mode 100644 index 0000000000..9ae4048497 --- /dev/null +++ b/plugins/love-resources/src/utils.ts @@ -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, + userId: string, + participantName: string +): Promise { + 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): 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 { + const wsURL = getMetadata(love.metadata.WebSocketURL) + if (wsURL !== undefined) { + await lk.prepareConnection(wsURL) + } +} + +void prepare() + +export const isConnected = writable(false) +export const isCurrentInstanceConnected = writable(false) +export let $isCurrentInstanceConnected: boolean = false +isCurrentInstanceConnected.subscribe((value) => { + $isCurrentInstanceConnected = value +}) +export const screenSharing = writable(false) +export const isRecording = writable(false) +export const isRecordingAvailable = writable(undefined) +export const isMicEnabled = writable(false) +export const isCameraEnabled = writable(false) +export const isSharingEnabled = writable(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 | undefined +let localVideo: LocalVideoTrack | undefined + +try { + blurProcessor = BackgroundBlur() +} catch (err) { + console.log("Can't set blur processor", err) +} + +async function setKrispProcessor (pub: LocalTrackPublication): Promise { + 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 { + 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 { + const client = getClient() + if ($myPreferences !== undefined) { + await client.update($myPreferences, { blurRadius: value }) + } else { + const space = getCurrentAccount()._id as string as Ref + 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 { + 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 { + await new Promise((resolve) => { + if (lk.state === ConnectionState.Connected) { + resolve() + } + lk.once(RoomEvent.Connected, () => { + resolve() + }) + }) +} + +export async function disconnect (): Promise { + 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 { + 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 { + 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 { + 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 { + 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): 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): 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 { + 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 { + 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 { + await disconnect() + await moveToRoom(x, y, currentInfo, currentPerson, room) + await connectLK(currentPerson, room) +} + +export const joinRequest: Ref | 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, + currentInfo: ParticipantInfo | undefined, + room: Room, + info: ParticipantInfo[], + currentRequests: JoinRequest[], + currentInvites: Invite[], + place?: { x: number, y: number } +): Promise { + 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, room: Ref | undefined): Promise { + 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 { + await setMic(!get(isMicEnabled)) +} + +export async function toggleVideo (): Promise { + await setCam(!get(isCameraEnabled)) +} + +export async function record (room: Room): Promise { + 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 { + 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() diff --git a/plugins/love-resources/svelte.config.js b/plugins/love-resources/svelte.config.js new file mode 100644 index 0000000000..944a06f73e --- /dev/null +++ b/plugins/love-resources/svelte.config.js @@ -0,0 +1,5 @@ +const sveltePreprocess = require('svelte-preprocess') + +module.exports = { + preprocess: sveltePreprocess() +}; \ No newline at end of file diff --git a/plugins/love-resources/tsconfig.json b/plugins/love-resources/tsconfig.json new file mode 100644 index 0000000000..fb6ccb2d18 --- /dev/null +++ b/plugins/love-resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types" + } +} \ No newline at end of file diff --git a/plugins/love/.eslintrc.js b/plugins/love/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/plugins/love/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/love/.npmignore b/plugins/love/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/plugins/love/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/plugins/love/config/rig.json b/plugins/love/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/plugins/love/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/plugins/love/jest.config.js b/plugins/love/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/love/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/love/package.json b/plugins/love/package.json new file mode 100644 index 0000000000..22790a24f8 --- /dev/null +++ b/plugins/love/package.json @@ -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" + } +} diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts new file mode 100644 index 0000000000..6fd3ca35b0 --- /dev/null +++ b/plugins/love/src/index.ts @@ -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 + width: number + height: number + x: number + y: number +} + +export interface Office extends Room { + person: Ref | 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 + name: string + room: Ref + x: number + y: number +} + +export interface RoomInfo extends Doc { + persons: Ref[] + room: Ref + isOffice: boolean +} + +export enum RequestStatus { + Pending, + Approved, + Rejected +} + +export interface JoinRequest extends Doc { + person: Ref + room: Ref + status: RequestStatus +} + +export interface Invite extends Doc { + from: Ref + target: Ref + room: Ref + 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>, + Floor: '' as Ref>, + Office: '' as Ref>, + ParticipantInfo: '' as Ref>, + JoinRequest: '' as Ref>, + DevicesPreference: '' as Ref>, + RoomInfo: '' as Ref>, + Invite: '' as Ref> + }, + action: { + ToggleMic: '' as Ref, + ToggleVideo: '' as Ref + }, + 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, + Reception: '' as Ref, + InviteNotification: '' as Ref, + KnockNotification: '' as Ref + }, + 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, + ServiceEnpdoint: '' as Metadata + }, + space: { + Rooms: '' as Ref, + Drive: '' as Ref + } +}) + +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 diff --git a/plugins/love/src/utils.ts b/plugins/love/src/utils.ts new file mode 100644 index 0000000000..8c13f3606e --- /dev/null +++ b/plugins/love/src/utils.ts @@ -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 + width: number + height: number + x: number + y: number +} + +export function isOffice (room: Data): room is Office { + return (room as Office).person !== undefined +} + +export function createDefaultRooms (employees: Ref[]): Data[] { + const res: Data[] = [] + // create 12 offices + for (let index = 0; index < 12; index++) { + const office: Data = { + 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 +} diff --git a/plugins/love/tsconfig.json b/plugins/love/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/plugins/love/tsconfig.json @@ -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" + } +} \ No newline at end of file diff --git a/pods/server/package.json b/pods/server/package.json index 10e0c3db80..20295341bf 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -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" diff --git a/pods/server/src/server.ts b/pods/server/src/server.ts index 8b4d9605e1..0bc2cf245b 100644 --- a/pods/server/src/server.ts +++ b/pods/server/src/server.ts @@ -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')) diff --git a/rush.json b/rush.json index 18a5d6f25a..d53bfac7f2 100644 --- a/rush.json +++ b/rush.json @@ -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", diff --git a/server-plugins/love-resources/.eslintrc.js b/server-plugins/love-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/love-resources/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server-plugins/love-resources/.npmignore b/server-plugins/love-resources/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/love-resources/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/love-resources/.prettierignore b/server-plugins/love-resources/.prettierignore new file mode 100644 index 0000000000..21c8f7d149 --- /dev/null +++ b/server-plugins/love-resources/.prettierignore @@ -0,0 +1 @@ +tsdoc-metadata.json \ No newline at end of file diff --git a/server-plugins/love-resources/config/rig.json b/server-plugins/love-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/love-resources/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/server-plugins/love-resources/jest.config.js b/server-plugins/love-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/love-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/server-plugins/love-resources/package.json b/server-plugins/love-resources/package.json new file mode 100644 index 0000000000..8b57d153f1 --- /dev/null +++ b/server-plugins/love-resources/package.json @@ -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" + } +} diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts new file mode 100644 index 0000000000..753625d909 --- /dev/null +++ b/server-plugins/love-resources/src/index.ts @@ -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 { + const actualTx = TxProcessor.extractTx(tx) as TxMixin + 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, control: TriggerControl): Promise { + const account = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: acc as Ref })[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, control: TriggerControl): Promise { + const account = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: acc as Ref })[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 { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (actualTx.objectClass !== core.class.UserStatus) return [] + if (actualTx._class === core.class.TxCreateDoc) { + const createTx = actualTx as TxCreateDoc + const status = TxProcessor.createDoc2Doc(createTx) + return await createUserInfo(status.user, control) + } else if (actualTx._class === core.class.TxUpdateDoc) { + const updateTx = actualTx as TxUpdateDoc + 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 { + 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 { + const roomInfos = await control.queryFind(love.class.RoomInfo, {}) + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (actualTx._class === core.class.TxCreateDoc) { + const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + 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).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 { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + 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 { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + 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 + } +}) diff --git a/server-plugins/love-resources/tsconfig.json b/server-plugins/love-resources/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/server-plugins/love-resources/tsconfig.json @@ -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" + } +} \ No newline at end of file diff --git a/server-plugins/love/.eslintrc.js b/server-plugins/love/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/love/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server-plugins/love/.npmignore b/server-plugins/love/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/love/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/love/config/rig.json b/server-plugins/love/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/love/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/server-plugins/love/jest.config.js b/server-plugins/love/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/love/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/server-plugins/love/package.json b/server-plugins/love/package.json new file mode 100644 index 0000000000..04f7f98bb4 --- /dev/null +++ b/server-plugins/love/package.json @@ -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" + } +} diff --git a/server-plugins/love/src/index.ts b/server-plugins/love/src/index.ts new file mode 100644 index 0000000000..e67d1c1250 --- /dev/null +++ b/server-plugins/love/src/index.ts @@ -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, + OnUserStatus: '' as Resource, + OnParticipantInfo: '' as Resource, + OnInvite: '' as Resource, + OnKnock: '' as Resource + } +}) diff --git a/server-plugins/love/tsconfig.json b/server-plugins/love/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/server-plugins/love/tsconfig.json @@ -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" + } +} \ No newline at end of file