Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2025-10-22 11:30:30 +07:00
157 changed files with 4335 additions and 1228 deletions
+198 -8
View File
@@ -514,6 +514,15 @@ importers:
'@rush-temp/importer':
specifier: file:./projects/importer.tgz
version: file:projects/importer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/inbox':
specifier: file:./projects/inbox.tgz
version: file:projects/inbox.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/inbox-assets':
specifier: file:./projects/inbox-assets.tgz
version: file:projects/inbox-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/inbox-resources':
specifier: file:./projects/inbox-resources.tgz
version: file:projects/inbox-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/integration-client':
specifier: file:./projects/integration-client.tgz
version: file:projects/integration-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
@@ -664,6 +673,9 @@ importers:
'@rush-temp/model-huly-mail':
specifier: file:./projects/model-huly-mail.tgz
version: file:projects/model-huly-mail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/model-inbox':
specifier: file:./projects/model-inbox.tgz
version: file:projects/model-inbox.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@rush-temp/model-inventory':
specifier: file:./projects/model-inventory.tgz
version: file:projects/model-inventory.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
@@ -4928,7 +4940,7 @@ packages:
version: 0.0.0
'@rush-temp/desktop@file:projects/desktop.tgz':
resolution: {integrity: sha512-HiBnbdBc/mg0wlsEUcRw05sKWnlyUnExnNV3vkyEFlnSIN3ROFYSIhEI26+UpXhfa+8t6TTSQx9JK8gmiII0aQ==, tarball: file:projects/desktop.tgz}
resolution: {integrity: sha512-OqIIA7HRUTB18L4jdl39LCzh258UAs2Rp3qbua38PAsOV7qmoQC0+xFmtH0aAmaFL8cut6K88/FnbjjtzAYP8A==, tarball: file:projects/desktop.tgz}
version: 0.0.0
'@rush-temp/devmodel-resources@file:projects/devmodel-resources.tgz':
@@ -5103,6 +5115,18 @@ packages:
resolution: {integrity: sha512-PXqcGrMal8EqtqMeGqvXahFZSbfitShE6UKT5WlIE9hCrNlMv2H13YqSSDoDmYPjmNbUlZ4RlL8z1BtZgC6XxA==, tarball: file:projects/importer.tgz}
version: 0.0.0
'@rush-temp/inbox-assets@file:projects/inbox-assets.tgz':
resolution: {integrity: sha512-Fe6/dReQqNS0AhZR3gFEajiP8Qm9CinWX9fSXeYnXbfTMh0ijzxIq9ra8kwsgIjI8bQ2GE09likyzVAa91IW1Q==, tarball: file:projects/inbox-assets.tgz}
version: 0.0.0
'@rush-temp/inbox-resources@file:projects/inbox-resources.tgz':
resolution: {integrity: sha512-xszlMc55DpQSp+mfmiY3EM3udDFfhLfWvzre9Px6XMN8Gnt0DcVFm0S3KyR8NKJzV3uVOPHaweXoXmZa2GFvxw==, tarball: file:projects/inbox-resources.tgz}
version: 0.0.0
'@rush-temp/inbox@file:projects/inbox.tgz':
resolution: {integrity: sha512-IxHFbCtBvZ8rEL/XqrSuSB+VgL83uYcjbO9OiPetTNWtz7rDmvLyj+UnkXJWtlyTKWn9dKfcsuB3bkHQZkm/fA==, tarball: file:projects/inbox.tgz}
version: 0.0.0
'@rush-temp/integration-client@file:projects/integration-client.tgz':
resolution: {integrity: sha512-p3uOUY9iHR+qtaBa8CNxEQc31gsM8dGdKU++quV8XDUAD+hOl+Ikjj8Fh+V1OLA0KXb2wN2Ob8MQUwmHsKPHIQ==, tarball: file:projects/integration-client.tgz}
version: 0.0.0
@@ -5204,7 +5228,7 @@ packages:
version: 0.0.0
'@rush-temp/model-all@file:projects/model-all.tgz':
resolution: {integrity: sha512-h3FPiVybmZEa2IAX89OlWzrRUJYLg5fVLdq2lO3ctmkwK20sbvBdhoLJZfg1YYaeO1NTsUP4j8kyu5DxTxu8og==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-DNEVWMrvvqLdwjxdOH270bA0W+mIxoLFBqx49J647HY4E9mmWqqgeMNSLLUXY1WlhpFSok3GyZqVV8AncdBCCQ==, tarball: file:projects/model-all.tgz}
version: 0.0.0
'@rush-temp/model-analytics-collector@file:projects/model-analytics-collector.tgz':
@@ -5303,6 +5327,10 @@ packages:
resolution: {integrity: sha512-icnJmXV6KKX2gci/n1l8fOZIFPiRpq68/VIlhu2TdknycHtRKDADLSsZGpXaKoUq35tjwMmuZ9ZLF93jzWGSNQ==, tarball: file:projects/model-huly-mail.tgz}
version: 0.0.0
'@rush-temp/model-inbox@file:projects/model-inbox.tgz':
resolution: {integrity: sha512-lVBzx78GwVYVjBePMXW/hZ2c88MwaS6g2WGXV+3Sp5O+OoRzKaZ0Uoj6mKOabr4l5pJ/A/KnBFJ50Lh4jVNTZQ==, tarball: file:projects/model-inbox.tgz}
version: 0.0.0
'@rush-temp/model-inventory@file:projects/model-inventory.tgz':
resolution: {integrity: sha512-pYB7K/D3aGlYDsxFoqnScmPQr1TiJ3NbLRXNEIzqqI0cp5Fpaz9cqvOuKhscFxDJQ6fujf/NLOdKLHh3FxM3Cg==, tarball: file:projects/model-inventory.tgz}
version: 0.0.0
@@ -5324,7 +5352,7 @@ packages:
version: 0.0.0
'@rush-temp/model-notification@file:projects/model-notification.tgz':
resolution: {integrity: sha512-FKgjGbbY8VKz0bA+//O/OKfEXUhhTMfYMy7fcydejDs0uJ/m1XSaDF0IBkBR9XvDNaXO4v9/IYOmQJTIvAs6Ow==, tarball: file:projects/model-notification.tgz}
resolution: {integrity: sha512-uRMC3n/pyGJlDk/GCbSkZrUMklyVhUmcn+tvlqZxpXznr4WSlSDMkY3VfB7064Ydn/K4KX5Wj5BjdxFWowgTdg==, tarball: file:projects/model-notification.tgz}
version: 0.0.0
'@rush-temp/model-preference@file:projects/model-preference.tgz':
@@ -5560,7 +5588,7 @@ packages:
version: 0.0.0
'@rush-temp/notification-resources@file:projects/notification-resources.tgz':
resolution: {integrity: sha512-k27Ea3pPJEKFqeuWkV4rahl88Uu7xCWeRz/ErVgPctWxIFw0xVGjbrsNyg+ufI+vZp40yaWVMw+tHn44s6VM1A==, tarball: file:projects/notification-resources.tgz}
resolution: {integrity: sha512-h0E2s9AO3VTo0SXkqqGMzpZFAasOIuaMJeX5L2FTUylV8fJtS5ONbDmmNssPLwQ+c2JWa5Ok6uXYB5KJx0I56g==, tarball: file:projects/notification-resources.tgz}
version: 0.0.0
'@rush-temp/notification@file:projects/notification.tgz':
@@ -5680,7 +5708,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-server@file:projects/pod-server.tgz':
resolution: {integrity: sha512-aR9P6JsNdSrHmfvsc3oqgYvdeEhH03KBR/xb0YmaL+Zjcey4erHd4lT0pz5bl8OKQnJKeL9hmuYp+G4PA/J3GQ==, tarball: file:projects/pod-server.tgz}
resolution: {integrity: sha512-kQYQpvwBHRxO7a4DXdzC8reqtzPv718JOuilFmULpoEa2KWNBUQKLHN71K3vxtMQwK/+vu7pl8H9jtjskSLySA==, tarball: file:projects/pod-server.tgz}
version: 0.0.0
'@rush-temp/pod-sign@file:projects/pod-sign.tgz':
@@ -5756,7 +5784,7 @@ packages:
version: 0.0.0
'@rush-temp/prod@file:projects/prod.tgz':
resolution: {integrity: sha512-jk0NWxDQLOtjcbnQVNRPS7/IriZT/UntwqO+yH8RswKec9XKe99w7kjku2ooCIp85qZyeUiT0HxIWrApnsKBUw==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-cBCJ4o7HyPky1hndFE/lQ/AxXM47a0iC4s8H6ihpgZ17SD3iLJwx5V7Wk9+zmfJK03XuqxE96oUt4EjezIStuw==, tarball: file:projects/prod.tgz}
version: 0.0.0
'@rush-temp/products-assets@file:projects/products-assets.tgz':
@@ -5844,7 +5872,7 @@ packages:
version: 0.0.0
'@rush-temp/server-activity-resources@file:projects/server-activity-resources.tgz':
resolution: {integrity: sha512-mlvStvUKlViTLzdD2iir0RFdYbMtThsI2SGaSJYQS4xNx6kovGnuNzqNh/zT38g7eol5oNDQOep/yV8z+Y4ztQ==, tarball: file:projects/server-activity-resources.tgz}
resolution: {integrity: sha512-m1Ha9wIYVNbZdPtMCTmDjM8wOuMMSOgiykWCZQRWzf6OBtTlM+c04oUZ51bju78JiRCwAicYNcahJe8PNXcCyg==, tarball: file:projects/server-activity-resources.tgz}
version: 0.0.0
'@rush-temp/server-activity@file:projects/server-activity.tgz':
@@ -6316,7 +6344,7 @@ packages:
version: 0.0.0
'@rush-temp/workbench-resources@file:projects/workbench-resources.tgz':
resolution: {integrity: sha512-bk7Jl+uJ94+3ceG1IJ/PjfDcxsINfvWlp0zIdwhs3Px3F/NQWhznRcI4pnb8CEWYmGWSI2AQhfzqDchC88NJ3g==, tarball: file:projects/workbench-resources.tgz}
resolution: {integrity: sha512-JVucJQFm2Dl0k/S5H3fnmMN/29bHklJ6MOCPbsPrrucIr8W59f+Wja5syWfsqvq/tvGgnhX2uCQD1Nz9gs54yg==, tarball: file:projects/workbench-resources.tgz}
version: 0.0.0
'@rush-temp/workbench@file:projects/workbench.tgz':
@@ -22292,6 +22320,131 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/inbox-assets@file:projects/inbox-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/platform': 0.7.5
'@hcengineering/platform-rig': 0.7.19(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@types/jest': 29.5.12
'@types/node': 22.15.29
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.9.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.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@8.56.0)(typescript@5.9.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@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
prettier: 3.6.2
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- sass
- stylus
- sugarss
- supports-color
- ts-node
'@rush-temp/inbox-resources@file:projects/inbox-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/analytics': 0.7.5
'@hcengineering/communication-shared': 0.7.5
'@hcengineering/communication-types': 0.7.5
'@hcengineering/core': 0.7.8
'@hcengineering/platform': 0.7.5
'@hcengineering/platform-rig': 0.7.19(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@hcengineering/rank': 0.7.5
'@hcengineering/text': 0.7.5(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)
'@hcengineering/text-markdown': 0.7.5
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.9.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.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@8.56.0)(typescript@5.9.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.20)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
fast-equals: 5.2.2
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
prettier: 3.6.2
prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20)
sass: 1.93.2
svelte: 4.2.20
svelte-check: 3.6.9(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(svelte@4.2.20)
svelte-eslint-parser: 0.33.1(svelte@4.2.20)
svelte-loader: 3.2.0(svelte@4.2.20)
svelte-preprocess: 5.1.4(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(svelte@4.2.20)(typescript@5.9.3)
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- prosemirror-inputrules
- prosemirror-model
- prosemirror-state
- prosemirror-view
- pug
- stylus
- sugarss
- supports-color
- ts-node
'@rush-temp/inbox@file:projects/inbox.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/core': 0.7.8
'@hcengineering/platform': 0.7.5
'@hcengineering/platform-rig': 0.7.19(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.9.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.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@8.56.0)(typescript@5.9.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@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
prettier: 3.6.2
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- sass
- stylus
- sugarss
- supports-color
- ts-node
'@rush-temp/integration-client@file:projects/integration-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/account-client': 0.7.6
@@ -24261,6 +24414,43 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/model-inbox@file:projects/model-inbox.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/core': 0.7.8
'@hcengineering/model': 0.7.5
'@hcengineering/platform': 0.7.5
'@hcengineering/platform-rig': 0.7.19(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
'@types/jest': 29.5.12
'@types/node': 22.15.29
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.9.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.9.3))(eslint@8.56.0)(typescript@5.9.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@8.56.0)(typescript@5.9.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@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))
prettier: 3.6.2
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- sass
- stylus
- sugarss
- supports-color
- ts-node
'@rush-temp/model-inventory@file:projects/model-inventory.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3)))(postcss@8.5.3)(sass@1.93.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.9.3))':
dependencies:
'@hcengineering/core': 0.7.8
+1 -1
View File
@@ -1 +1 @@
"0.7.269"
"0.7.278"
+3
View File
@@ -266,6 +266,9 @@
"@hcengineering/ai-assistant": "^0.7.0",
"@hcengineering/ai-assistant-assets": "^0.7.0",
"@hcengineering/ai-assistant-resources": "^0.7.0",
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/inbox-assets": "^0.7.0",
"@hcengineering/inbox-resources": "^0.7.0",
"electron-squirrel-startup": "~1.0.0",
"dotenv": "~16.0.0",
"electron-context-menu": "^4.0.4",
+16 -1
View File
@@ -24,7 +24,12 @@
box-sizing: border-box;
}
body {
background-color: var(--bg-body, #ffffff);
}
:root {
--bg-body: #ffffff;
--bg-secondary: #f3f3f3;
--bg-tertiary: #ffffff;
--bg-hover: #e8e8e8;
@@ -39,6 +44,7 @@
}
[data-theme="theme-dark"] {
--bg-body: #1a1928;
--bg-secondary: #323233;
--bg-tertiary: #252526;
--bg-hover: #2a2d2e;
@@ -236,4 +242,13 @@
</body>
</html>
</html>
<% if (typeof isWindows !== 'undefined' && isWindows) { %>
<script>
const lastUsedTheme = localStorage.getItem('last-used-theme')
if (lastUsedTheme != null) {
document.body.setAttribute('data-theme', lastUsedTheme)
}
</script>
<% } %>
+28 -18
View File
@@ -25,6 +25,7 @@ import {
closePopup,
createApp,
getCurrentResolvedLocation,
Menu,
navigate,
parseLocation,
pushRootBarProgressComponent,
@@ -33,10 +34,15 @@ import {
} from '@hcengineering/ui'
import { handleDownloadItem } from '@hcengineering/desktop-downloads'
import notification, { notificationId } from '@hcengineering/notification'
import { chatId } from '@hcengineering/chat'
import { inboxId } from '@hcengineering/inbox'
import workbench, { workbenchId, logOut } from '@hcengineering/workbench'
import view, { Action, encodeObjectURI } from '@hcengineering/view'
import { resolveLocation } from '@hcengineering/notification-resources'
import { themeStore, ThemeVariant } from '@hcengineering/theme'
import type { Application } from '@hcengineering/workbench'
import { isAllowedToRole } from '@hcengineering/workbench-resources'
import card from '@hcengineering/card'
import communication from '@hcengineering/communication'
import { isOwnerOrMaintainer, getCurrentAccount, Ref } from '@hcengineering/core'
import { configurePlatform } from './platform'
@@ -44,9 +50,6 @@ import { setupTitleBarMenu } from './titleBarMenu'
import { defineScreenShare, defineGetDisplayMedia } from './screenShare'
import { CommandLogout, CommandSelectWorkspace, CommandOpenSettings, CommandOpenInbox, CommandOpenPlanner, CommandOpenOffice, CommandOpenApplication, LaunchApplication, NotificationParams, CommandCloseTab } from './types'
import { ipcMainExposed } from './typesUtils'
import { themeStore, ThemeVariant } from '@hcengineering/theme'
import type { Application } from '@hcengineering/workbench'
import { isAllowedToRole } from '@hcengineering/workbench-resources'
import { IpcMessage } from './ipcMessages'
function currentOsIsWindows (): boolean {
@@ -82,11 +85,13 @@ window.addEventListener('DOMContentLoaded', () => {
}
})
void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => {
menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light)
}).catch(() => {
menuBar.setTheme(ThemeVariant.Light) // fallback
})
if (menuBar.lastUsedThemeIsUnknown()) {
void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => {
menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light)
}).catch(() => {
menuBar.setTheme(ThemeVariant.Light) // fallback
})
}
})
}
}
@@ -194,39 +199,44 @@ window.addEventListener('DOMContentLoaded', () => {
navigate(parseLocation(urlObject))
}
function getBasicNotificationPath (notificationParams: NotificationParams): string {
return `${workbenchId}/${notificationParams.application ?? notificationId}/${notificationId}`
function getBasicNotificationPath (worksapce: string, app: string): string {
return `${workbenchId}/${worksapce}/${app}`
}
ipcMain.handleNotificationNavigation((notificationParams: NotificationParams) => {
const currentLocation = getCurrentResolvedLocation()
const workspace = currentLocation.path[1]
// Support for new inbox with cardId (card-based)
if (notificationParams.cardId != null) {
const currentLocation = getCurrentResolvedLocation()
navigateToUrl(`${workbenchId}/${currentLocation.path[1]}/${chatId}/${notificationParams.cardId}`)
const objectUri = encodeObjectURI(notificationParams.cardId, card.class.Card)
navigateToUrl(`${workbenchId}/${workspace}/${inboxId}/${objectUri}`)
return
}
const isCommunicationEnabled = getMetadata(communication.metadata.Enabled) ?? false
const app = isCommunicationEnabled ? inboxId : notificationParams.application
// Support for old inbox with objectId + objectClass (legacy)
if (notificationParams.objectId != null && notificationParams.objectClass != null) {
const encodedObjectURI = encodeObjectURI(notificationParams.objectId, notificationParams.objectClass)
const notificationLocation = {
path: [workbenchId, notificationParams.application, notificationId, encodedObjectURI],
path: [workbenchId, workspace, app, encodedObjectURI],
fragment: undefined,
query: {}
query: undefined
}
void resolveLocation(notificationLocation).then((resolvedLocation) => {
if (resolvedLocation?.loc != null) {
navigate(resolvedLocation.loc)
} else {
navigateToUrl(`${workbenchId}/${notificationParams.application}/${notificationId}/${encodedObjectURI}`)
navigateToUrl(`${workbenchId}/${workspace}/${app}/${encodedObjectURI}`)
}
}).catch(() => {
navigateToUrl(getBasicNotificationPath(notificationParams))
navigateToUrl(getBasicNotificationPath(workspace, app))
})
} else {
// Fallback to basic notification navigation
navigateToUrl(getBasicNotificationPath(notificationParams))
navigateToUrl(getBasicNotificationPath(workspace, app))
}
})
+2 -2
View File
@@ -29,7 +29,7 @@ import workbench, { workbenchId } from '@hcengineering/workbench'
import desktopPreferences, { defaultNotificationPreference } from '@hcengineering/desktop-preferences'
import { activePreferences } from '@hcengineering/desktop-preferences-resources'
import { getDisplayInboxData, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { chatId } from '@hcengineering/chat'
import { inboxId } from '@hcengineering/inbox'
import communication from '@hcengineering/communication'
import { ipcMainExposed } from './typesUtils'
@@ -179,7 +179,7 @@ export function configureNotifications (): void {
notificationHistory.set(notification.id, notification.created.getTime())
electronAPI.sendNotification({
silent: !preferences.playSound,
application: chatId,
application: inboxId,
title: notification.content.title,
body: `${notification.content.senderName}: ${notification.content.shortText}`,
cardId: notification.cardId,
+4
View File
@@ -76,6 +76,7 @@ import { viewId } from '@hcengineering/view'
import workbench, { workbenchId } from '@hcengineering/workbench'
import { mailId } from '@hcengineering/mail'
import { chatId } from '@hcengineering/chat'
import { inboxId } from '@hcengineering/inbox'
import { achievementId } from '@hcengineering/achievement'
import communication, { communicationId } from '@hcengineering/communication'
import { emojiId } from '@hcengineering/emoji'
@@ -133,6 +134,7 @@ import '@hcengineering/view-assets'
import '@hcengineering/workbench-assets'
import '@hcengineering/mail-assets'
import '@hcengineering/chat-assets'
import '@hcengineering/inbox-assets'
import '@hcengineering/achievement-assets'
import '@hcengineering/emoji-assets'
import '@hcengineering/media-assets'
@@ -256,6 +258,7 @@ function configureI18n (): void {
addStringsLoader(cardId, async (lang: string) => await import(`@hcengineering/card-assets/lang/${lang}.json`))
addStringsLoader(mailId, async (lang: string) => await import(`@hcengineering/mail-assets/lang/${lang}.json`))
addStringsLoader(chatId, async (lang: string) => await import(`@hcengineering/chat-assets/lang/${lang}.json`))
addStringsLoader(inboxId, async (lang: string) => await import(`@hcengineering/inbox-assets/lang/${lang}.json`))
addStringsLoader(processId, async (lang: string) => await import(`@hcengineering/process-assets/lang/${lang}.json`))
addStringsLoader(achievementId, async (lang: string) => await import(`@hcengineering/achievement-assets/lang/${lang}.json`))
addStringsLoader(communicationId, async (lang: string) => await import(`@hcengineering/communication-assets/lang/${lang}.json`))
@@ -433,6 +436,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise<void
addLocation(surveyId, () => import(/* webpackChunkName: "survey" */ '@hcengineering/survey-resources'))
addLocation(cardId, () => import(/* webpackChunkName: "card" */ '@hcengineering/card-resources'))
addLocation(chatId, () => import(/* webpackChunkName: "chat" */ '@hcengineering/chat-resources'))
addLocation(inboxId, () => import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources'))
addLocation(processId, () => import(/* webpackChunkName: "process" */ '@hcengineering/process-resources'))
addLocation(achievementId, () => import(/* webpackChunkName: "achievement" */ '@hcengineering/achievement-resources'))
addLocation(communicationId, () => import(/* webpackChunkName: "communication" */ '@hcengineering/communication-resources'))
+15 -8
View File
@@ -16,7 +16,7 @@
import { IPCMainExposed, MenuBarAction } from './types'
import { isMenuBarAction } from './typesUtils'
import { TitleBarMenuState } from './titleBarMenuState'
import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme'
import { type ThemeVariantType } from '@hcengineering/theme'
const ToggleMinimizeToTrayAction: MenuBarAction = 'toggle-minimize-to-tray'
const ToggleAutoLaunchAction: MenuBarAction = 'toggle-auto-launch'
@@ -28,7 +28,7 @@ const LabelAutoLaunchEnabled = '☑ Launch at Login'
const LabelAutoLaunchDisabled = '☐ Launch at Login'
export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): Promise<MenuBar> {
const themeManager = new ThemeManager(ThemeVariant.Light)
const themeManager = new ThemeManager()
const minimizeToTrayEnabled = await ipcMain.isMinimizeToTrayEnabled()
const isAutoLaunchEnabled = await ipcMain.isAutoLaunchEnabled()
const menuManager = new MenuBarManager(root, minimizeToTrayEnabled, isAutoLaunchEnabled)
@@ -75,12 +75,19 @@ export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElem
}
export class MenuBar {
private static readonly lastUsedThemeKey = 'last-used-theme'
constructor (private readonly theme: ThemeManager) {
}
public setTheme (theme: ThemeVariantType): void {
localStorage.setItem(MenuBar.lastUsedThemeKey, theme)
this.theme.setTheme(theme)
}
public lastUsedThemeIsUnknown (): boolean {
return localStorage.getItem(MenuBar.lastUsedThemeKey) == null
}
}
export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean, autoLaunchEnabled: boolean): HTMLElement {
@@ -130,14 +137,14 @@ export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean, autoLa
}
class ThemeManager {
private readonly domThemeKey = 'data-theme'
constructor (theme: ThemeVariantType) {
this.setTheme(theme)
}
private static readonly domThemeKey = 'data-theme'
public setTheme (theme: ThemeVariantType): void {
document.body.setAttribute(this.domThemeKey, theme)
ThemeManager.setTheme(theme)
}
public static setTheme (theme: ThemeVariantType): void {
document.body.setAttribute(ThemeManager.domThemeKey, theme)
}
}
+3
View File
@@ -286,6 +286,9 @@
"@hcengineering/ai-assistant": "^0.7.0",
"@hcengineering/ai-assistant-assets": "^0.7.0",
"@hcengineering/ai-assistant-resources": "^0.7.0",
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/inbox-assets": "^0.7.0",
"@hcengineering/inbox-resources": "^0.7.0",
"readable-stream": "^4.7.0",
"svelte": "^4.2.20"
}
+4 -1
View File
@@ -73,7 +73,8 @@ import { mailId } from '@hcengineering/mail'
import { chatId } from '@hcengineering/chat'
import github, { githubId } from '@hcengineering/github'
import { bitrixId } from '@hcengineering/bitrix'
import { achievementId } from '@hcengineering/achievement'
import {inboxId} from '@hcengineering/inbox'
import {achievementId} from '@hcengineering/achievement'
import communication, { communicationId } from '@hcengineering/communication'
import { emojiId } from '@hcengineering/emoji'
import billingPlugin, { billingId } from '@hcengineering/billing'
@@ -129,6 +130,7 @@ import '@hcengineering/media-assets'
import '@hcengineering/view-assets'
import '@hcengineering/workbench-assets'
import '@hcengineering/chat-assets'
import '@hcengineering/inbox-assets'
import '@hcengineering/mail-assets'
import '@hcengineering/github-assets'
import '@hcengineering/achievement-assets'
@@ -387,6 +389,7 @@ function configureI18n (): void {
communicationId,
async (lang: string) => await import(`@hcengineering/communication-assets/lang/${lang}.json`)
)
addStringsLoader(inboxId, async (lang: string) => await import(`@hcengineering/inbox-assets/lang/${lang}.json`))
addStringsLoader(emojiId, async (lang: string) => await import(`@hcengineering/emoji-assets/lang/${lang}.json`))
addStringsLoader(billingId, async (lang: string) => await import(`@hcengineering/billing-assets/lang/${lang}.json`))
addStringsLoader(
+1 -2
View File
@@ -23,7 +23,7 @@ import {
type ViewAction,
type ViewActionAvailabilityFunction
} from '@hcengineering/view'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type NotificationGroup } from '@hcengineering/notification'
export default mergeIds(activityId, activity, {
string: {
@@ -44,7 +44,6 @@ export default mergeIds(activityId, activity, {
ids: {
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
ActivityNotificationGroup: '' as Ref<NotificationGroup>,
AddReactionNotification: '' as Ref<NotificationType>,
AddReactionAction: '' as Ref<Action>,
SaveForLaterAction: '' as Ref<Action>,
RemoveFromLaterAction: '' as Ref<Action>,
+2 -1
View File
@@ -133,6 +133,7 @@
"@hcengineering/model-emoji": "^0.7.0",
"@hcengineering/model-billing": "^0.7.0",
"@hcengineering/model-huly-mail": "^0.7.0",
"@hcengineering/model-ai-assistant": "^0.7.0"
"@hcengineering/model-ai-assistant": "^0.7.0",
"@hcengineering/model-inbox": "^0.7.0"
}
}
+6
View File
@@ -118,6 +118,7 @@ import survey, { surveyId, createModel as surveyModel } from '@hcengineering/mod
import { presenceId, createModel as presenceModel } from '@hcengineering/model-presence'
import chat, { chatId, createModel as chatModel } from '@hcengineering/model-chat'
import processes, { processId, createModel as processModel } from '@hcengineering/model-process'
import inbox, { createModel as inboxModel, inboxId } from '@hcengineering/model-inbox'
import { achievementId, createModel as achievementModel } from '@hcengineering/model-achievement'
import { emojiId, createModel as emojiModel } from '@hcengineering/model-emoji'
import { billingId, createModel as billingModel } from '@hcengineering/model-billing'
@@ -480,6 +481,11 @@ export default function buildModel (): Builder {
chatId,
{ label: chat.string.Chat, hidden: true, enabled: false, beta: true, classFilter: defaultFilter }
],
[
inboxModel,
inboxId,
{ label: inbox.string.Inbox, hidden: true, enabled: false, beta: true, classFilter: defaultFilter }
],
[achievementModel, achievementId],
[emojiModel, emojiId],
[communicationModel, communicationId],
+2
View File
@@ -57,6 +57,7 @@ import { surveyOperation } from '@hcengineering/model-survey'
import { cardOperation } from '@hcengineering/model-card'
import { aiBotId, aiBotOperation } from '@hcengineering/model-ai-bot'
import { chatId, chatOperation } from '@hcengineering/model-chat'
import { inboxId, inboxOperation } from '@hcengineering/model-inbox'
import { processId, processOperation } from '@hcengineering/model-process'
import { communicationId, communicationOperation } from '@hcengineering/model-communication'
import { recorderId, recorderOperation } from '@hcengineering/model-recorder'
@@ -106,6 +107,7 @@ export const migrateOperations: [string, MigrateOperation][] = [
['survey', surveyOperation],
[aiBotId, aiBotOperation],
[chatId, chatOperation],
[inboxId, inboxOperation],
[processId, processOperation],
[communicationId, communicationOperation],
[recorderId, recorderOperation]
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@hcengineering/model-inbox",
"version": "0.7.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Hardcore Engineering Inc",
"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",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"test": "jest --passWithNoTests --silent --forceExit"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@typescript-eslint/eslint-plugin": "^6.21.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.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"@types/node": "^22.15.29",
"jest": "^29.7.0",
"@types/jest": "^29.5.5",
"ts-jest": "^29.1.1"
},
"dependencies": {
"@hcengineering/card": "^0.7.0",
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/inbox-resources": "^0.7.0",
"@hcengineering/core": "^0.7.8",
"@hcengineering/model": "^0.7.5",
"@hcengineering/model-core": "^0.7.0",
"@hcengineering/model-workbench": "^0.7.0",
"@hcengineering/platform": "^0.7.5",
"@hcengineering/setting": "^0.7.0",
"@hcengineering/ui": "^0.7.0",
"@hcengineering/view": "^0.7.0",
"@hcengineering/workbench": "^0.7.0"
}
}
+42
View File
@@ -0,0 +1,42 @@
//
// Copyright © 2025 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 Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import workbench from '@hcengineering/model-workbench'
import { inboxId } from '@hcengineering/inbox'
import inbox from './plugin'
export { inboxId } from '@hcengineering/inbox'
export { inboxOperation } from './migration'
export default inbox
export function createModel (builder: Builder): void {
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: inbox.string.Inbox,
icon: inbox.icon.Inbox,
alias: inboxId,
hidden: false,
component: inbox.component.InboxApplication,
position: 'top',
order: 100
},
inbox.app.Inbox
)
}
+21
View File
@@ -0,0 +1,21 @@
//
// Copyright © 2025 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 MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model'
export const inboxOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {}
}
+26
View File
@@ -0,0 +1,26 @@
//
// Copyright © 2025 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 { inboxId } from '@hcengineering/inbox'
import inbox from '@hcengineering/inbox-resources/src/plugin'
import { type Ref } from '@hcengineering/core'
import { type Application } from '@hcengineering/model-workbench'
import { mergeIds } from '@hcengineering/platform'
export default mergeIds(inboxId, inbox, {
app: {
Inbox: '' as Ref<Application>
}
})
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+1
View File
@@ -39,6 +39,7 @@
"@hcengineering/contact": "^0.7.0",
"@hcengineering/core": "^0.7.8",
"@hcengineering/model": "^0.7.5",
"@hcengineering/model-activity": "^0.7.0",
"@hcengineering/model-attachment": "^0.7.0",
"@hcengineering/model-core": "^0.7.0",
"@hcengineering/model-preference": "^0.7.0",
+16 -11
View File
@@ -14,7 +14,7 @@
// limitations under the License.
//
import activity, { type ActivityMessage } from '@hcengineering/activity'
import activity, { type ActivityMessage, type Reaction } from '@hcengineering/activity'
import { type PersonSpace } from '@hcengineering/contact'
import {
AccountRole,
@@ -83,7 +83,8 @@ import {
type NotificationType,
type NotificationTypeSetting,
type PushSubscription,
type PushSubscriptionKeys
type PushSubscriptionKeys,
type ReactionInboxNotification
} from '@hcengineering/notification'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
@@ -285,6 +286,17 @@ export class TMentionInboxNotification extends TCommonInboxNotification implemen
mentionedInClass!: Ref<Class<Doc>>
}
@Model(notification.class.ReactionInboxNotification, notification.class.CommonInboxNotification)
export class TReactionInboxNotification extends TCommonInboxNotification implements ReactionInboxNotification {
emoji!: string
ref!: Ref<Reaction>
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
attachedTo!: Ref<ActivityMessage>
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
attachedToClass!: Ref<Class<ActivityMessage>>
}
@Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL)
export class TActivityNotificationViewlet extends TDoc implements ActivityNotificationViewlet {
messageMatch!: DocumentQuery<Doc>
@@ -358,7 +370,8 @@ export function createModel (builder: Builder): void {
TNotificationProvider,
TNotificationProviderSetting,
TNotificationTypeSetting,
TNotificationProviderDefaults
TNotificationProviderDefaults,
TReactionInboxNotification
)
builder.mixin(notification.class.BrowserNotification, core.class.Class, core.mixin.TransientConfiguration, {
@@ -637,14 +650,6 @@ export function createModel (builder: Builder): void {
notification.category.Notification
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: notification.component.ReactionNotificationPresenter
})
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_NOTIFICATION,
indexes: [{ keys: { user: 1, archived: 1, space: 1 } }],
+93 -1
View File
@@ -44,9 +44,12 @@ import notification, {
type BrowserNotification,
type DocNotifyContext,
type InboxNotification,
type OldCollaborators
type OldCollaborators,
type ReactionInboxNotification,
type ActivityInboxNotification
} from '@hcengineering/notification'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import activity, { type ActivityMessage, type DocUpdateMessage, type Reaction } from '@hcengineering/activity'
import {
DOMAIN_SPACE,
@@ -56,6 +59,7 @@ import {
getSocialIdFromOldAccount
} from '@hcengineering/model-core'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY } from './index'
import { DOMAIN_ACTIVITY, DOMAIN_REACTION } from '@hcengineering/model-activity'
export async function removeNotifications (
client: MigrationClient,
@@ -311,6 +315,88 @@ async function migrateCollaborators (client: MigrationClient): Promise<void> {
client.logger.log('finished processing collaborators ', {})
}
async function migrateReactionNotifications (client: MigrationClient): Promise<void> {
const hierarchy = client.hierarchy
const iterator = await client.traverse<DocNotifyContext>(DOMAIN_DOC_NOTIFY, {
_class: notification.class.DocNotifyContext
})
try {
while (true) {
const contexts = await iterator.next(500)
const res: ReactionInboxNotification[] = []
const removeIds: Ref<InboxNotification>[] = []
if (contexts == null || contexts.length === 0) break
const filtered = contexts.filter((it) => hierarchy.isDerived(it.objectClass, activity.class.ActivityMessage))
if (filtered.length === 0) continue
for (const context of filtered) {
const notifications = await client.find<ActivityInboxNotification>(DOMAIN_NOTIFICATION, {
docNotifyContext: context._id,
_class: notification.class.ActivityInboxNotification,
attachedToClass: activity.class.DocUpdateMessage
})
if (notifications.length === 0) continue
const messages = await client.find<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_id: { $in: notifications.map((it) => it.attachedTo) as Ref<DocUpdateMessage>[] },
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
})
const contextMessage = (
await client.find<ActivityMessage>(DOMAIN_ACTIVITY, { _id: context.objectId as any })
)[0]
if (contextMessage == null) continue
const newContext = (
await client.find<DocNotifyContext>(DOMAIN_DOC_NOTIFY, {
user: context.user,
objectId: contextMessage.attachedTo
})
)[0]
if (newContext == null) {
continue
}
for (const it of notifications) {
const reactionMessage = messages.find((m) => m._id === it.attachedTo)
if (reactionMessage == null) continue
const emoji =
it.data ??
(await client.find<Reaction>(DOMAIN_REACTION, { _id: reactionMessage.objectId as Ref<Reaction> }))[0].emoji
if (emoji == null || emoji.trim() === '') continue
res.push({
_id: it._id as Ref<ReactionInboxNotification>,
_class: notification.class.ReactionInboxNotification,
space: it.space,
emoji,
user: it.user,
docNotifyContext: newContext._id,
objectId: newContext.objectId,
objectClass: newContext.objectClass,
ref: reactionMessage.objectId as Ref<Reaction>,
attachedTo: reactionMessage.attachedTo as Ref<ActivityMessage>,
attachedToClass: reactionMessage.attachedToClass as Ref<Class<ActivityMessage>>,
isViewed: it.isViewed,
archived: it.archived,
modifiedOn: it.modifiedOn,
createdBy: it.createdBy,
createdOn: it.createdOn,
modifiedBy: it.modifiedBy
})
}
}
if (removeIds.length > 0) {
await client.deleteMany(DOMAIN_NOTIFICATION, { _id: { $in: removeIds } })
}
if (res.length > 0) {
await client.create(DOMAIN_NOTIFICATION, res)
}
}
} catch (e) {
console.error(e)
}
}
/**
* Migrates old accounts to new accounts/social ids.
* Should be applied to prodcution directly without applying migrateSocialIdsToAccountUuids
@@ -396,6 +482,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
client.logger.log('finished processing collaborators ', {})
client.logger.log('processing notifications fields ', {})
function chunkArray<T> (array: T[], chunkSize: number): T[][] {
const chunks: T[][] = []
for (let i = 0; i < array.length; i += chunkSize) {
@@ -845,6 +932,11 @@ export const notificationOperation: MigrateOperation = {
state: 'migrate-collaborators-v2',
mode: 'upgrade',
func: migrateCollaborators
},
{
state: 'migrate-reaction-notifications',
mode: 'upgrade',
func: migrateReactionNotifications
}
])
},
+5 -5
View File
@@ -315,7 +315,7 @@ export function createModel (builder: Builder): void {
},
label: process.string.RunProcess,
icon: process.icon.Process,
input: 'focus',
input: 'any',
category: view.category.General,
target: card.class.Card,
context: {
@@ -588,10 +588,10 @@ export function createModel (builder: Builder): void {
component: process.component.ProcessesSettingSection
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: workbench.extensions.WorkbenchExtensions,
component: process.component.NotifierExtension
})
// builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
// extension: workbench.extensions.WorkbenchExtensions,
// component: process.component.NotifierExtension
// })
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
-4
View File
@@ -26,10 +26,6 @@ export { activityServerOperation } from './migration'
export { serverActivityId } from '@hcengineering/server-activity'
export function createModel (builder: Builder): void {
builder.mixin(activity.class.Reaction, core.class.Class, serverNotification.mixin.NotificationPresenter, {
presenter: serverActivity.function.ReactionNotificationContentProvider
})
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverActivity.function.DocUpdateMessageTextPresenter
})
@@ -593,5 +593,54 @@ describe('drawing module tests', () => {
])
})
})
describe('tools selection', () => {
it('select color after panning', () => {
const colorsList: ColorsList = [
['alpha', new ThemeAwareColor('red', 'yellow')],
['beta', new ThemeAwareColor('blue', 'blue')]
]
const toolChangedSpy = jest.fn()
const initialPenColor: ColorMetaNameOrHex = 'alpha'
const systemUnderTest = drawing(drawingPlugInPoint, {
colorsList,
getCurrentTheme: () => 'theme-light',
subscribeOnThemeChange: () => {},
readonly: false,
imageWidth: 400,
imageHeight: 300,
commands: [],
tool: 'pen',
penColor: initialPenColor,
toolChanged: toolChangedSpy
})
systemUnderTest.update?.({
colorsList,
getCurrentTheme: () => 'theme-light',
subscribeOnThemeChange: () => {},
readonly: false,
tool: 'pan',
toolChanged: toolChangedSpy
})
expect(toolChangedSpy).toHaveBeenCalledWith('pan')
toolChangedSpy.mockClear()
const newColor: ColorMetaNameOrHex = 'beta'
systemUnderTest.update?.({
colorsList,
getCurrentTheme: () => 'theme-light',
subscribeOnThemeChange: () => {},
readonly: false,
penColor: newColor,
toolChanged: toolChangedSpy
})
expect(toolChangedSpy).toHaveBeenCalledWith('pen')
})
})
})
})
@@ -230,6 +230,7 @@
penWidth,
eraserWidth,
fontSize,
enableMiddleMousePanning: false,
changingCmdId,
cmdAdded: addCommand,
cmdChanging: showCommandProps,
@@ -240,6 +241,9 @@
cmdDeleted: deleteCommand,
editorCreated: (editor) => {
cmdEditor = editor
},
toolChanged: (newTool) => {
tool = newTool
}
}}
>
+60 -17
View File
@@ -57,6 +57,7 @@ export interface DrawingProps {
changingCmdId?: CommandUid
personCursorPos?: Point
personCursorVisible?: boolean
enableMiddleMousePanning?: boolean
cmdAdded?: (cmd: DrawingCmd) => void
cmdChanging?: (id: CommandUid) => void
cmdUnchanged?: (id: CommandUid) => void
@@ -67,6 +68,7 @@ export interface DrawingProps {
personCursorMoved?: (nodePos: Point) => void
panning?: (offset: Point) => void
panned?: (offset: Point) => void
toolChanged?: (tool: DrawingTool) => void
}
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text'
@@ -81,6 +83,7 @@ type PointStatus = 'last-point' | 'intermediate-point'
class DrawState {
on = false
usedBeforePanningTool: DrawingTool | undefined = undefined
tool: DrawingTool = 'pen'
penColor: ColorMetaNameOrHex = 'alpha'
penWidth = 4
@@ -373,6 +376,7 @@ export function drawing (
resizeObserver.observe(canvas)
let touchId: number | undefined
let isMiddleMousePanning = false
function findTouch (touches: TouchList, id: number | undefined = touchId): Touch | undefined {
for (let i = 0; i < touches.length; i++) {
@@ -398,7 +402,8 @@ export function drawing (
}
const touch = e.changedTouches[0]
touchId = touch.identifier
drawStart(touchToNodePoint(touch, canvas))
const forcePan = false
drawStart(touchToNodePoint(touch, canvas), forcePan)
}
canvas.ontouchmove = (e) => {
@@ -407,7 +412,8 @@ export function drawing (
}
const touch = findTouch(e.changedTouches)
if (touch !== undefined) {
drawContinue(touchToNodePoint(touch, canvas))
const forcePan = false
drawContinue(touchToNodePoint(touch, canvas), forcePan)
}
}
@@ -417,7 +423,8 @@ export function drawing (
}
const touch = findTouch(e.changedTouches)
if (touch !== undefined) {
drawEnd(touchToNodePoint(touch, canvas))
const forcePan = false
drawEnd(touchToNodePoint(touch, canvas), forcePan)
}
touchId = undefined
}
@@ -428,12 +435,22 @@ export function drawing (
if (readonly) {
return
}
const MiddleMouseButton = 1
if (e.button === MiddleMouseButton && props.enableMiddleMousePanning === true) {
e.preventDefault()
isMiddleMousePanning = true
canvas.setPointerCapture(e.pointerId)
const forcePan = true
drawStart(pointerToNodePoint(e), forcePan)
return
}
if (e.button !== 0) {
return
}
e.preventDefault()
canvas.setPointerCapture(e.pointerId)
drawStart(pointerToNodePoint(e))
const forcePan = false
drawStart(pointerToNodePoint(e), forcePan)
}
canvas.onpointermove = (e) => {
@@ -441,7 +458,8 @@ export function drawing (
return
}
e.preventDefault()
drawContinue(pointerToNodePoint(e))
const forcePan = isMiddleMousePanning
drawContinue(pointerToNodePoint(e), forcePan)
}
canvas.onpointerup = (e) => {
@@ -450,10 +468,23 @@ export function drawing (
}
e.preventDefault()
canvas.releasePointerCapture(e.pointerId)
drawEnd(pointerToNodePoint(e))
const forcePan = isMiddleMousePanning
drawEnd(pointerToNodePoint(e), forcePan)
if (e.button === 1) {
isMiddleMousePanning = false
}
}
canvas.onpointercancel = canvas.onpointerup
canvas.onpointercancel = (e) => {
if (readonly) {
return
}
e.preventDefault()
canvas.releasePointerCapture(e.pointerId)
const forcePan = isMiddleMousePanning
drawEnd(pointerToNodePoint(e), forcePan)
isMiddleMousePanning = false
}
canvas.onpointerenter = () => {
if (!readonly && draw.isDrawingTool()) {
@@ -472,20 +503,20 @@ export function drawing (
return makeMouseScaledPoint(scaled.x, scaled.y)
}
function drawStart (p: NodePoint): void {
function drawStart (p: NodePoint, forcePan: boolean): void {
const scaledPoint = rescaleWithCss(p)
draw.on = true
draw.points = []
prevPos = scaledPoint
if (draw.isDrawingTool()) {
if (!forcePan && draw.isDrawingTool()) {
draw.addPoint(scaledPoint)
}
}
function drawContinue (p: NodePoint): void {
function drawContinue (p: NodePoint, forcePan: boolean): void {
const scaledPoint = rescaleWithCss(p)
if (draw.isDrawingTool()) {
if (!forcePan && draw.isDrawingTool()) {
const cursorSize = draw.cursorWidth()
const canvasOffsetInParent = offsetInParent(node, canvas)
@@ -501,7 +532,7 @@ export function drawing (
}
}
if (draw.on && draw.tool === 'pan') {
if (draw.on && (draw.tool === 'pan' || forcePan)) {
requestAnimationFrame(() => {
draw.offset.x += scaledPoint.x - prevPos.x
draw.offset.y += scaledPoint.y - prevPos.y
@@ -513,22 +544,22 @@ export function drawing (
})
}
if (draw.on && draw.tool === 'text') {
if (draw.on && draw.tool === 'text' && !forcePan) {
prevPos = scaledPoint
}
if (props.pointerMoved !== undefined) {
if (props.pointerMoved !== undefined && !forcePan) {
props.pointerMoved(draw.mouseToCanvasPoint(scaledPoint))
}
}
function drawEnd (p: NodePoint): void {
function drawEnd (p: NodePoint, forcePan: boolean): void {
const scaledPoint = rescaleWithCss(p)
if (draw.on) {
if (draw.isDrawingTool()) {
if (!forcePan && draw.isDrawingTool()) {
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
storeLineCommand()
} else if (draw.tool === 'pan') {
} else if (draw.tool === 'pan' || forcePan) {
props.panned?.(draw.offset)
} else if (draw.tool === 'text') {
if (liveTextBox !== undefined) {
@@ -1024,6 +1055,11 @@ export function drawing (
}
if (props.tool !== undefined) {
if (draw.tool !== props.tool) {
if (props.tool === 'pan') {
draw.usedBeforePanningTool = draw.tool
} else {
draw.usedBeforePanningTool = props.tool
}
draw.tool = props.tool
syncToolCursor = true
toolChanged = true
@@ -1031,6 +1067,10 @@ export function drawing (
}
if (props.penColor !== undefined) {
if (draw.penColor !== props.penColor) {
if (draw.tool === 'pan' && draw.usedBeforePanningTool != null) {
draw.tool = draw.usedBeforePanningTool
toolChanged = true
}
draw.penColor = props.penColor
syncLiveTextBox = true
syncToolCursor = true
@@ -1084,6 +1124,9 @@ export function drawing (
}
}
if (toolChanged) {
props.toolChanged?.(draw.tool)
}
if (syncToolCursor) {
updateToolCursor()
}
@@ -19,7 +19,7 @@ import core, {
type Client,
type Collection,
type Doc,
groupByArrayAsync,
groupByArray,
type Hierarchy,
type Mixin,
type Ref,
@@ -29,7 +29,6 @@ import view, { type AttributeModel } from '@hcengineering/view'
import { getClient, getFiltredKeys } from '@hcengineering/presentation'
import {
buildRemovedDoc,
checkIsObjectRemoved,
getAttributePresenter,
getDocLinkTitle,
hasAttributePresenter
@@ -37,7 +36,6 @@ import {
import contact, { type Person } from '@hcengineering/contact'
import { type IntlString } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui'
import { getPersonRefByPersonId } from '@hcengineering/contact-resources'
import activity, {
type ActivityMessage,
type DisplayActivityMessage,
@@ -241,10 +239,10 @@ function wrapMessages (
return { toCombine, uncombined }
}
export async function combineActivityMessages (
export function combineActivityMessages (
messages: ActivityMessage[],
sortingOrder: SortingOrder = SortingOrder.Ascending
): Promise<DisplayActivityMessage[]> {
): DisplayActivityMessage[] {
const client = getClient()
const { uncombined, toCombine } = wrapMessages(client.getHierarchy(), messages)
@@ -256,10 +254,7 @@ export async function combineActivityMessages (
const result: Array<DisplayActivityMessage | undefined> = [...uncombined]
const groupedByType: Map<string, DocUpdateMessage[]> = await groupByArrayAsync(
docUpdateMessages,
getDocUpdateMessageKey
)
const groupedByType: Map<string, DocUpdateMessage[]> = groupByArray(docUpdateMessages, getDocUpdateMessageKey)
for (const [, groupedMessages] of groupedByType) {
const cantMerge = groupedMessages.filter(
@@ -280,40 +275,6 @@ export async function combineActivityMessages (
result.push(...cantMerge)
}
const viewlets = client.getModel().findAllSync(activity.class.DocUpdateMessageViewlet, {})
for (const [index, message] of result.entries()) {
if (message?._class !== activity.class.DocUpdateMessage) {
continue
}
const docUpdateMessage = message as DocUpdateMessage
if (
docUpdateMessage.action === 'update' &&
!hasAttributeModel(client, docUpdateMessage.attributeUpdates, docUpdateMessage.objectClass)
) {
result[index] = undefined
continue
}
const hideIfRemoved = viewlets.some(
(viewlet) =>
viewlet.action === docUpdateMessage.action &&
viewlet.hideIfRemoved === true &&
viewlet.objectClass === docUpdateMessage.objectClass
)
if (!hideIfRemoved) {
continue
}
const isRemoved = await checkIsObjectRemoved(client, docUpdateMessage.objectId, docUpdateMessage.objectClass)
if (isRemoved) {
result[index] = undefined
}
}
return sortActivityMessages(
result.filter((msg): msg is DisplayActivityMessage => msg !== undefined),
sortingOrder
@@ -367,17 +328,21 @@ function groupByTime<T extends ActivityMessage> (messages: T[]): T[][] {
return result
}
async function getDocUpdateMessageKey (message: DocUpdateMessage): Promise<string> {
const personRef = await getPersonRefByPersonId(message.createdBy as any)
function getDocUpdateMessageKey (message: DocUpdateMessage): string {
if (message.action === 'update') {
return [message._class, message.attachedTo, message.action, personRef, getAttributeUpdatesKey(message)].join('_')
return [
message._class,
message.attachedTo,
message.action,
message.createdBy,
getAttributeUpdatesKey(message)
].join('_')
}
return [
message._class,
message.attachedTo,
personRef,
message.createdBy,
message.updateCollection,
message.objectId === message.attachedTo
].join('_')
@@ -579,18 +544,6 @@ export function isActivityMessage (message?: Doc): message is ActivityMessage {
return getClient().getHierarchy().isDerived(message._class, activity.class.ActivityMessage)
}
export function isReactionMessage (message?: ActivityMessage): message is DocUpdateMessage {
if (message === undefined) {
return false
}
if (!isDocUpdateMessage(message)) {
return false
}
return message.objectClass === activity.class.Reaction
}
export function isActivityMessageClass (_class?: Ref<Class<Doc>>): boolean {
if (_class === undefined) {
return false
@@ -215,17 +215,15 @@
$: allMessages = sortActivityMessages(messages.concat(refs))
async function updateActivityMessages (objectId: Ref<Doc>, order: SortingOrder): Promise<void> {
function updateActivityMessages (objectId: Ref<Doc>, order: SortingOrder): void {
isMessagesLoading = true
const res = activityMessagesQuery.query(
activity.class.ActivityMessage,
{ attachedTo: objectId, space: getSpace(object) },
(result: ActivityMessage[]) => {
void combineActivityMessages(result, order).then((res) => {
messages = res
isMessagesLoading = false
})
messages = combineActivityMessages(result, order)
isMessagesLoading = false
},
{
sort: {
@@ -252,7 +250,7 @@
void scrollToMessage(selectedMessageId)
}
$: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
$: updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
export function editLastMessage (): void {
if (isMessagesLoading) return
@@ -36,6 +36,8 @@
export let headerIcon: Asset | undefined = undefined
export let header: IntlString | undefined = undefined
export let headerParams: Record<string, any> = {}
export let color: 'primary' | 'secondary' = 'primary'
export let lower = false
const client = getClient()
const limit = 300
@@ -132,8 +134,9 @@
{#if text || intlLabel}
<span
class="textContent overflow-label font-normal"
class="textContent overflow-label font-normal {color}"
class:contentOnly={type === 'content-only'}
class:lower
use:tooltip={{ label: text ? getEmbeddedLabel(getTooltipText(text)) : intlLabel }}
>
{#if intlLabel}
@@ -249,6 +252,9 @@
max-height: 1.25rem;
color: var(--global-primary-TextColor);
&.secondary {
color: var(--global-secondary-TextColor);
}
&.contentOnly {
margin-left: 0;
}
@@ -64,9 +64,12 @@
}
return (inboxNotificationsByContext?.get(context._id) ?? [])
.filter((notification) => {
const activityNotifications = notification as ActivityInboxNotification
return activityNotifications.attachedToClass !== activity.class.DocUpdateMessage
.filter((it) => {
const activityNotifications = it as ActivityInboxNotification
return (
activityNotifications.attachedToClass !== activity.class.DocUpdateMessage &&
it._class !== notification.class.ReactionInboxNotification
)
})
.some(({ isViewed }) => !isViewed)
}
@@ -40,6 +40,7 @@
{#if previewMixin}
<Component
is={previewMixin.presenter}
showLoading={false}
props={{
value,
type,
@@ -30,7 +30,6 @@
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
import { savedMessagesStore } from '../../activity'
import { isReactionMessage } from '../../activityMessagesUtils'
import { MessageInlineAction } from '../../types'
import ActivityMessageActions from '../ActivityMessageActions.svelte'
import MessageTimestamp from '../MessageTimestamp.svelte'
@@ -276,7 +275,7 @@
class:isShort
>
<ActivityMessageActions
message={isReactionMessage(message) ? parentMessage : message}
{message}
{actions}
{withActionMenu}
{excludedActions}
+2 -1
View File
@@ -341,7 +341,8 @@ export default plugin(activityId, {
},
ids: {
AllFilter: '' as Ref<ActivityMessagesFilter>,
MentionNotification: '' as Ref<Doc>
MentionNotification: '' as Ref<Doc>,
AddReactionNotification: '' as Ref<any>
},
extension: {
ActivityEmployeePresenter: '' as ComponentExtensionId
+1 -2
View File
@@ -119,7 +119,6 @@ async function generateLocation (loc: Location, id: string): Promise<ResolvedLoc
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const space = doc.space
const special = doc._class
const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
@@ -131,7 +130,7 @@ async function generateLocation (loc: Location, id: string): Promise<ResolvedLoc
fragment: getPanelURI(component, doc._id, doc._class, 'content')
},
defaultLocation: {
path: [appComponent, workspace, cardId, space, special],
path: [appComponent, workspace, cardId, 'type', special],
fragment: getPanelURI(component, doc._id, doc._class, 'content')
}
}
@@ -20,31 +20,15 @@
import chat, { chatId } from '@hcengineering/chat'
import { Navigator } from '@hcengineering/card-resources'
import communication from '@hcengineering/communication'
import { ModernButton, NavGroup } from '@hcengineering/ui'
import { NavGroup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createNotificationsQuery } from '@hcengineering/presentation'
import NotifyMarker from './inbox/NotifyMarker.svelte'
import NavigationHeader from './NavigationHeader.svelte'
import InboxNavigation from './inbox/InboxNavigation.svelte'
import InboxHeader from './inbox/InboxHeader.svelte'
export let card: Card | undefined = undefined
export let type: Ref<MasterTag> | undefined = undefined
export let special: 'favorites' | 'all' | string | undefined = undefined
export let mode: 'chat' | 'inbox' = 'chat'
const dispatch = createEventDispatcher()
let hasNewInboxNotifications: boolean = false
const notificationCountQuery = createNotificationsQuery()
notificationCountQuery.query({ read: false, limit: 1 }, (res) => {
hasNewInboxNotifications = res.getResult().length > 0
})
function getSpecial (special: 'favorites' | string | undefined): string | undefined {
if (special === 'favorites') {
return 'favorites'
@@ -52,85 +36,52 @@
return undefined
}
function handleInboxClick (): void {
mode = 'inbox'
}
function handleAllUpdatesClick (): void {
dispatch('selectAll')
}
function selectInboxCard (event: CustomEvent): void {
const card = event.detail?.card
if (card != null) {
dispatch('selectCard', card)
}
}
</script>
{#if mode === 'inbox'}
<InboxHeader bind:mode />
<InboxNavigation {card} on:select={selectInboxCard} />
{:else}
<NavigationHeader label={chat.string.Chat}>
<div slot="actions" class="flex-row-center">
<ModernButton
icon={chat.icon.Inbox}
label={chat.string.Inbox}
size="small"
iconSize="small"
on:click={handleInboxClick}
>
{#if hasNewInboxNotifications}
<div class="flex pl-0-5">
<NotifyMarker kind="simple" size="xx-small" />
</div>
{/if}
</ModernButton>
</div>
</NavigationHeader>
<div class="navigation-section">
<NavGroup
_id="all"
categoryName="all"
type="selectable-header"
icon={chat.icon.All}
label={chat.string.All}
empty={true}
selected={special === 'all'}
noDivider
on:click={handleAllUpdatesClick}
/>
</div>
<Navigator
config={{
variant: 'cards',
types: [cardPlugin.class.Card],
savedViews: true,
groupBySpace: false,
hideEmpty: true,
limit: 5,
labelFilter: [SubscriptionLabelID],
preorder: [
{ type: chat.masterTag.Thread, order: 1 },
{ type: communication.type.Direct, order: 2 }
],
fixedTypes: [chat.masterTag.Thread, communication.type.Direct],
allowCreate: true,
defaultSorting: 'recent',
lookback: '2w',
showTypeIcon: false,
showCardIcon: true
}}
applicationId={chatId}
selectedType={type}
selectedCard={card?._id}
selectedSpecial={getSpecial(special)}
on:selectType
on:selectCard
on:favorites
<div class="navigation-section">
<NavGroup
_id="all"
categoryName="all"
type="selectable-header"
icon={chat.icon.All}
label={chat.string.All}
empty={true}
selected={special === 'all'}
noDivider
on:click={handleAllUpdatesClick}
/>
{/if}
</div>
<Navigator
config={{
variant: 'cards',
types: [cardPlugin.class.Card],
savedViews: true,
groupBySpace: false,
hideEmpty: true,
limit: 5,
labelFilter: [SubscriptionLabelID],
preorder: [
{ type: chat.masterTag.Thread, order: 1 },
{ type: communication.type.Direct, order: 2 }
],
fixedTypes: [chat.masterTag.Thread, communication.type.Direct],
allowCreate: true,
defaultSorting: 'recent',
lookback: '2w',
showTypeIcon: false,
showCardIcon: true
}}
applicationId={chatId}
selectedType={type}
selectedCard={card?._id}
selectedSpecial={getSpecial(special)}
on:selectType
on:selectCard
on:favorites
/>
<style lang="scss">
.navigation-section {
@@ -1,181 +0,0 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { NotificationContext } from '@hcengineering/communication-types'
import { Card } from '@hcengineering/card'
import { createEventDispatcher } from 'svelte'
import { createNotificationsQuery, getCommunicationClient } from '@hcengineering/presentation'
import { CheckBox, Spinner } from '@hcengineering/ui'
import { AccountRole, getCurrentAccount } from '@hcengineering/core'
import InboxNotification from './InboxNotification.svelte'
import InboxCardIcon from './InboxCardIcon.svelte'
export let context: NotificationContext
export let card: Card
export let selected: boolean = false
const account = getCurrentAccount()
const dispatch = createEventDispatcher()
const communicationClient = getCommunicationClient()
const notificationsQuery = createNotificationsQuery()
let total = 0
notificationsQuery.query({ limit: 1, total: true, read: false, strict: true, contextId: context.id }, (res) => {
total = res.getTotal()
})
let isRemoving = false
async function handleToggle (): Promise<void> {
isRemoving = true
try {
await communicationClient.removeNotificationContext(context.id)
} finally {
isRemoving = false
}
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="inbox-card" class:selected on:click={() => dispatch('select', { context, card })}>
<div class="inbox-card__header">
<InboxCardIcon {card} count={total ?? 0} />
<div class="inbox-card__labels">
<span class="inbox-card__title overflow-label clear-mins" title={card.title}>
{card.title}
</span>
</div>
{#if account.role !== AccountRole.ReadOnlyGuest}
<div class="inbox-card__remove">
{#if isRemoving}
<Spinner size="small" />
{:else}
<CheckBox kind="todo" size="medium" on:value={handleToggle} />
{/if}
</div>
{/if}
</div>
<div class="inbox-card__content">
<div class="inbox-card__notifications">
{#each context.notifications ?? [] as notification}
<div class="inbox-card__notification">
<div class="inbox-card__marker" />
<InboxNotification {notification} {card} />
</div>
{/each}
</div>
</div>
</div>
<style lang="scss">
.inbox-card {
display: flex;
flex-direction: column;
position: relative;
cursor: pointer;
padding: 0.5rem;
border-bottom: 1px solid var(--global-ui-BorderColor);
min-height: 5.625rem;
&.selected {
background-color: var(--global-ui-highlight-BackgroundColor);
}
&:hover:not(.selected) {
background-color: var(--global-ui-highlight-BackgroundColor);
}
&__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: var(--spacing-0_5);
}
&__labels {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
color: var(--global-primary-TextColor);
font-size: 0.875rem;
overflow: hidden;
}
&__title {
font-weight: 400;
color: var(--global-primary-TextColor);
overflow: hidden;
text-overflow: ellipsis;
}
&__remove {
display: flex;
align-items: center;
margin-left: auto;
min-width: 1.5rem;
}
&__content {
display: flex;
width: 100%;
padding-left: 0.5rem;
}
&__notifications {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
margin-top: var(--spacing-1);
}
&__notification {
position: relative;
display: flex;
flex-direction: column;
cursor: pointer;
user-select: none;
&:first-child .inbox-card__marker {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
&:last-child .inbox-card__marker {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
&:hover .inbox-card__marker {
border-radius: 0.5rem;
background: var(--global-primary-LinkColor);
}
}
&__marker {
position: absolute;
width: 0.25rem;
height: 100%;
background: var(--global-ui-highlight-BackgroundColor);
border-radius: 0;
}
}
</style>
@@ -1,68 +0,0 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import cardPlugin, { Card } from '@hcengineering/card'
import { Component } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import communication from '@hcengineering/communication'
import NotifyMarker from './NotifyMarker.svelte'
export let card: Card
export let count: number = 0
const client = getClient()
const hierarchy = client.getHierarchy()
$: size = hierarchy.isDerived(card._class, communication.type.Direct) ? 'large' : 'medium'
</script>
<div class="card-icon">
<Component is={cardPlugin.component.CardIcon} props={{ value: card, size }} />
{#if count > 0}
<div class="card-icon__marker">
<NotifyMarker {count} size="small" />
</div>
{/if}
</div>
<style lang="scss">
.card-icon {
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
min-width: 2.5rem;
min-height: 2.5rem;
color: var(--global-secondary-TextColor);
background-color: var(--global-ui-BackgroundColor);
border: 1px solid var(--global-subtle-ui-BorderColor);
border-radius: var(--medium-BorderRadius);
fill: var(--global-secondary-TextColor);
&__marker {
position: absolute;
top: -0.375rem;
right: 0;
transform: translateX(calc(100% - 0.875rem));
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
@@ -1,77 +0,0 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { SortingOrder } from '@hcengineering/core'
import { IconDelete, IconSettings, Label, ModernButton, showPopup, eventToHTMLElement } from '@hcengineering/ui'
import { getCommunicationClient } from '@hcengineering/presentation'
import chat from '../../plugin'
import InboxViewSettings from './InboxViewSettings.svelte'
import { Analytics } from '@hcengineering/analytics'
const communicationClient = getCommunicationClient()
export let mode: 'chat' | 'inbox' = 'chat'
let clearing = false
async function clearInbox (): Promise<void> {
if (clearing) return
try {
clearing = true
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
await Promise.all(contexts.map((context) => communicationClient.removeNotificationContext(context.id)))
clearing = false
} catch (e: any) {
clearing = false
Analytics.handleError(e)
}
}
function click (e: MouseEvent): void {
showPopup(InboxViewSettings, {}, eventToHTMLElement(e))
}
function goToChat (): void {
mode = 'chat'
}
</script>
<div class="hulyNavPanel-header withButton small inbox-header">
<span class="overflow-label"><Label label={chat.string.Inbox} /></span>
<div class="flex-row-center flex-gap-2">
<ModernButton
tooltip={{ label: chat.string.ClearAll }}
icon={IconDelete}
size="small"
iconSize="small"
on:click={clearInbox}
loading={clearing}
/>
<ModernButton icon={IconSettings} on:click={click} size="small" iconSize="small" />
<div class="spacer" />
<ModernButton
label={chat.string.Chat}
icon={chat.icon.ChatBubble}
size="small"
iconSize="small"
on:click={goToChat}
/>
</div>
</div>
<style lang="scss">
.inbox-header {
background-color: var(--theme-popup-color);
border-bottom: 1px solid var(--theme-navpanel-border);
}
</style>
@@ -1,201 +0,0 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import cardPlugin, { Card } from '@hcengineering/card'
import { Label, ListView, Loading, Scroller } from '@hcengineering/ui'
import { SortingOrder } from '@hcengineering/core'
import { createNotificationContextsQuery, createQuery, getCommunicationClient } from '@hcengineering/presentation'
import { NotificationContext, Window } from '@hcengineering/communication-types'
import { createEventDispatcher } from 'svelte'
import InboxCard from './InboxCard.svelte'
import chat from '../../plugin'
export let card: Card | undefined = undefined
const dispatch = createEventDispatcher()
const limit = 20
let cards: Card[] = []
let contexts: NotificationContext[] = []
const communicationClient = getCommunicationClient()
const cardsQuery = createQuery()
const query = createNotificationContextsQuery()
let divScroll: HTMLElement | undefined | null = undefined
let contentDiv: HTMLElement | undefined | null = undefined
let window: Window<NotificationContext> | undefined = undefined
let list: ListView
let listSelection = 0
let isLoading = true
query.query(
{
notifications: {
order: SortingOrder.Descending,
limit: 3
},
order: SortingOrder.Descending,
limit
},
(res) => {
window = res
contexts = res.getResult().filter((it) => (it.notifications?.length ?? 0) > 0)
isLoading = false
if (contexts.length < limit && window.hasPrevPage()) {
void window.loadPrevPage()
}
},
{ message: true }
)
$: cardsQuery.query(cardPlugin.class.Card, { _id: { $in: contexts.map((c) => c.cardId) } }, (res) => {
cards = res
})
function handleScroll (): void {
if (divScroll != null && window != null && window.hasPrevPage()) {
const isAtBottom = divScroll.scrollTop + divScroll.clientHeight >= divScroll.scrollHeight - 10
if (isAtBottom) {
void window.loadPrevPage()
}
}
}
async function onKeydown (key: KeyboardEvent): Promise<void> {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection + 1)
}
if (key.code === 'Backspace') {
key.preventDefault()
key.stopPropagation()
const context = contexts[listSelection]
if (context != null) {
await communicationClient.removeNotificationContext(context.id)
}
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const context = contexts[listSelection]
const card = cards.find((c) => c._id === context.cardId)
if (card && context) {
dispatch('select', { context, card })
}
}
}
$: if (contentDiv != null) {
contentDiv.focus()
}
function getContextKey (index: number): string {
const contextId = contexts[index].id
return contextId ?? index.toString()
}
</script>
{#if contexts.length > 0}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Scroller bind:divScroll padding="0" shrink onScroll={handleScroll}>
<div class="inbox-list" bind:this={contentDiv} tabindex="0" on:keydown={onKeydown}>
<ListView
bind:this={list}
bind:selection={listSelection}
count={contexts.length}
items={contexts}
highlightIndex={contexts.findIndex((it) => it.cardId === card?._id)}
noScroll
kind="full-size"
colorsSchema="lumia"
lazy={true}
getKey={getContextKey}
>
<svelte:fragment slot="item" let:item={itemIndex}>
{@const context = contexts[itemIndex]}
{@const contextCard = cards.find((c) => c._id === context.cardId)}
{#if context && contextCard}
<InboxCard
{context}
card={contextCard}
on:select={(e) => {
dispatch('select', e.detail)
listSelection = itemIndex
}}
/>
{/if}
</svelte:fragment>
</ListView>
</div>
</Scroller>
{:else if isLoading}
<div class="placeholder">
<Loading />
</div>
{:else}
<div class="placeholder">
<div class="placeholder__header">
<Label label={chat.string.InboxIsClear} />
</div>
<span class="mb-4">
<Label label={chat.string.YouDontHaveAnyNewMessages} />
</span>
</div>
{/if}
<style lang="scss">
.inbox-list {
display: flex;
flex-direction: column;
width: 100%;
&:focus {
outline: 0;
}
:global(.list-item) {
border-radius: 0;
}
}
.placeholder {
display: flex;
align-self: center;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
height: inherit;
width: 100%;
gap: 0.5rem;
&__header {
font-weight: 600;
}
}
</style>
@@ -291,7 +291,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {})
},
async (res) => {
const result = await combineActivityMessages(res.reverse())
const result = combineActivityMessages(res.reverse())
this.tailStore.set(result)
this.isTailLoaded.set(true)
@@ -368,7 +368,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
return {
from: from.createdOn ?? from.modifiedOn,
to: to.createdOn ?? to.modifiedOn,
data: isBackward ? await combineActivityMessages(messages.reverse()) : await combineActivityMessages(messages)
data: isBackward ? combineActivityMessages(messages.reverse()) : combineActivityMessages(messages)
}
}
@@ -22,6 +22,7 @@
export let loadingOverlay: boolean = false
export let onScroll: () => void = () => {}
export let onResize: () => void = () => {}
export let key: string
</script>
{#if loadingOverlay}
@@ -41,7 +42,9 @@
{onScroll}
{onResize}
>
<slot />
{#key key}
<slot />
{/key}
</Scroller>
<style lang="scss">
@@ -611,6 +611,9 @@
editLastMessage()
}
}
function getKey (messages: ActivityMessage[]): string {
return `${messages.length}-${Math.max(...messages.map((m) => m.modifiedOn))}`
}
</script>
<div class="flex-col relative" class:h-full={fullHeight}>
@@ -627,6 +630,7 @@
loadingOverlay={$isLoadingStore || !isScrollInitialized}
onScroll={handleScroll}
onResize={handleResize}
key={getKey(messages)}
>
{#if showBlankView}
<BlankView
@@ -48,17 +48,15 @@
</script>
<BaseMessagePreview text={value.message} message={value} {type} {readonly} {actions} on:click>
{#if value.attachments && type === 'full' && !isEmptyMarkup(value.message)}
{#if value.attachments && !isEmptyMarkup(value.message)}
<div class="attachments" use:tooltip={{ component: AttachmentsTooltip, props: { attachments } }}>
{value.attachments}
<Icon icon={attachment.icon.Attachment} size="small" />
</div>
{:else if attachments.length > 0 && isEmptyMarkup(value.message)}
<span class="font-normal">
<span class="font-normal secondaryColor">
<Label label={attachment.string.Attachments} />:
<span class="ml-1">
{attachments.map(({ name }) => name).join(', ')}
</span>
{attachments.map(({ name }) => name).join(', ')}
</span>
{/if}
</BaseMessagePreview>
@@ -77,4 +75,9 @@
color: var(--global-primary-TextColor);
}
}
.secondaryColor {
color: var(--global-secondary-TextColor);
margin-left: -0.5rem;
}
</style>
@@ -61,7 +61,7 @@
}
</script>
<span class="flex-presenter flex-gap-1 font-semi-bold">
<span class="flex-presenter flex-gap-1 header">
{#if isThread || (object.replies ?? 0) > 0}
<Label label={chunter.string.Thread} />
{:else}
@@ -84,10 +84,17 @@
{/await}
{/if}
</span>
<span class="font-normal">
<span class="font-normal mt-1">
{#if isThread}
<ThreadMessagePreview value={toThread(object)} readonly type="content-only" />
{:else}
<ChatMessagePreview value={object} readonly type="content-only" />
{/if}
</span>
<style lang="scss">
.header {
font-size: 0.875rem;
font-weight: 600;
}
</style>
@@ -13,12 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { DisplayDocUpdateMessage } from '@hcengineering/activity'
import { ActivityMessagePreviewType, DisplayDocUpdateMessage } from '@hcengineering/activity'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import chunter from '../../plugin'
export let message: DisplayDocUpdateMessage
export let type: ActivityMessagePreviewType = 'full'
</script>
<BaseMessagePreview intlLabel={chunter.string.YouJoinedChannel} {message} on:click />
<BaseMessagePreview intlLabel={chunter.string.YouJoinedChannel} {message} {type} on:click />
@@ -15,8 +15,10 @@
<script lang="ts">
import { ThreadMessage } from '@hcengineering/chunter'
import ThreadMessagePreview from '../threads/ThreadMessagePreview.svelte'
import { ActivityMessagePreviewType } from '@hcengineering/activity'
export let message: ThreadMessage
export let type: ActivityMessagePreviewType = 'full'
</script>
<ThreadMessagePreview value={message} on:click />
<ThreadMessagePreview value={message} {type} on:click />
+8 -10
View File
@@ -19,7 +19,6 @@ import activity, {
type DisplayDocUpdateMessage,
type DocUpdateMessage
} from '@hcengineering/activity'
import { isReactionMessage } from '@hcengineering/activity-resources'
import aiBot from '@hcengineering/ai-bot'
import { summarizeMessages as aiSummarizeMessages, translate as aiTranslate } from '@hcengineering/ai-bot-resources'
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
@@ -42,7 +41,8 @@ import notification, { type DocNotifyContext, type InboxNotification } from '@hc
import {
InboxNotificationsClientImpl,
isActivityNotification,
isMentionNotification
isMentionNotification,
isReactionNotification
} from '@hcengineering/notification-resources'
import { type Asset, getMetadata, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
@@ -414,13 +414,7 @@ export async function readChannelMessages (
const notifications = get(inboxClient.activityInboxNotifications)
.filter(({ attachedTo, $lookup, isViewed }) => {
if (isViewed) return false
const includes = allIds.includes(attachedTo)
if (includes) return true
const msg = $lookup?.attachedTo
if (isReactionMessage(msg)) {
return allIds.includes(msg.attachedTo as Ref<ActivityMessage>)
}
return false
return allIds.includes(attachedTo)
})
.map((n) => n._id)
@@ -428,6 +422,10 @@ export async function readChannelMessages (
.filter((n) => !n.isViewed && isMentionNotification(n) && allIds.includes(n.mentionedIn as Ref<ActivityMessage>))
.map((n) => n._id)
const reactionNotifications = get(inboxClient.otherInboxNotifications)
.filter((n) => !n.isViewed && isReactionNotification(n) && allIds.includes(n.attachedTo))
.map((n) => n._id)
chatReadMessagesStore.update((store) => new Set([...store, ...allIds]))
const storedTimestampUpdates = get(contextsTimestampStore).get(context._id)
@@ -441,7 +439,7 @@ export async function readChannelMessages (
})
await op.update(context, { lastViewedTimestamp: newTimestamp })
}
await inboxClient.readNotifications(op, [...notifications, ...relatedMentions])
await inboxClient.readNotifications(op, [...notifications, ...relatedMentions, ...reactionNotifications])
} finally {
await op.commit()
}
+1 -1
View File
@@ -38,7 +38,7 @@
"@types/snappyjs": "^0.7.1"
},
"dependencies": {
"@hcengineering/analytics": "^0.7.3",
"@hcengineering/analytics": "^0.7.5",
"@hcengineering/client": "^0.7.3",
"@hcengineering/communication-sdk-types": "^0.7.5",
"@hcengineering/communication-types": "^0.7.5",
@@ -19,9 +19,10 @@
import type { Ref } from '@hcengineering/core'
import { Blob as PlatformBlob } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
import {
import presentation, { Card, getFileUrl, uiContext } from '@hcengineering/presentation'
import ui, {
AnySvelteComponent,
Button,
ColorDefinition,
Label,
TabList,
@@ -171,6 +172,12 @@
}
}
function handleClearClick (): void {
selectedAvatar = undefined
selectedAvatarType = AvatarType.COLOR
selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
}
const showColorPopup = (event: MouseEvent) => {
showPopup(
ColorsPopup,
@@ -238,6 +245,9 @@
on:select={handleDropdownSelection}
/>
</div>
<svelte:fragment slot="buttons">
<Button minWidth={'5rem'} label={ui.string.Clear} kind={'ghost'} size={'large'} on:click={handleClearClick} />
</svelte:fragment>
<svelte:fragment slot="footer">
{#if selectedAvatarType === AvatarType.GRAVATAR}
<div class="flex-col">
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="inbox" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 1.5C8.5 1.22386 8.27614 1 8 1C7.72386 1 7.5 1.22386 7.5 1.5V2.02746C5.25002 2.27619 3.5 4.18372 3.5 6.5V7.44766C3.5 8.01534 3.3068 8.56611 2.95217 9.00939L2.09004 10.0871C1.70809 10.5645 1.5 11.1577 1.5 11.7691C1.5 12.4489 2.05108 13 2.73087 13H6C6 13.2626 6.05173 13.5227 6.15224 13.7654C6.25275 14.008 6.40007 14.2285 6.58579 14.4142C6.7715 14.5999 6.99198 14.7472 7.23463 14.8478C7.47728 14.9483 7.73736 15 8 15C8.26264 15 8.52272 14.9483 8.76537 14.8478C9.00802 14.7472 9.2285 14.5999 9.41421 14.4142C9.59993 14.2285 9.74725 14.008 9.84776 13.7654C9.94827 13.5227 10 13.2626 10 13H13.2691C13.9489 13 14.5 12.4489 14.5 11.7691C14.5 11.1577 14.2919 10.5645 13.91 10.0871L13.0478 9.00939C12.6932 8.56611 12.5 8.01534 12.5 7.44766V6.5C12.5 4.18372 10.75 2.27619 8.5 2.02746V1.5ZM9 13H7C7 13.1313 7.02587 13.2614 7.07612 13.3827C7.12638 13.504 7.20003 13.6142 7.29289 13.7071C7.38575 13.8 7.49599 13.8736 7.61732 13.9239C7.73864 13.9741 7.86868 14 8 14C8.13132 14 8.26136 13.9741 8.38268 13.9239C8.50401 13.8736 8.61425 13.8 8.70711 13.7071C8.79997 13.6142 8.87362 13.504 8.92388 13.3827C8.97413 13.2614 9 13.1313 9 13ZM2.5 11.7691C2.5 11.8966 2.60336 12 2.73087 12H13.2691C13.3966 12 13.5 11.8966 13.5 11.7691C13.5 11.3848 13.3692 11.0119 13.1291 10.7118L12.267 9.63409C11.7705 9.01349 11.5 8.24241 11.5 7.44766V6.5C11.5 4.567 9.933 3 8 3C6.067 3 4.5 4.567 4.5 6.5V7.44766C4.5 8.24241 4.22952 9.01349 3.73304 9.63409L2.87091 10.7118C2.63081 11.0119 2.5 11.3848 2.5 11.7691Z" />
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Doručená pošta",
"ReactedToYourMessage": "reagoval na vaši zprávu",
"ClearAll": "Vymazať všetko",
"InboxIsClear": "Doručená pošta je vymazaná",
"YouDontHaveAnyNewMessages": "Nemáte žiadne nové správy",
"ReadAll": "Prečítať všetko",
"Clearing": "Vymazávam...",
"Reading": "Čítam...",
"HideUserNames": "Skryť mená používateľov"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Posteingang",
"ReactedToYourMessage": "reagierte auf Ihre Nachricht",
"ClearAll": "Alles löschen",
"InboxIsClear": "Posteingang ist leer",
"YouDontHaveAnyNewMessages": "Sie haben keine neuen Nachrichten",
"ReadAll": "Alle lesen",
"Clearing": "Lösche...",
"Reading": "Lese...",
"HideUserNames": "Benutzernamen ausblenden"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Inbox",
"ReactedToYourMessage": "reacted to your message",
"ClearAll": "Clear all",
"InboxIsClear": "Inbox is clear",
"YouDontHaveAnyNewMessages": "You don’t have any new messages",
"ReadAll": "Read all",
"Clearing": "Clearing...",
"Reading": "Reading...",
"HideUserNames": "Hide user names"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Bande",
"ReactedToYourMessage": "reaccionó a tu mensaje",
"ClearAll": "Borrar todo",
"InboxIsClear": "Bande está vacía",
"YouDontHaveAnyNewMessages": "No tienes ninguna nueva mensaje",
"ReadAll": "Leer todo",
"Clearing": "Borrando...",
"Reading": "Leyendo...",
"HideUserNames": "Ocultar nombres de usuario"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Boîte de réception",
"ReactedToYourMessage": "a réagi à votre message",
"ClearAll": "Tout effacer",
"InboxIsClear": "Boîte de réception est vide",
"YouDontHaveAnyNewMessages": "Vous n'avez aucun nouveau message",
"ReadAll": "Tout lire",
"Clearing": "Effacement...",
"Reading": "Lecture...",
"HideUserNames": "Cacher les noms d'utilisateur"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Inbox",
"ReactedToYourMessage": "ha reagito al tuo messaggio",
"ClearAll": "Cancella tutto",
"InboxIsClear": "Inbox è vuoto",
"YouDontHaveAnyNewMessages": "Non hai alcun nuovo messaggio",
"ReadAll": "Leggi tutto",
"Clearing": "Cancellazione in corso...",
"Reading": "Lettura in corso...",
"HideUserNames": "Nascondi nomi utente"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "受信箱",
"ReactedToYourMessage": "あなたのメッセージに反応しました",
"ClearAll": "すべてクリア",
"InboxIsClear": "受信箱が空です",
"YouDontHaveAnyNewMessages": "新しいメッセージはありません",
"ReadAll": "すべて読む",
"Clearing": "クリア中...",
"Reading": "読み込み中...",
"HideUserNames": "ユーザー名を非表示"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Caixa de entrada",
"ReactedToYourMessage": "reacionou ao seu mensagem",
"ClearAll": "Limpar tudo",
"InboxIsClear": "Caixa de entrada está vazia",
"YouDontHaveAnyNewMessages": "Não tem nenhuma nova mensagem",
"ReadAll": "Ler tudo",
"Clearing": "Limpando...",
"Reading": "Lendo...",
"HideUserNames": "Esconder nomes de usuário"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "Входящие",
"ReactedToYourMessage": "отреагировал(а) на ваше сообщение",
"ClearAll": "Очистить всё",
"InboxIsClear": "Входящие пусты",
"YouDontHaveAnyNewMessages": "У вас нет новых сообщений",
"ReadAll": "Прочитать всё",
"Clearing": "Очистка...",
"Reading": "Чтение...",
"HideUserNames": "Скрыть имена пользователей"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"string": {
"Inbox": "收件箱",
"ReactedToYourMessage": "对您的消息做出了反应",
"ClearAll": "清除全部",
"InboxIsClear": "收件箱是空的",
"YouDontHaveAnyNewMessages": "您没有任何新消息",
"ReadAll": "全部已读",
"Clearing": "正在清除...",
"Reading": "正在读取...",
"HideUserNames": "隐藏用户名"
}
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@hcengineering/inbox-assets",
"version": "0.7.0",
"main": "src/index.ts",
"author": "Hardcore Engineering Inc",
"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.7.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.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.6.2",
"@types/node": "^22.15.29",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"typescript": "^5.9.3"
},
"dependencies": {
"@hcengineering/platform": "^0.7.5",
"@hcengineering/inbox": "^0.7.0"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
@@ -0,0 +1,21 @@
//
// Copyright © 2025 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 { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)
+22
View File
@@ -0,0 +1,22 @@
//
// Copyright © 2025 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 inbox from '@hcengineering/inbox'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(inbox.icon, {
Inbox: `${icons}#inbox`
})
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"types": ["node", "jest"],
"tsBuildInfoFile": ".build/build.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}
+22
View File
@@ -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"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
}
+68
View File
@@ -0,0 +1,68 @@
{
"name": "@hcengineering/inbox-resources",
"version": "0.7.0",
"main": "src/index.ts",
"author": "Hardcore Engineering Inc",
"license": "EPL-2.0",
"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": {
"@hcengineering/platform-rig": "^0.7.19",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.54.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-plugin-svelte": "^2.35.1",
"jest": "^29.7.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"sass": "^1.80.0",
"svelte-check": "^3.6.9",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.2.0",
"svelte-preprocess": "^5.1.4",
"ts-jest": "^29.1.1",
"typescript": "^5.9.3"
},
"dependencies": {
"@hcengineering/analytics": "^0.7.5",
"@hcengineering/card": "^0.7.0",
"@hcengineering/communication": "^0.7.0",
"@hcengineering/communication-resources": "^0.7.0",
"@hcengineering/communication-types": "^0.7.5",
"@hcengineering/communication-shared": "^0.7.5",
"@hcengineering/contact": "^0.7.0",
"@hcengineering/contact-resources": "^0.7.0",
"@hcengineering/core": "^0.7.8",
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/platform": "^0.7.5",
"@hcengineering/presentation": "^0.7.0",
"@hcengineering/rank": "^0.7.5",
"@hcengineering/text": "^0.7.5",
"@hcengineering/text-markdown": "^0.7.5",
"@hcengineering/ui": "^0.7.0",
"@hcengineering/view": "^0.7.0",
"@hcengineering/view-resources": "^0.7.0",
"@hcengineering/emoji-resources": "^0.7.0",
"@hcengineering/activity": "^0.7.0",
"@hcengineering/activity-resources": "^0.7.0",
"@hcengineering/chunter": "^0.7.0",
"@hcengineering/notification": "^0.7.0",
"@hcengineering/notification-resources": "^0.7.0",
"fast-equals": "^5.2.2",
"svelte": "^4.2.20"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright © 2025 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 InboxNotificationsClient } from '@hcengineering/notification'
import { writable, derived, get } from 'svelte/store'
import {
createNotificationContextsQuery,
getCommunicationClient,
onCommunicationClient
} from '@hcengineering/presentation'
import { notEmpty, SortingOrder } from '@hcengineering/core'
import cardPlugin from '@hcengineering/card'
import { type NotificationContext, type Window } from '@hcengineering/communication-types'
import { getDisplayInboxData, removeContextNotifications } from '@hcengineering/notification-resources'
import { type NavigationItem } from './type'
const limit = 20
const isLoadingStore = writable<boolean>(true)
const contextsWindowStore = writable<Window<NotificationContext> | undefined>(undefined)
const contextStore = writable<NotificationContext[]>([])
const contextsQuery = createNotificationContextsQuery(true)
export class NavigationClient {
private readonly contextStore = contextStore
private readonly contextsWindowStore = contextsWindowStore
public readonly isLoadingStore = isLoadingStore
private readonly limitStore = writable<number>(limit)
private isPrevLoading = false
private readonly modernNavigationItemsStore = derived(
[contextStore],
([contexts]): NavigationItem[] => {
return contexts
.map((context): NavigationItem => {
return {
type: 'modern',
_id: context.cardId,
_class: cardPlugin.class.Card,
context,
date: context.lastNotify ?? context.lastUpdate
}
})
.filter(notEmpty)
},
[] as NavigationItem[]
)
private readonly legacyNavigationItemsStore
public readonly navigationItemsStore
public readonly allNavigationItemsStore
constructor (private readonly oldClient: InboxNotificationsClient) {
this.legacyNavigationItemsStore = derived(
[this.oldClient.inboxNotificationsByContext, this.oldClient.contextById],
([notificationsByContext, contextById]) => {
const inboxData = getDisplayInboxData(notificationsByContext)
return Array.from(inboxData.entries())
.map(([ctx, notifications]): NavigationItem | undefined => {
if (notifications.length === 0) return undefined
const context = contextById.get(ctx)
if (context == null) return undefined
return {
type: 'legacy',
_id: context.objectId,
_class: context.objectClass,
context,
date: new Date(notifications[0].createdOn ?? Date.now()),
notifications: notifications.slice(0, 3)
}
})
.filter(notEmpty)
}
)
this.allNavigationItemsStore = derived(
[this.modernNavigationItemsStore, this.legacyNavigationItemsStore],
([modern, legacy]): NavigationItem[] =>
[...modern, ...legacy].sort((a, b) => b.date.getTime() - a.date.getTime()),
[] as NavigationItem[]
)
this.navigationItemsStore = derived(
[this.allNavigationItemsStore, this.isLoadingStore, this.limitStore],
([items, isLoading, limit]): NavigationItem[] => (isLoading ? [] : items.slice(0, limit)),
[] as NavigationItem[]
)
}
public hasPrevPage (): boolean {
const allItems = get(this.allNavigationItemsStore)
const navItems = get(this.navigationItemsStore)
const hasNextPage = get(contextsWindowStore)?.hasPrevPage() ?? false
return hasNextPage || allItems.length > navItems.length
}
public async prev (): Promise<void> {
if (this.isPrevLoading || !this.hasPrevPage()) return
const w = get(contextsWindowStore)
if (w == null) return
this.isPrevLoading = true
await w.loadPrevPage()
this.limitStore.update((it) => it + limit)
this.isPrevLoading = false
}
public async remove (item: NavigationItem): Promise<void> {
if (item.type === 'modern') {
const client = getCommunicationClient()
await client.removeNotificationContext(item.context.id)
} else {
await removeContextNotifications(item.context)
}
}
}
function updateContextsQuery (): void {
contextsQuery.query(
{
notifications: {
order: SortingOrder.Descending,
limit: 3
},
order: SortingOrder.Descending,
limit
},
(res) => {
contextsWindowStore.set(res)
const contexts = res.getResult().filter((it) => (it.notifications?.length ?? 0) > 0)
contextStore.set(contexts)
isLoadingStore.set(false)
if (contexts.length < limit && res.hasPrevPage()) {
void res.loadPrevPage()
}
},
{ message: true }
)
}
onCommunicationClient(() => {
updateContextsQuery()
})
@@ -0,0 +1,274 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import {
defineSeparators,
Separator,
deviceOptionsStore as deviceInfo,
resolvedLocationStore,
Location,
restoreLocation,
Component,
closePanel,
getCurrentLocation
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { getClient } from '@hcengineering/presentation'
import { inboxId } from '@hcengineering/inbox'
import view from '@hcengineering/view'
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import { Notification } from '@hcengineering/communication-types'
import chunter from '@hcengineering/chunter'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { get } from 'svelte/store'
import cardPlugin from '@hcengineering/card'
import InboxNavigation from './InboxNavigation.svelte'
import { closeDoc, getDocInfoFromLocation, getMessageInfoFromLocation, navigateToDoc } from '../location'
import InboxHeader from './InboxHeader.svelte'
import { NavigationItem } from '../type'
const client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient()
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
const contextByIdStore = inboxClient.contextById
const contextByDocStore = inboxClient.contextByDoc
let replacedPanelElement: HTMLElement
let doc: Doc | undefined = undefined
let legacyContext: DocNotifyContext | undefined = undefined
let legacyMessage: ActivityMessage | undefined = undefined
let needRestoreLoc = true
let urlObjectId: Ref<Doc> | undefined = undefined
let urlObjectClass: Ref<Class<Doc>> | undefined = undefined
async function syncLocation (loc: Location): Promise<void> {
if (loc.path[2] !== inboxId) {
return
}
const docInfo = getDocInfoFromLocation(loc)
if (docInfo == null) {
doc = undefined
urlObjectId = undefined
urlObjectClass = undefined
legacyContext = undefined
if (needRestoreLoc) {
needRestoreLoc = false
restoreLocation(loc, inboxId)
}
return
}
urlObjectId = docInfo._id
urlObjectClass = docInfo._class
needRestoreLoc = false
const thread = loc?.path[4] as Ref<ActivityMessage>
if (docInfo._id !== doc?._id) {
doc = await client.findOne(docInfo._class, { _id: docInfo._id })
if (doc != null && !hierarchy.isDerived(doc._class, cardPlugin.class.Card)) {
const queryContext = loc.query?.context as Ref<DocNotifyContext>
const ctx =
$contextByIdStore.get(queryContext) ?? $contextByDocStore.get(thread) ?? $contextByDocStore.get(urlObjectId)
legacyContext =
ctx ??
(await client.findOne(notification.class.DocNotifyContext, {
objectId: doc._id,
user: getCurrentAccount().uuid
}))
}
}
const messageInfo = getMessageInfoFromLocation(loc)
const messageId = messageInfo?.id
if (thread !== undefined) {
const fn = await getResource(chunter.function.OpenThreadInSidebar)
void fn(thread, undefined, undefined, messageInfo?.id as Ref<ActivityMessage>, { autofocus: false }, false)
}
if (messageId != null && messageInfo?.date == null) {
legacyMessage = get(inboxClient.activityInboxNotifications).find(({ attachedTo }) => attachedTo === messageId)
?.$lookup?.attachedTo
if (legacyMessage === undefined) {
legacyMessage = await client.findOne(activity.class.ActivityMessage, { _id: messageId as Ref<ActivityMessage> })
}
}
}
let selectedNotificationId: string | undefined = undefined
function select (
event: CustomEvent<{
navItem: NavigationItem
doc: Doc
notification?: InboxNotification | Notification
}>
): void {
const { navItem, notification, doc: ddoc } = event.detail
if (ddoc == null) return
const loc = getCurrentLocation()
const notificationId = (notification as any)?._id ?? (notification as any)?.id
if (
navItem.type === 'modern' &&
doc?._id === navItem._id &&
loc.path[2] === inboxId &&
selectedNotificationId === notificationId
) {
return
}
if (
navItem.type === 'legacy' &&
doc?._id === navItem._id &&
legacyContext?._id === navItem.context._id &&
loc.path[2] === inboxId &&
selectedNotificationId === notificationId
) {
return
}
closePanel()
selectedNotificationId = notificationId
navigateToDoc(navItem, ddoc, notification)
}
function handleClose (): void {
closePanel()
doc = undefined
legacyContext = undefined
legacyMessage = undefined
closeDoc()
}
function isChunterChannel (_class: Ref<Class<Doc>>, urlObjectClass?: Ref<Class<Doc>>): boolean {
const isActivityMessageContext = hierarchy.isDerived(_class, activity.class.ActivityMessage)
const chunterClass = isActivityMessageContext ? (urlObjectClass ?? _class) : _class
return hierarchy.isDerived(chunterClass, chunter.class.ChunterSpace)
}
$: void readLegacyDoc(doc, legacyContext, urlObjectClass)
async function readLegacyDoc (
doc: Doc | undefined,
selectedContext?: DocNotifyContext,
urlObjectClass?: Ref<Class<Doc>>
): Promise<void> {
if (doc == null) return
const isChunter = isChunterChannel(doc._class, urlObjectClass)
const contextNotifications = $notificationsByContextStore.get(selectedContext?._id ?? ('' as any)) ?? []
const ops = getClient().apply(undefined, 'readNotifications')
try {
await inboxClient.readNotifications(
ops,
contextNotifications
.filter(({ _class, isViewed }) =>
isChunter ? _class === notification.class.CommonInboxNotification : !isViewed
)
.map(({ _id }) => _id)
)
} finally {
await ops.commit()
}
}
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void syncLocation(loc)
})
)
defineSeparators('new-inbox', [
{ minSize: 15, maxSize: 60, size: 30, float: 'navigator' },
{ size: 'auto', minSize: 20, maxSize: 'auto' }
])
$: $deviceInfo.replacedPanel = replacedPanelElement
onDestroy(() => ($deviceInfo.replacedPanel = undefined))
</script>
<div class="hulyPanels-container inbox">
{#if $deviceInfo.navigator.visible}
<div
class="antiPanel-navigator {$deviceInfo.navigator.direction === 'horizontal'
? 'portrait'
: 'landscape'} border-left inbox__navigator"
class:fly={$deviceInfo.navigator.float}
>
<div class="antiPanel-wrap__content hulyNavPanel-container">
<InboxHeader />
<div class="antiPanel-wrap__content hulyNavPanel-container">
<InboxNavigation {doc} {legacyContext} on:select={select} />
</div>
</div>
{#if !($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
<Separator name="new-inbox" float={$deviceInfo.navigator.float ? 'navigator' : true} index={0} />
{/if}
</div>
<Separator
name="new-inbox"
float={$deviceInfo.navigator.float}
index={0}
color={'transparent'}
separatorSize={0}
short
/>
{/if}
<div bind:this={replacedPanelElement} class="hulyComponent inbox__panel">
{#if doc}
{@const panel = client.getHierarchy().classHierarchyMixin(doc._class, view.mixin.ObjectPanel)}
<Component
is={panel?.component ?? view.component.EditDoc}
props={{
_id: doc._id,
_class: doc._class,
context: legacyContext,
autofocus: false,
embedded: true,
activityMessage: legacyMessage,
props: { autofocus: false, context: legacyContext, activityMessage: legacyMessage }
}}
on:close={handleClose}
/>
{/if}
</div>
</div>
<style lang="scss">
.inbox {
&__navigator {
position: relative;
}
&__panel {
position: relative;
}
}
</style>
@@ -0,0 +1,230 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createNotificationsQuery, createQuery } from '@hcengineering/presentation'
import { CheckBox, Loading, Spinner } from '@hcengineering/ui'
import { AccountRole, Doc, getCurrentAccount } from '@hcengineering/core'
import notification, { ActivityNotificationViewlet, InboxNotification } from '@hcengineering/notification'
import { Card } from '@hcengineering/card'
import { Notification, NotificationType } from '@hcengineering/communication-types'
import InboxCardIcon from './InboxCardIcon.svelte'
import InboxCardTitle from './InboxCardTitle.svelte'
import ModernNotifications from './ModernNotifications.svelte'
import LegacyNotifications from './legacy/LegacyNotifications.svelte'
import { NavigationItem } from '../type'
import { NavigationClient } from '../client'
import { isReactionNotification } from '@hcengineering/notification-resources'
export let navClient: NavigationClient
export let navItem: NavigationItem
export let selected: boolean = false
export let viewlets: ActivityNotificationViewlet[] = []
const account = getCurrentAccount()
const dispatch = createEventDispatcher()
let total = 0
const modernNotificationsQuery = createNotificationsQuery()
const legacyNotificationsQuery = createQuery()
const query = createQuery()
let doc: Doc | undefined = undefined
let isLoading = true
query.query(
navItem._class,
{ _id: navItem._id },
(res) => {
doc = res[0]
dispatch('doc', doc)
isLoading = false
},
{ limit: 1 }
)
$: if (doc !== undefined && doc?._id !== navItem._id) {
doc = undefined
isLoading = true
}
$: if (navItem.type === 'modern') {
legacyNotificationsQuery.unsubscribe()
modernNotificationsQuery.query(
{ limit: 1, total: true, read: false, strict: true, contextId: navItem.context.id },
(res) => {
total = res.getTotal()
}
)
} else {
modernNotificationsQuery.unsubscribe()
legacyNotificationsQuery.query(
notification.class.InboxNotification,
{ isViewed: false, docNotifyContext: navItem.context._id },
(res) => {
total = res.total
},
{
total: true,
limit: 1
}
)
}
let isRemoving = false
async function handleToggle (): Promise<void> {
isRemoving = true
try {
await navClient.remove(navItem)
} finally {
isRemoving = false
}
}
function asCard (doc: Doc): Card {
return doc as Card
}
function onNotification (event: CustomEvent<InboxNotification | Notification>): void {
if (doc == null) return
dispatch('select', { doc, notification: event.detail })
}
function calcHeight (navItem: NavigationItem): number {
const base = 4.125
const messageHeightPx = 2
const reactionsHeightPx = 4
let res = base
if (navItem.type === 'modern') {
for (const n of navItem.context.notifications ?? []) {
if (n.type === NotificationType.Message) {
res += messageHeightPx
} else if (n.type === NotificationType.Reaction) {
res += reactionsHeightPx
}
}
} else {
for (const n of navItem.notifications) {
if (isReactionNotification(n)) {
res += reactionsHeightPx
} else {
res += messageHeightPx
}
}
}
return res
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="inbox-card"
class:selected
style:height={`${calcHeight(navItem)}rem`}
on:click={() => {
if (doc == null) return
dispatch('select', { doc })
}}
>
{#if isLoading}
<div class="loading">
<Loading />
</div>
{:else if doc}
<div class="inbox-card__header">
<InboxCardIcon {doc} count={total ?? 0} />
<InboxCardTitle {doc} {navItem} />
{#if account.role !== AccountRole.ReadOnlyGuest}
<div class="inbox-card__remove">
{#if isRemoving}
<Spinner size="small" />
{:else}
<CheckBox kind="todo" size="medium" on:value={handleToggle} />
{/if}
</div>
{/if}
</div>
<div class="inbox-card__content">
<div class="inbox-card__notifications">
{#if navItem.type === 'modern'}
<ModernNotifications doc={asCard(doc)} context={navItem.context} on:click={onNotification} />
{:else}
<LegacyNotifications {doc} notifications={navItem.notifications} {viewlets} on:click={onNotification} />
{/if}
</div>
</div>
{/if}
</div>
<style lang="scss">
.loading {
display: flex;
align-items: center;
flex: 1;
}
.inbox-card {
display: flex;
flex-direction: column;
position: relative;
cursor: pointer;
padding: 0.5rem;
border-bottom: 1px solid var(--global-ui-BorderColor);
min-height: 5.625rem;
&.selected {
background-color: var(--global-ui-highlight-BackgroundColor);
}
&:hover:not(.selected) {
background-color: var(--global-ui-highlight-BackgroundColor);
}
&__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: var(--spacing-0_5);
height: 2.5rem;
min-height: 2.5rem;
max-height: 2.5rem;
}
&__remove {
display: flex;
align-items: center;
margin-left: auto;
min-width: 1.5rem;
}
&__content {
display: flex;
width: 100%;
padding-left: 0.5rem;
}
&__notifications {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
margin-top: var(--spacing-1);
}
}
</style>
@@ -0,0 +1,75 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import cardPlugin from '@hcengineering/card'
import { Component, IconSize, Icon } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import communication from '@hcengineering/communication'
import { Class, Doc, Ref } from '@hcengineering/core'
import { ObjectIcon } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import NotifyMarker from './NotifyMarker.svelte'
export let doc: Doc | undefined
export let count: number = 0
const client = getClient()
const hierarchy = client.getHierarchy()
</script>
<div class="card-icon">
{#if doc}
{@const size = hierarchy.isDerived(doc._class, communication.type.Direct) ? 'large' : 'medium'}
{#if hierarchy.isDerived(doc._class, cardPlugin.class.Card)}
<Component is={cardPlugin.component.CardIcon} props={{ value: doc, size }} />
{:else}
<ObjectIcon value={doc} {size} showLoading={false} />
{/if}
{/if}
{#if count > 0}
<div class="card-icon__marker">
<NotifyMarker {count} size="small" />
</div>
{/if}
</div>
<style lang="scss">
.card-icon {
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
min-width: 2.5rem;
min-height: 2.5rem;
color: var(--global-secondary-TextColor);
background-color: var(--global-ui-BackgroundColor);
border: 1px solid var(--global-subtle-ui-BorderColor);
border-radius: var(--medium-BorderRadius);
fill: var(--global-secondary-TextColor);
&__marker {
position: absolute;
top: -0.375rem;
right: 0;
transform: translateX(calc(100% - 0.875rem));
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
@@ -0,0 +1,120 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Component, Label, tooltip } from '@hcengineering/ui'
import notification from '@hcengineering/notification'
import { getDocIdentifier, getDocTitle } from '@hcengineering/view-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Doc } from '@hcengineering/core'
import { NavigationItem } from '../type'
import cardPlugin, { Card } from '@hcengineering/card'
export let doc: Doc | undefined
export let navItem: NavigationItem
const client = getClient()
$: presenterMixin = client
.getHierarchy()
.classHierarchyMixin(navItem._class, notification.mixin.NotificationContextPresenter)
let idTitle: string | undefined
let title: string | undefined
$: navItem.type === 'legacy' &&
doc &&
getDocIdentifier(client, doc._id, doc._class, doc).then((res) => {
idTitle = res
})
$: navItem.type === 'legacy' &&
doc &&
getDocTitle(client, doc._id, doc._class, doc).then((res) => {
title = res
})
function asCard (doc: Doc): Card {
return doc as Card
}
</script>
<div class="labels">
{#if client.getHierarchy().isDerived(navItem._class, cardPlugin.class.Card)}
{@const label = client.getHierarchy().getClass(navItem._class).label}
<span class="title--bold overflow-label clear-mins" use:tooltip={{ label }}>
<Label {label} />
</span>
{#if doc}
<span class="title overflow-label clear-mins" use:tooltip={{ label: getEmbeddedLabel(asCard(doc).title) }}>
{asCard(doc).title}
</span>
{/if}
{:else}
{#if presenterMixin?.labelPresenter && navItem.type === 'legacy'}
{#if doc}
<Component
is={presenterMixin.labelPresenter}
props={{ context: navItem.context, object: doc }}
showLoading={false}
/>
{/if}
{:else if idTitle}
<span class="title--bold overflow-label clear-mins" use:tooltip={{ label: getEmbeddedLabel(idTitle) }}>
{idTitle}
</span>
{:else}
{@const label = client.getHierarchy().getClass(navItem._class).label}
<span class="title--bold overflow-label clear-mins" use:tooltip={{ label }}>
<Label {label} />
</span>
{/if}
{@const label = title != null ? getEmbeddedLabel(title) : client.getHierarchy().getClass(navItem._class).label}
<span class="title overflow-label clear-mins" use:tooltip={{ label }}>
<Label {label} />
</span>
{/if}
</div>
<style lang="scss">
.labels {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
min-width: 0;
color: var(--global-primary-TextColor);
font-size: 0.875rem;
overflow: hidden;
height: 2.5rem;
min-height: 2.5rem;
max-height: 2.5rem;
}
.title {
font-weight: 400;
color: var(--global-primary-TextColor);
overflow: hidden;
text-overflow: ellipsis;
}
.title--bold {
font-size: 0.875rem;
font-weight: 600;
color: var(--global-primary-TextColor);
overflow: hidden;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,139 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { SortingOrder } from '@hcengineering/core'
import {
IconDelete,
IconSettings,
Label,
ModernButton,
showPopup,
eventToHTMLElement,
IconMoreV,
ButtonMenu,
Loading
} from '@hcengineering/ui'
import { getCommunicationClient } from '@hcengineering/presentation'
import { Analytics } from '@hcengineering/analytics'
import view from '@hcengineering/view'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import inbox from '../plugin'
import InboxViewSettings from './InboxViewSettings.svelte'
const communicationClient = getCommunicationClient()
const oldNotificationClient = InboxNotificationsClientImpl.getClient()
let clearing = false
let reading = false
async function clearInbox (): Promise<void> {
if (clearing || reading) return
try {
clearing = true
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
await Promise.all([
...contexts.map((context) => communicationClient.removeNotificationContext(context.id)),
oldNotificationClient.removeAllNotifications()
])
clearing = false
} catch (e: any) {
clearing = false
Analytics.handleError(e)
}
}
async function readInbox (): Promise<void> {
if (reading || clearing) return
try {
reading = true
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
await Promise.all([
...contexts.map((context) => communicationClient.updateNotifications(context.id, {}, true)),
oldNotificationClient.readAllNotifications()
])
reading = false
} catch (e: any) {
reading = false
Analytics.handleError(e)
}
}
function click (e: MouseEvent): void {
showPopup(InboxViewSettings, {}, eventToHTMLElement(e))
}
async function onSelect (id?: 'clear' | 'read'): Promise<void> {
if (id == null) return
if (id === 'clear') {
void clearInbox()
}
if (id === 'read') {
void readInbox()
}
}
</script>
<div class="hulyNavPanel-header withButton small inbox-header">
<span class="overflow-label"><Label label={inbox.string.Inbox} /></span>
<div class="flex-row-center flex-gap-2">
{#if clearing}
<Loading size="small">
<span class="loading-label uppercase">
<Label label={inbox.string.Clearing} />
</span>
</Loading>
{:else if reading}
<Loading size="small">
<span class="loading-label uppercase">
<Label label={inbox.string.Reading} />
</span>
</Loading>
{/if}
<ModernButton icon={IconSettings} on:click={click} size="small" iconSize="small" />
<ButtonMenu
size="small"
noSelection
icon={IconMoreV}
items={[
{
id: 'read',
icon: view.icon.CheckCircle,
label: inbox.string.ReadAll
},
{
id: 'clear',
icon: IconDelete,
label: inbox.string.ClearAll
}
]}
on:selected={(ev) => onSelect(ev.detail)}
/>
</div>
</div>
<style lang="scss">
.inbox-header {
background-color: var(--theme-popup-color);
border-bottom: 1px solid var(--theme-navpanel-border);
}
.loading-label {
color: var(--global-secondary-TextColor);
font-weight: 500;
font-size: 0.625rem;
margin-left: 0.25rem;
}
</style>
@@ -0,0 +1,220 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { Label, ListView, Loading, Scroller } from '@hcengineering/ui'
import { Doc, Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import notification, { ActivityNotificationViewlet, DocNotifyContext } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import view from '@hcengineering/view'
import inbox from '../plugin'
import { NavigationItem } from '../type'
import { NavigationClient } from '../client'
import InboxCard from './InboxCard.svelte'
export let doc: Doc | undefined = undefined
export let legacyContext: DocNotifyContext | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
const navClient: NavigationClient = new NavigationClient(InboxNotificationsClientImpl.getClient())
const navigationItemsStore = navClient.navigationItemsStore
const isLoadingStore = navClient.isLoadingStore
let navItems: NavigationItem[] = []
$: navItems = $navigationItemsStore
let divScroll: HTMLElement | undefined | null = undefined
let contentDiv: HTMLElement | undefined | null = undefined
let list: ListView
let listSelection = 0
let viewlets: ActivityNotificationViewlet[] = []
void client.findAll(notification.class.ActivityNotificationViewlet, {}).then((res) => {
viewlets = res
})
let isPrevPageLoading = false
function handleScroll (): void {
if (divScroll != null && navClient.hasPrevPage() && !isPrevPageLoading) {
const isAtBottom = divScroll.scrollTop + divScroll.clientHeight >= divScroll.scrollHeight - 10
if (isAtBottom) {
isPrevPageLoading = true
void navClient.prev().then(() => {
isPrevPageLoading = false
})
}
}
}
const docById = new Map<Ref<Doc>, Doc>()
async function onKeydown (key: KeyboardEvent): Promise<void> {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection + 1)
}
if (key.code === 'Backspace') {
key.preventDefault()
key.stopPropagation()
const navItem = navItems[listSelection]
if (navItem != null) {
await navClient.remove(navItem)
}
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const navItem = navItems[listSelection]
const doc = docById.get(navItem?._id)
if (navItem != null && doc != null) {
dispatch('select', {
navItem,
doc
})
}
}
}
$: if (contentDiv != null) {
contentDiv.focus()
}
function getContextKey (index: number): string {
const item = navItems[index]
if (item == null) return index.toString()
return `${item._id}-${item.type}`
}
</script>
{#if navItems.length > 0}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Scroller bind:divScroll padding="0" shrink onScroll={handleScroll}>
<div class="inbox-list" bind:this={contentDiv} tabindex="0" on:keydown={onKeydown}>
<ListView
bind:this={list}
bind:selection={listSelection}
count={navItems.length}
items={navItems}
highlightIndex={legacyContext
? navItems.findIndex((it) => it._id === legacyContext.objectId)
: navItems.findIndex((it) => it._id === doc?._id)}
noScroll
kind="full-size"
colorsSchema="lumia"
lazy={true}
minHeight="6.125rem"
getKey={getContextKey}
>
<svelte:fragment slot="item" let:item={itemIndex}>
{@const navItem = navItems[itemIndex]}
{#if navItem}
<InboxCard
{navClient}
{navItem}
{viewlets}
selected={navItem._id === legacyContext?.objectId || navItem._id === doc?._id}
on:doc={(e) => {
const d = e.detail
if (d == null) return
docById.set(d._id, e.detail)
}}
on:select={(e) => {
dispatch('select', { navItem, doc: e.detail.doc, notification: e.detail.notification })
listSelection = itemIndex
}}
/>
{/if}
</svelte:fragment>
</ListView>
</div>
{#if isPrevPageLoading}
<div class="loader">
<Label label={view.string.Loading} />
</div>
{/if}
</Scroller>
{:else if $isLoadingStore}
<div class="placeholder">
<Loading />
</div>
{:else}
<div class="placeholder">
<div class="placeholder__header">
<Label label={inbox.string.InboxIsClear} />
</div>
<span class="mb-4">
<Label label={inbox.string.YouDontHaveAnyNewMessages} />
</span>
</div>
{/if}
<style lang="scss">
.inbox-list {
display: flex;
flex-direction: column;
width: 100%;
&:focus {
outline: 0;
}
:global(.list-item) {
border-radius: 0;
}
}
.placeholder {
display: flex;
align-self: center;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
height: inherit;
width: 100%;
gap: 0.5rem;
&__header {
font-weight: 600;
}
}
.loader {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--global-secondary-TextColor);
font-weight: 500;
height: 5rem;
min-height: 5rem;
}
</style>
@@ -1,23 +1,22 @@
<!--
// 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.
-->
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createFocusManager, FocusHandler, Label, ListView, ModernToggle, resizeObserver } from '@hcengineering/ui'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { updateViewSetting, isViewSettingEnabled, viewSettingsStore, hideUserNamesSettingId } from '../../settings'
import inbox from '../plugin'
import { updateViewSetting, isViewSettingEnabled, viewSettingsStore, hideUserNamesSettingId } from '../settings'
const dispatch = createEventDispatcher()
@@ -26,7 +25,7 @@
const items = [
{
id: hideUserNamesSettingId,
label: getEmbeddedLabel('Hide user names')
label: inbox.string.HideUserNames
}
]
@@ -23,4 +23,9 @@
export let card: Card
</script>
<NotificationPreview {card} message={notification.message} creator={notification.creator} date={notification.created} />
<NotificationPreview
{card}
message={notification.message}
socialId={notification.message?.creator ?? notification.creator}
date={notification.created}
/>
@@ -10,21 +10,23 @@
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { IntlString } from '@hcengineering/platform'
export let label: IntlString
<script lang="ts">
import { Card } from '@hcengineering/card'
import { NotificationContext } from '@hcengineering/communication-types'
import { createEventDispatcher } from 'svelte'
import InboxNotification from './InboxNotification.svelte'
import NotificationTemplate from './NotificationTemplate.svelte'
export let doc: Card
export let context: NotificationContext
const dispatch = createEventDispatcher()
</script>
<div class="hulyNavPanel-header withButton small navigation-header">
<span class="overflow-label"><Label {label} /></span>
<slot name="actions" />
</div>
<style lang="scss">
.navigation-header {
background-color: var(--theme-popup-color);
border-bottom: 1px solid var(--theme-navpanel-border);
}
</style>
{#each context.notifications ?? [] as notification (notification.id)}
<NotificationTemplate on:click={() => dispatch('click', notification)}>
<InboxNotification {notification} card={doc} />
</NotificationTemplate>
{/each}
@@ -0,0 +1,53 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
</script>
<div class="notification" on:click|stopPropagation|preventDefault>
<div class="marker" />
<slot />
</div>
<style lang="scss">
.notification {
position: relative;
display: flex;
flex-direction: column;
cursor: pointer;
user-select: none;
&:first-child .marker {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
&:last-child .marker {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
&:hover .marker {
border-radius: 0.5rem;
background: var(--global-primary-LinkColor);
}
}
.marker {
position: absolute;
width: 0.25rem;
height: 100%;
background: var(--global-ui-highlight-BackgroundColor);
border-radius: 0;
}
</style>
@@ -1,16 +1,15 @@
<!--
// Copyright © 2025 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.
-->
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { Message, Notification, ReactionNotificationContent, SocialID } from '@hcengineering/communication-types'
@@ -22,7 +21,7 @@
import NotificationPreview from './preview/NotificationPreview.svelte'
import PreviewTemplate from './preview/PreviewTemplate.svelte'
import chat from '../../plugin'
import inbox from '../plugin'
export let notification: Notification
export let card: Card
@@ -45,35 +44,30 @@
$: message = notification.message
</script>
{#if notification.message}
<div class="reaction-notification">
<PreviewTemplate
socialId={notification.creator}
date={notification.created}
color="secondary"
showSeparator={false}
>
<svelte:fragment slot="content">
<span class="ml-1-5" />
<Label label={chat.string.ReactedToYourMessage} />
</svelte:fragment>
</PreviewTemplate>
<div class="reaction-notification">
<PreviewTemplate socialId={notification.creator} date={notification.created} color="secondary" showSeparator={false}>
<svelte:fragment slot="content">
<Label label={inbox.string.ReactedToYourMessage} />
</svelte:fragment>
</PreviewTemplate>
<div class="reaction-notification__body">
<div class="reaction-notification__emoji">
<EmojiPresenter emoji={content.emoji} fitSize center />
</div>
<div class="reaction-notification__body">
<div class="reaction-notification__emoji">
<EmojiPresenter emoji={content.emoji} fitSize center />
</div>
{#if message}
<NotificationPreview
{card}
{message}
creator={notification.creator}
socialId={message?.creator ?? notification.creator}
date={notification.created}
kind="column"
padding="0"
hideHeader
/>
</div>
{/if}
</div>
{/if}
</div>
<style lang="scss">
.reaction-notification {
@@ -93,12 +87,13 @@
&__emoji {
display: flex;
align-items: center;
font-size: 1.5rem;
width: 2rem;
min-width: 2rem;
min-height: 2rem;
height: 2rem;
font-size: 1.25rem;
width: 1.325rem;
min-width: 1.325rem;
min-height: 1.325rem;
height: 1.325rem;
overflow: hidden;
margin-right: 0.25rem;
}
}
</style>
@@ -0,0 +1,114 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { matchQuery, Doc } from '@hcengineering/core'
import { ActivityNotificationViewlet, DisplayActivityInboxNotification } from '@hcengineering/notification'
import {
ActivityMessagePreview,
combineActivityMessages,
sortActivityMessages
} from '@hcengineering/activity-resources'
import activity, { ActivityMessage, DisplayActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Component } from '@hcengineering/ui'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Person } from '@hcengineering/contact'
import PreviewTemplate from '../preview/PreviewTemplate.svelte'
export let object: Doc
export let value: DisplayActivityInboxNotification
export let viewlets: ActivityNotificationViewlet[] = []
const client = getClient()
let viewlet: ActivityNotificationViewlet | undefined = undefined
let displayMessage: DisplayActivityMessage | undefined = undefined
$: void updateDisplayMessage(value.combinedMessages)
async function updateDisplayMessage (messages: ActivityMessage[]): Promise<void> {
const combinedMessages = combineActivityMessages(sortActivityMessages(messages))
displayMessage = combinedMessages[0]
}
$: updateViewlet(viewlets, displayMessage)
function matchViewlet (viewlet: ActivityNotificationViewlet, message: DisplayActivityMessage): boolean {
const hierarchy = client.getHierarchy()
const matched = matchQuery([message], viewlet.messageMatch, message._class, hierarchy, true)[0]
if (matched !== undefined) return true
if (hierarchy.isDerived(message._class, activity.class.DocUpdateMessage)) {
const dum = message as DocUpdateMessage
const dumUpdated: DocUpdateMessage = {
...dum,
objectClass: hierarchy.getParentClass(dum.objectClass)
}
const matched = matchQuery([dumUpdated], viewlet.messageMatch, message._class, hierarchy, true)[0]
return matched !== undefined
}
return false
}
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage): void {
if (viewlets.length === 0 || message === undefined) {
viewlet = undefined
return
}
for (const v of viewlets) {
const matched = matchViewlet(v, message)
if (matched) {
viewlet = v
return
}
}
viewlet = undefined
}
let person: Person | undefined = undefined
</script>
{#if displayMessage !== undefined}
<PreviewTemplate
kind="default"
color="primary"
bind:person
socialId={displayMessage.createdBy ?? displayMessage.modifiedBy}
date={new Date(displayMessage.createdOn ?? displayMessage.modifiedOn)}
fixHeight={true}
>
<svelte:fragment slot="content">
{#if viewlet}
<Component
is={viewlet.presenter}
showLoading={false}
props={{
message: displayMessage,
notification: value,
type: 'content-only'
}}
on:click
/>
{:else}
<ActivityMessagePreview value={displayMessage} doc={object} type="content-only" />
{/if}
</svelte:fragment>
</PreviewTemplate>
{/if}
@@ -0,0 +1,121 @@
<!--
// 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.
-->
<script lang="ts">
import core, { Doc, Markup } from '@hcengineering/core'
import { CommonInboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, IntlString, translateCB } from '@hcengineering/platform'
import { Icon, Label, themeStore, tooltip } from '@hcengineering/ui'
import { Person } from '@hcengineering/contact'
import { getClient, LiteMessageViewer } from '@hcengineering/presentation'
import { classIcon, DocNavLink } from '@hcengineering/view-resources'
import PreviewTemplate from '../preview/PreviewTemplate.svelte'
export let value: CommonInboxNotification
const client = getClient()
let content: Markup = ''
let person: Person | undefined = undefined
$: void updateContent(value.message, value.messageHtml)
async function updateContent (message?: IntlString, messageHtml?: Markup): Promise<void> {
if (messageHtml !== undefined) {
content = messageHtml
} else if (message !== undefined) {
translateCB(message, value.props, $themeStore.language, (res) => {
content = res
})
}
}
let headerObject: Doc | undefined = undefined
$: value.headerObjectId &&
value.headerObjectClass &&
client.findOne(value.headerObjectClass, { _id: value.headerObjectId }).then((doc) => {
headerObject = doc
})
let tooltipLabel: IntlString | undefined = undefined
$: if (headerObject !== undefined) {
tooltipLabel = value.header ?? client.getHierarchy().getClass(headerObject._class).label
} else if (person !== undefined) {
tooltipLabel = getEmbeddedLabel(person.name)
} else {
tooltipLabel = core.string.System
}
</script>
<PreviewTemplate
kind="default"
color="primary"
bind:person
socialId={value.createdBy ?? value.modifiedBy}
date={new Date(value.createdOn ?? value.modifiedOn)}
fixHeight={true}
>
<svelte:fragment slot="content">
<span class="row overflow-label">
{#if headerObject}
{@const icon = value.headerIcon ?? classIcon(client, headerObject._class)}
<div class="header">
{#if icon}
<span class="icon" use:tooltip={{ label: tooltipLabel }}>
<Icon {icon} size="small" />
</span>
{/if}
<DocNavLink object={headerObject} colorInherit>
<Label
label={value.header ?? client.getHierarchy().getClass(headerObject._class).label}
params={value.intlParams}
/>
:
</DocNavLink>
</div>
{/if}
<LiteMessageViewer message={content} colorInherit />
</span>
</svelte:fragment>
</PreviewTemplate>
<style lang="scss">
.row {
display: inline-flex;
align-items: flex-start;
gap: 0.25rem;
max-height: 1.125rem;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
max-width: 1rem;
min-width: 1rem;
}
.header {
display: flex;
align-items: center;
gap: var(--spacing-0_5);
font-weight: 500;
color: var(--global-primary-TextColor);
}
</style>
@@ -0,0 +1,63 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import notificationPlugin, {
ActivityNotificationViewlet,
CommonInboxNotification,
DisplayActivityInboxNotification,
DisplayInboxNotification,
MentionInboxNotification,
ReactionInboxNotification
} from '@hcengineering/notification'
import { Doc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import ActivityInboxNotificationPresenter from './ActivityInboxNotificationPresenter.svelte'
import MentionInboxNotificationPresenter from './MentionInboxNotificationPresenter.svelte'
import CommonInboxNotificationPresenter from './CommonInboxNotificationPresenter.svelte'
import ReactionInboxNotificationPresenter from './ReactionInboxNotificationPresenter.svelte'
export let notification: DisplayInboxNotification
export let doc: Doc
export let viewlets: ActivityNotificationViewlet[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
function asDisplayActivityNotification (notification: DisplayInboxNotification): DisplayActivityInboxNotification {
return notification as DisplayActivityInboxNotification
}
function asMentionNotification (notification: DisplayInboxNotification): MentionInboxNotification {
return notification as MentionInboxNotification
}
function asReactionNotification (notification: DisplayInboxNotification): ReactionInboxNotification {
return notification as ReactionInboxNotification
}
function asCommonNotification (notification: DisplayInboxNotification): CommonInboxNotification {
return notification as CommonInboxNotification
}
</script>
{#if hierarchy.isDerived(notification._class, notificationPlugin.class.ActivityInboxNotification)}
<ActivityInboxNotificationPresenter value={asDisplayActivityNotification(notification)} object={doc} {viewlets} />
{:else if hierarchy.isDerived(notification._class, notificationPlugin.class.MentionInboxNotification)}
<MentionInboxNotificationPresenter value={asMentionNotification(notification)} object={doc} />
{:else if hierarchy.isDerived(notification._class, notificationPlugin.class.ReactionInboxNotification)}
<ReactionInboxNotificationPresenter value={asReactionNotification(notification)} object={doc} />
{:else if hierarchy.isDerived(notification._class, notificationPlugin.class.CommonInboxNotification)}
<CommonInboxNotificationPresenter value={asCommonNotification(notification)} />
{/if}
@@ -0,0 +1,33 @@
<!-- Copyright © 2025 Hardcore Engineering Inc. -->
<!-- -->
<!-- Licensed under the Eclipse Public License, Version 2.0 (the "License"); -->
<!-- you may not use this file except in compliance with the License. You may -->
<!-- obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -->
<!-- -->
<!-- Unless required by applicable law or agreed to in writing, software -->
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
<!-- -->
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
import { createEventDispatcher } from 'svelte'
import NotificationTemplate from '../NotificationTemplate.svelte'
import LegacyNotification from './LegacyNotification.svelte'
export let doc: Doc
export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = []
const dispatch = createEventDispatcher()
</script>
{#each notifications as notification (notification._id)}
<NotificationTemplate on:click={() => dispatch('click', notification)}>
<LegacyNotification {notification} {doc} {viewlets} />
</NotificationTemplate>
{/each}
@@ -0,0 +1,61 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityMessagePreview } from '@hcengineering/activity-resources'
import { MentionInboxNotification } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Doc } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Person } from '@hcengineering/contact'
import CommonInboxNotificationPresenter from './CommonInboxNotificationPresenter.svelte'
import PreviewTemplate from '../preview/PreviewTemplate.svelte'
export let object: Doc
export let value: MentionInboxNotification
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
let message: ActivityMessage | undefined = undefined
let person: Person | undefined = undefined
$: if (hierarchy.isDerived(value.mentionedInClass, activity.class.ActivityMessage)) {
query.query(value.mentionedInClass, { _id: value.mentionedIn }, (res) => {
message = res[0] as ActivityMessage
})
} else {
query.unsubscribe()
}
</script>
{#if message}
<PreviewTemplate
kind="default"
color="primary"
bind:person
socialId={message.createdBy ?? message.modifiedBy}
date={new Date(message.createdOn ?? message.modifiedOn)}
fixHeight={true}
>
<svelte:fragment slot="content">
<ActivityMessagePreview value={message} doc={object} type="content-only" />
</svelte:fragment>
</PreviewTemplate>
{:else}
<CommonInboxNotificationPresenter {value} on:click />
{/if}
@@ -0,0 +1,101 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityMessagePreview } from '@hcengineering/activity-resources'
import { ReactionInboxNotification } from '@hcengineering/notification'
import { createQuery } from '@hcengineering/presentation'
import { ActivityMessage } from '@hcengineering/activity'
import { Doc } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Person } from '@hcengineering/contact'
import { Label } from '@hcengineering/ui'
import { EmojiPresenter } from '@hcengineering/emoji-resources'
import PreviewTemplate from '../preview/PreviewTemplate.svelte'
import inbox from '../../plugin'
export let object: Doc
export let value: ReactionInboxNotification
const query = createQuery()
let message: ActivityMessage | undefined = undefined
let person: Person | undefined = undefined
query.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
message = res[0]
})
$: socialId = value.createdBy ?? value.modifiedBy
$: date = new Date(value.createdOn ?? value.modifiedOn)
</script>
<div class="reaction-notification">
<PreviewTemplate {socialId} {date} color="secondary" showSeparator={false}>
<svelte:fragment slot="content">
<Label label={inbox.string.ReactedToYourMessage} />
</svelte:fragment>
</PreviewTemplate>
<div class="reaction-notification__body">
<div class="reaction-notification__emoji">
<EmojiPresenter emoji={value.emoji} fitSize center />
</div>
{#if message}
<PreviewTemplate
kind="column"
padding="0"
hideHeader
color="primary"
bind:person
socialId={message.createdBy ?? message.modifiedBy}
date={new Date(message.createdOn ?? message.modifiedOn)}
fixHeight={true}
>
<svelte:fragment slot="content">
<ActivityMessagePreview value={message} doc={object} type="content-only" />
</svelte:fragment>
</PreviewTemplate>
{/if}
</div>
</div>
<style lang="scss">
.reaction-notification {
display: flex;
flex-direction: column;
color: var(--global-secondary-TextColor);
white-space: nowrap;
&__body {
display: flex;
align-items: center;
gap: 0.25rem;
padding-right: var(--spacing-0_75);
padding-left: var(--spacing-1_25);
}
&__emoji {
display: flex;
align-items: center;
font-size: 1.25rem;
width: 1.325rem;
min-width: 1.325rem;
min-height: 1.325rem;
height: 1.325rem;
overflow: hidden;
margin-right: 0.25rem;
}
}
</style>
@@ -27,18 +27,17 @@
export let card: Card
export let message: Message | undefined = undefined
export let creator: SocialID
export let socialId: SocialID
export let date: Date
export let color: 'primary' | 'secondary' = 'primary'
export let kind: 'default' | 'column' = 'default'
export let padding: string | undefined = undefined
export let hideHeader: boolean = false
const tooltipLimit = 512
let person: WithLookup<Person> | undefined = undefined
$: void updatePerson(message?.creator ?? creator)
function getTooltipLabel (message: Message | undefined): IntlString {
if (message == null) return getEmbeddedLabel('')
const text = markupToText(jsonToMarkup(markdownToMarkup(message.content)))
@@ -47,21 +46,18 @@
}
return getEmbeddedLabel(text)
}
async function updatePerson (socialId: SocialID): Promise<void> {
person = $employeeByPersonIdStore.get(socialId) ?? (await getPersonByPersonId(socialId)) ?? undefined
}
</script>
<PreviewTemplate
{kind}
{color}
{person}
bind:person
{padding}
socialId={creator}
{socialId}
{date}
fixHeight={message == null || message.type !== MessageType.Activity}
tooltipLabel={getTooltipLabel(message)}
{hideHeader}
>
<svelte:fragment slot="content">
{#if message}
@@ -23,7 +23,7 @@
getPersonByPersonId
} from '@hcengineering/contact-resources'
import { SocialID } from '@hcengineering/communication-types'
import { isViewSettingEnabled, hideUserNamesSettingId, viewSettingsStore } from '../../../settings'
import { isViewSettingEnabled, hideUserNamesSettingId, viewSettingsStore } from '../../settings'
export let tooltipLabel: IntlString | undefined = undefined
export let person: Person | undefined = undefined
@@ -34,19 +34,14 @@
export let padding: string | undefined = undefined
export let fixHeight: boolean = false
export let showSeparator: boolean = true
export let hideHeader: boolean = false
let clientWidth: number
$: showPersonName = clientWidth > 300 && !isViewSettingEnabled($viewSettingsStore, hideUserNamesSettingId)
let _person: Person | undefined
$: void updatePerson(socialId, person)
async function updatePerson (socialId: SocialID, person: Person | undefined): Promise<void> {
_person = person ?? $employeeByPersonIdStore.get(socialId)
if (!_person) {
_person = (await getPersonByPersonId(socialId)) ?? undefined
}
$: updatePerson(socialId)
async function updatePerson (socialId: SocialID): Promise<void> {
person = $employeeByPersonIdStore.get(socialId) ?? (await getPersonByPersonId(socialId)) ?? undefined
}
</script>
@@ -58,15 +53,17 @@
style:padding
use:resizeObserver={(element) => (clientWidth = element.clientWidth)}
>
<span class="message-preview-template__column-header">
{#if _person}
<span class="overflow-label clear-mins">
<PersonPreviewProvider value={_person} inline>
{formatName(_person?.name ?? '')}
</PersonPreviewProvider>
</span>
{/if}
</span>
{#if !hideHeader}
<span class="message-preview-template__column-header">
{#if person}
<span class="overflow-label clear-mins">
<PersonPreviewProvider value={person} inline>
{formatName(person?.name ?? '')}
</PersonPreviewProvider>
</span>
{/if}
</span>
{/if}
<span class="message-preview-template__left">
{#if $$slots.content}
@@ -88,23 +85,21 @@
use:resizeObserver={(element) => (clientWidth = element.clientWidth)}
>
<span class="message-preview-template__left">
<PersonPreviewProvider value={_person} inline>
<span class="message-preview-template__person">
<PersonPreviewProvider value={person} inline>
<span class="message-preview-template__person" class:noGap={!showPersonName}>
<span class="message-preview-template__avatar">
{#if _person}
<Avatar size="card" person={_person} name={_person.name} />
{#if person}
<Avatar size="card" {person} name={person.name} />
{:else}
<SystemAvatar size="card" />
{/if}
</span>
{#if showPersonName && _person}
<span class="message-preview-template__name overflow-label {color}">{formatName(_person?.name ?? '')}</span>
{#if showPersonName && person}
<span class="message-preview-template__name overflow-label {color}">{formatName(person?.name ?? '')}</span>
{/if}
{#if showSeparator}
{#if showPersonName}
<span class="message-preview-template__separator"> • </span>
{:else}
<span class="mr-0-5" />
{/if}
{/if}
</span>
@@ -162,6 +157,10 @@
gap: 0.25rem;
color: var(--global-primary-TextColor);
font-size: 0.875rem;
&.noGap {
gap: 0;
}
}
&__avatar {

Some files were not shown because too many files have changed in this diff Show More