From 566b83ead7fa3a05024542933b78c42d596fb4a2 Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 22 Sep 2025 09:40:38 +0400 Subject: [PATCH] Communication v2 (#9867) Signed-off-by: Kristina Fefelova --- .vscode/launch.json | 19 - common/config/rush/pnpm-lock.yaml | 115 +- common/scripts/docker.sh | 1 - communication | 2 +- desktop/src/ui/platform.ts | 1 + desktop/src/ui/types.ts | 1 + dev/docker-compose.min.yaml | 1 - dev/docker-compose.yaml | 19 +- dev/prod/public/config.json | 1 + dev/prod/src/platform.ts | 4 +- dev/tool/package.json | 10 +- dev/tool/src/communication.ts | 1068 +++++++++++++++++ dev/tool/src/index.ts | 65 +- packages/presentation/package.json | 1 + packages/presentation/src/communication.ts | 95 +- packages/presentation/src/plugin.ts | 3 +- .../src/components/CardWidget.svelte | 2 +- .../src/components/CardWidgetTab.svelte | 2 +- .../src/components/EditCardNew.svelte | 2 +- .../src/components/FeedCardPresenter.svelte | 8 +- .../src/components/NewCardForm.svelte | 10 +- .../navigator-next/NavigatorCards.svelte | 2 +- .../NavigatorCardsSection.svelte | 2 +- .../src/components/inbox/InboxCard.svelte | 2 +- .../components/inbox/InboxNavigation.svelte | 4 +- .../inbox/MessageNotification.svelte | 5 +- .../inbox/ReactionNotification.svelte | 10 +- .../inbox/preview/NotificationPreview.svelte | 26 +- .../communication-resources/src/actions.ts | 30 +- .../src/components/AttachmentName.svelte | 2 +- .../CreateCardFromMessagePopup.svelte | 6 +- .../src/components/MessagesList.svelte | 49 +- .../src/components/ReactionPresenter.svelte | 6 +- .../src/components/ReactionsList.svelte | 17 +- .../src/components/ReactionsTooltip.svelte | 19 +- .../src/components/message/MessageBody.svelte | 36 +- .../message/MessageContentViewer.svelte | 4 - .../components/message/MessageFooter.svelte | 40 +- .../components/message/MessageInput.svelte | 21 +- .../message/MessagePresenter.svelte | 13 +- .../message/MessagesGroupPresenter.svelte | 2 - .../message/OneRowMessageBody.svelte | 24 +- .../src/components/thread/Thread.svelte | 9 +- .../thread/ThreadCollaborators.svelte | 37 +- plugins/communication-resources/src/draft.ts | 6 +- plugins/communication-resources/src/stores.ts | 12 +- plugins/communication-resources/src/types.ts | 4 +- plugins/communication-resources/src/utils.ts | 22 +- plugins/communication/src/poll.ts | 6 +- plugins/communication/src/types.ts | 13 +- pods/fulltext/package.json | 3 +- pods/fulltext/src/__tests__/indexing.spec.ts | 1 + pods/fulltext/src/index.ts | 3 + pods/fulltext/src/manager.ts | 3 + pods/fulltext/src/server.ts | 1 + pods/fulltext/src/workspace.ts | 3 + pods/media/src/handler.ts | 4 +- pods/server/src/server.ts | 7 +- rush.json | 10 - server/front/src/index.ts | 2 + server/front/src/starter.ts | 5 +- server/indexer/package.json | 2 +- server/indexer/src/indexer/indexer.ts | 156 ++- server/indexer/src/indexer/utils.ts | 4 +- server/server-pipeline/src/communication.ts | 34 +- services/mail/mail-common/src/message.ts | 4 +- services/mail/mail-common/src/txHandler.ts | 2 +- services/msg2file/.eslintrc.js | 7 - services/msg2file/.npmignore | 4 - services/msg2file/Dockerfile | 7 - services/msg2file/config/rig.json | 4 - services/msg2file/jest.config.js | 7 - services/msg2file/package.json | 79 -- services/msg2file/src/config.ts | 50 - services/msg2file/src/db.ts | 139 --- services/msg2file/src/index.ts | 21 - services/msg2file/src/main.ts | 56 - services/msg2file/src/parser.ts | 40 - services/msg2file/src/platform.ts | 48 - services/msg2file/src/server.ts | 118 -- services/msg2file/src/storage.ts | 50 - services/msg2file/src/worker.ts | 492 -------- services/msg2file/tsconfig.json | 12 - 83 files changed, 1551 insertions(+), 1686 deletions(-) create mode 100644 dev/tool/src/communication.ts delete mode 100644 services/msg2file/.eslintrc.js delete mode 100644 services/msg2file/.npmignore delete mode 100644 services/msg2file/Dockerfile delete mode 100644 services/msg2file/config/rig.json delete mode 100644 services/msg2file/jest.config.js delete mode 100644 services/msg2file/package.json delete mode 100644 services/msg2file/src/config.ts delete mode 100644 services/msg2file/src/db.ts delete mode 100644 services/msg2file/src/index.ts delete mode 100644 services/msg2file/src/main.ts delete mode 100644 services/msg2file/src/parser.ts delete mode 100644 services/msg2file/src/platform.ts delete mode 100644 services/msg2file/src/server.ts delete mode 100644 services/msg2file/src/storage.ts delete mode 100644 services/msg2file/src/worker.ts delete mode 100644 services/msg2file/tsconfig.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c13806bcbb..bd56c0509c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -972,25 +972,6 @@ "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "sourceMaps": true, "cwd": "${workspaceRoot}/services/export/pod-export" - }, - { - "name": "Msg2File", - "type": "node", - "request": "launch", - "args": ["src/index.ts"], - "env": { - "ACCOUNTS_URL": "http://localhost:3000", - "DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable", - "PORT": "9087", - "SECRET": "secret", - "SERVICE_ID": "msg2file-service", - "STORAGE_CONFIG": "datalake|http://huly.local:4030" - }, - "runtimeVersion": "20", - "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], - "sourceMaps": true, - "outputCapture": "std", - "cwd": "${workspaceRoot}/services/msg2file" } ] } diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 1e9b1f0b4b..883c4b6a9f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -304,9 +304,6 @@ importers: '@rush-temp/communication-types': specifier: file:./projects/communication-types.tgz version: file:projects/communication-types.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/communication-yaml': - specifier: file:./projects/communication-yaml.tgz - version: file:projects/communication-yaml.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/contact': specifier: file:./projects/contact.tgz version: file:projects/contact.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)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -928,9 +925,6 @@ importers: '@rush-temp/pod-media': specifier: file:./projects/pod-media.tgz version: file:projects/pod-media.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) - '@rush-temp/pod-msg2file': - specifier: file:./projects/pod-msg2file.tgz - version: file:projects/pod-msg2file.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) '@rush-temp/pod-notification': specifier: file:./projects/pod-notification.tgz version: file:projects/pod-notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) @@ -1732,9 +1726,6 @@ importers: '@types/node': specifier: ^22.15.29 version: 22.15.29 - '@types/node-cron': - specifier: ^3.0.11 - version: 3.0.11 '@types/nodemailer': specifier: ^6.4.17 version: 6.4.17 @@ -2155,9 +2146,6 @@ importers: msgpackr-extract: specifier: ^3.0.3 version: 3.0.3 - node-cron: - specifier: ^3.0.3 - version: 3.0.3 node-forge: specifier: ^1.3.1 version: 1.3.1 @@ -4580,7 +4568,7 @@ packages: version: 0.0.0 '@rush-temp/communication-client-query@file:projects/communication-client-query.tgz': - resolution: {integrity: sha512-6OA/sAql1ZLhhDX9J2yZOAEgfXqSxYh6yxpH/YGrC3PfYyhVAoe7piCjpB+JpKR5wL1kzpl1eP7Bj0IEq2jNJQ==, tarball: file:projects/communication-client-query.tgz} + resolution: {integrity: sha512-+68bXbpIU6XTujL5Dj3pFCdFelV7mE+Ln6zZEvf2NPHhAcJqy3+KjHQyIbytnWhCFsGyk+HE3S08ps6Kl6TKtw==, tarball: file:projects/communication-client-query.tgz} version: 0.0.0 '@rush-temp/communication-cockroach@file:projects/communication-cockroach.tgz': @@ -4588,7 +4576,7 @@ packages: version: 0.0.0 '@rush-temp/communication-query@file:projects/communication-query.tgz': - resolution: {integrity: sha512-sXcvSDoXmgZH233jFRePio23qQosvQZoACKqKgJ2Obr+oY4ZzJ2uX7DM0lyIKbhFUuLNFl56u2RfM5ra8SxRDg==, tarball: file:projects/communication-query.tgz} + resolution: {integrity: sha512-KMDSFpLNW1ekFoAf+BbRxPeRYnMUjYu+l80F+fgtvkKsX/6oKtb6xRhQyVh4EQiRlQBaMV//1b2d7cBwuF5HDw==, tarball: file:projects/communication-query.tgz} version: 0.0.0 '@rush-temp/communication-resources@file:projects/communication-resources.tgz': @@ -4604,21 +4592,17 @@ packages: version: 0.0.0 '@rush-temp/communication-server@file:projects/communication-server.tgz': - resolution: {integrity: sha512-KVc53phC1b9CNptvJmMb706t74gsCiEQGWvCpvTlRWMd3q+pj7i2994gJ0/KzuCojcHxPYVn2QxNojpNSV66Eg==, tarball: file:projects/communication-server.tgz} + resolution: {integrity: sha512-HfKzdbndaS3opK1inqcTOrVnKvU7PS2asLmKuR4JHooiTTvFFCHcXXYO++t1QmdGmankZRn3Qsq59CIt/UPgsA==, tarball: file:projects/communication-server.tgz} version: 0.0.0 '@rush-temp/communication-shared@file:projects/communication-shared.tgz': - resolution: {integrity: sha512-SustnNpr/eopCXmm0ZahvUZXZPcD7psSWM+CkWVwiPlLVPGWMVUURFOcnC94F3OhY+7qu9N0Rgr3gDvr4VKXWA==, tarball: file:projects/communication-shared.tgz} + resolution: {integrity: sha512-/ZUh5sviGQkQGJnOe6ImJHpKCcn7xNyyMeOcl4Tr3TqXjsH+uOQ0MAFI0Ul4f1LI+dGIrp1QsZWSVWvEVprRQQ==, tarball: file:projects/communication-shared.tgz} version: 0.0.0 '@rush-temp/communication-types@file:projects/communication-types.tgz': resolution: {integrity: sha512-uzZ82V1R+h+TmxBCrmKaAfUgoiX3Z8ciPGrkc42M++M5d5mlfIjTvPh0O+y50pHoFuQ5NQ3pNIjeDkrJE8Dp0w==, tarball: file:projects/communication-types.tgz} version: 0.0.0 - '@rush-temp/communication-yaml@file:projects/communication-yaml.tgz': - resolution: {integrity: sha512-D+fijwXQM2oMeT6QOmkRVLik5uyVCNsml07rz5RvKcPEpbqRh3YBtIvcUABoCStLvNTIzYuXnqbCHzSl/ibidw==, tarball: file:projects/communication-yaml.tgz} - version: 0.0.0 - '@rush-temp/communication@file:projects/communication.tgz': resolution: {integrity: sha512-kO/x+NBE4Nmtz5KhHO0kxty/0N1A2PH4dNVTdOviEV4mVAhFm7DkgWHu0SwWJvQrCzsB0xEga3/CGuGjcPyMjA==, tarball: file:projects/communication.tgz} version: 0.0.0 @@ -5424,7 +5408,7 @@ packages: version: 0.0.0 '@rush-temp/pod-fulltext@file:projects/pod-fulltext.tgz': - resolution: {integrity: sha512-0G3FKVxHWnyPuHWVBiR3Xtns3rLxGTC6ferQCX8NSC3dGOGOvk4TZgB8leohmJavwwxdZQKk1QFJqmXvHux10A==, tarball: file:projects/pod-fulltext.tgz} + resolution: {integrity: sha512-kPi4ermTxQtUKZ5xzbJgT07rLj2gG6m4YQgArWiONz47ooSoizi/Sg8Pu2FFyVV0klzQwqUOseuoVqgdySnFNg==, tarball: file:projects/pod-fulltext.tgz} version: 0.0.0 '@rush-temp/pod-github@file:projects/pod-github.tgz': @@ -5451,10 +5435,6 @@ packages: resolution: {integrity: sha512-5LRHcx/zPNVCJbIBfjigt+of64jfGrH5fjoDyvhASTEJbtG4RGRj8kKZoUI+1JHibDZzcVn2NLc+3nFMhksreA==, tarball: file:projects/pod-media.tgz} version: 0.0.0 - '@rush-temp/pod-msg2file@file:projects/pod-msg2file.tgz': - resolution: {integrity: sha512-7b2GrlsmevpahNIADhqNUBpshocrdjlc2L0kf2BAFVSEFpjzUtRju2XqwwoZ2l7xA0Z/b+Un9PHt+XYMWcNzlg==, tarball: file:projects/pod-msg2file.tgz} - version: 0.0.0 - '@rush-temp/pod-notification@file:projects/pod-notification.tgz': resolution: {integrity: sha512-1EeImD9ZNRxWq0jDp6o50hOpuoB0sNI54V/V1pm5VONdRF0S2lVzPCr35YehJ5BJcvbhy22au/7rk9XadoYI9w==, tarball: file:projects/pod-notification.tgz} version: 0.0.0 @@ -5524,7 +5504,7 @@ packages: version: 0.0.0 '@rush-temp/presentation@file:projects/presentation.tgz': - resolution: {integrity: sha512-nMn7FUSF2F6YuJAstYbvV8Yi9upkKqeSccsU3EOrKjJm1c57sa7x8i4K3Z6l4RoSQk86/F5CrzZVgqIMKCjF0w==, tarball: file:projects/presentation.tgz} + resolution: {integrity: sha512-yUvZa29zEHKk183hweT+OqJYrQtt4WwtYd6X4Y5WknLTG4fnd0kDEyFyOT9LgUUFF0/vW2iVYWLcMU93+W8PUg==, tarball: file:projects/presentation.tgz} version: 0.0.0 '@rush-temp/print-assets@file:projects/print-assets.tgz': @@ -5804,7 +5784,7 @@ packages: version: 0.0.0 '@rush-temp/server-indexer@file:projects/server-indexer.tgz': - resolution: {integrity: sha512-OYx2FWCD6jtTnsKmYnhhS3BsvhtCN9pIYkvIoSe0mzZi3xEAhnwu2T59+JpZgRO1fvD1yVxBy7nw0hyFtz03Hg==, tarball: file:projects/server-indexer.tgz} + resolution: {integrity: sha512-InjSApXF2jmClorioQIIV+JxbKtuYjl3a9i4eetEWuG2z7R+Ya7L6f98cqhEhYkdA4hM5TTZYbS+BcRLWlSqQg==, tarball: file:projects/server-indexer.tgz} version: 0.0.0 '@rush-temp/server-inventory-resources@file:projects/server-inventory-resources.tgz': @@ -6116,7 +6096,7 @@ packages: version: 0.0.0 '@rush-temp/tool@file:projects/tool.tgz': - resolution: {integrity: sha512-bsqWzFX5qr02kfG1zQZOgHsGWzR/Z66VC16ZoQnXs6mkdHFSmQ2p2l8eNL/aYPu2qfrRkMNAnI8cu6aULfKuDg==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-RQFbb0YGcuuAzNtGrbYDMyFE5VKDEsPA+bylES+hRUvKKBou2icVHwzDbFWrdSwGwfrr0ZhXVFQfTwSKOM18Bw==, tarball: file:projects/tool.tgz} version: 0.0.0 '@rush-temp/tracker-assets@file:projects/tracker-assets.tgz': @@ -7176,9 +7156,6 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node-cron@3.0.11': - resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} @@ -11808,10 +11785,6 @@ packages: node-api-version@0.2.0: resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} - node-cron@3.0.3: - resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} - engines: {node: '>=6.0.0'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -19286,29 +19259,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/communication-yaml@file:projects/communication-yaml.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@types/js-yaml': 4.0.9 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - esbuild: 0.25.9 - esbuild-plugin-copy: 2.1.1(esbuild@0.25.9) - 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.8.3))(eslint@8.56.0)(typescript@5.8.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.8.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.8.3)) - js-yaml: 4.1.0 - prettier: 3.2.5 - typescript: 5.8.3 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - node-notifier - - supports-color - - ts-node - '@rush-temp/communication@file:projects/communication.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)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@types/jest': 29.5.12 @@ -25435,47 +25385,6 @@ snapshots: - node-notifier - supports-color - '@rush-temp/pod-msg2file@file:projects/pod-msg2file.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))': - dependencies: - '@tsconfig/node16': 1.0.4 - '@types/cors': 2.8.17 - '@types/express': 4.17.21 - '@types/jest': 29.5.12 - '@types/js-yaml': 4.0.9 - '@types/node': 22.15.29 - '@types/node-cron': 3.0.11 - '@types/uuid': 8.3.4 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - cors: 2.8.5 - dotenv: 16.0.3 - esbuild: 0.25.9 - 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.8.3))(eslint@8.56.0)(typescript@5.8.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.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-node: 11.1.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - express: 4.21.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.8.3)) - js-yaml: 4.1.0 - node-cron: 3.0.3 - postgres: 3.4.7 - prettier: 3.2.5 - 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.8.3)))(typescript@5.8.3) - ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) - typescript: 5.8.3 - uuid: 8.3.2 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - '@swc/core' - - '@swc/wasm' - - babel-jest - - babel-plugin-macros - - node-notifier - - supports-color - '@rush-temp/pod-notification@file:projects/pod-notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))': dependencies: '@tsconfig/node16': 1.0.4 @@ -30348,6 +30257,7 @@ snapshots: '@elastic/elasticsearch': 7.17.14 '@faker-js/faker': 8.4.1 '@types/jest': 29.5.12 + '@types/js-yaml': 4.0.9 '@types/mime-types': 2.1.4 '@types/minio': 7.0.18 '@types/node': 22.15.29 @@ -30368,6 +30278,7 @@ snapshots: eslint-plugin-promise: 6.1.1(eslint@8.56.0) 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.8.3)) + js-yaml: 4.1.0 libphonenumber-js: 1.10.56 mime-types: 2.1.35 mongodb: 6.16.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) @@ -32146,8 +32057,6 @@ snapshots: dependencies: '@types/node': 22.15.29 - '@types/node-cron@3.0.11': {} - '@types/node-fetch@2.6.12': dependencies: '@types/node': 22.15.29 @@ -37734,10 +37643,6 @@ snapshots: dependencies: semver: 7.7.2 - node-cron@3.0.3: - dependencies: - uuid: 8.3.2 - node-domexception@1.0.0: {} node-fetch@2.7.0(encoding@0.1.13): diff --git a/common/scripts/docker.sh b/common/scripts/docker.sh index fc5085a4a7..01a4553752 100755 --- a/common/scripts/docker.sh +++ b/common/scripts/docker.sh @@ -19,7 +19,6 @@ rush docker:build -p 20 \ --to @hcengineering/pod-datalake \ --to @hcengineering/pod-mail-worker \ --to @hcengineering/pod-export \ ---to @hcengineering/pod-msg2file \ --to @hcengineering/pod-media \ --to @hcengineering/pod-preview \ --to @hcengineering/pod-external \ diff --git a/communication b/communication index eb30332c8b..b0f5d9481a 160000 --- a/communication +++ b/communication @@ -1 +1 @@ -Subproject commit eb30332c8ba9bb0820c0b3ad8aa353c393aa07c9 +Subproject commit b0f5d9481a126132a9e97692e10a2570a365df4a diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 36570461ff..26d5cce9ec 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -298,6 +298,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise l.trim()) diff --git a/dev/tool/package.json b/dev/tool/package.json index a41b6a7e8d..fc28c68afe 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -21,7 +21,7 @@ "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging", "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool", "run-local-mongo": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js", - "run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js", + "run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' HULYLAKE_URL=http://huly.local:8096 ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js", "run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js", "run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js", "upgrade-mongo": "rushx run-local-mongo upgrade-workspace -- $1", @@ -55,7 +55,8 @@ "@types/request": "~2.48.8", "jest": "^29.7.0", "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" + "@types/jest": "^29.5.5", + "@types/js-yaml": "^4.0.9" }, "dependencies": { "@elastic/elasticsearch": "^7.17.14", @@ -180,6 +181,9 @@ "msgpackr-extract": "^3.0.3", "@hcengineering/kafka": "^0.6.0", "@hcengineering/api-client": "^0.6.0", - "@faker-js/faker": "^8.4.1" + "@faker-js/faker": "^8.4.1", + "@hcengineering/hulylake-client": "^0.6.0", + "js-yaml": "^4.1.0", + "@hcengineering/communication-types": "^0.1.0" } } diff --git a/dev/tool/src/communication.ts b/dev/tool/src/communication.ts new file mode 100644 index 0000000000..b825bd4f8f --- /dev/null +++ b/dev/tool/src/communication.ts @@ -0,0 +1,1068 @@ +// 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 Workspace } from '@hcengineering/account' +import { type HulylakeClient, type JsonPatch } from '@hcengineering/hulylake-client' +import type postgres from 'postgres' +import { type StorageAdapter } from '@hcengineering/server-core' +import { + generateUuid, + groupByArray, + type MeasureContext, + notEmpty, + type PersonId, + type PersonUuid, + type WorkspaceUuid +} from '@hcengineering/core' +import { + type Attachment, + type AttachmentData, + type AttachmentDoc, + type AttachmentID, + type AttachmentUpdateData, + type BlobID, + type CardID, + type CardType, + type Emoji, + linkPreviewType, + type Markdown, + type MessageDoc, + type MessageExtra, + type MessageID, + type MessagesDoc, + type MessagesGroup, + type MessagesGroupDoc, + type MessagesGroupsDoc, + MessageType, + type SocialID, + type ThreadDoc +} from '@hcengineering/communication-types' +import yaml from 'js-yaml' +import { type AccountClient } from '@hcengineering/account-client' + +const MESSAGES_GROUP_TABLE = 'communication.messages_group' +const MESSAGES_TABLE = 'communication.message' +const PATCH_TABLE = 'communication.patch' +const ATTACHMENT_TABLE = 'communication.attachment' +const REACTION_TABLE = 'communication.reaction' +const THREAD_INDEX_TABLE = 'communication.thread_index' + +export async function migrateWorkspaceMessages ( + ctx: MeasureContext, + ws: Workspace, + db: postgres.Sql, + storage: StorageAdapter, + hulylake: HulylakeClient, + accountClient: AccountClient, + personUuidBySocialId: Map +): Promise { + const initializedCards = new Set() + + await migrateMessagesGroups(ctx, ws, db, storage, hulylake, accountClient, personUuidBySocialId, initializedCards) + await migrateMessages(ctx, ws, db, hulylake, accountClient, personUuidBySocialId, initializedCards) +} + +async function migrateMessages ( + ctx: MeasureContext, + ws: Workspace, + db: postgres.Sql, + hulylake: HulylakeClient, + accountClient: AccountClient, + personUuidBySocialId: Map, + initializedCards: Set +): Promise { + const cards = await getMessagesCards(ws.uuid, db) + + for (const card of cards) { + try { + if (!initializedCards.has(card)) { + await initializeCardGroups(ctx, ws, hulylake, card) + initializedCards.add(card) + } + + ctx.info(`Migrating messages for card ${card}`) + const cursor = getMessagesCursor(ws.uuid, card, db) + + for await (const messages of cursor) { + if (messages.length === 0) continue + const oldMessages = messages.map(deserializeOldMessage).filter(notEmpty) + await migrateMessagesBatch(card, hulylake, accountClient, personUuidBySocialId, oldMessages) + } + } catch (e) { + ctx.error('Failed to migrate messages for card', card) + ctx.error('Error', { error: e }) + } + } +} + +async function migrateMessagesBatch ( + cardId: CardID, + hulylake: HulylakeClient, + accountClient: AccountClient, + personUuidBySocialId: Map, + messages: OldMessage[] +): Promise { + const fromDate = messages[0].created + const toDate = messages[messages.length - 1].created + const blobId = generateUuid() as BlobID + const newGroupDoc: MessagesGroupDoc = { + cardId, + blobId, + fromDate: fromDate.toISOString(), + toDate: toDate.toISOString(), + count: messages.length + } + const newMessagesDoc: MessagesDoc = { + cardId, + fromDate: fromDate.toISOString(), + toDate: toDate.toISOString(), + messages: {}, + language: 'original' + } + + for (const oldMessage of messages) { + const newMessageDoc = await oldMessageToNewMessageDoc(oldMessage, accountClient, personUuidBySocialId) + newMessagesDoc.messages[newMessageDoc.id] = newMessageDoc + } + + const jsonPatches: JsonPatch[] = [ + { + hop: 'add', + path: `/${blobId}`, + value: newGroupDoc, + safe: true + } as const + ] + + await hulylake.patchJson(`${cardId}/messages/groups`, jsonPatches, undefined, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + }) + await hulylake.putJson(`${cardId}/messages/${blobId}`, newMessagesDoc, undefined, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + }) +} + +async function migrateMessagesGroups ( + ctx: MeasureContext, + ws: Workspace, + db: postgres.Sql, + storage: StorageAdapter, + hulylake: HulylakeClient, + accountClient: AccountClient, + personUuidBySocialId: Map, + initializedCards: Set +): Promise { + const cursor = getMessagesGroupsCursor(ws.uuid, db) + + for await (const groups of cursor) { + for (const _g of groups) { + const group: MessagesGroup = deserializeMessagesGroup(_g) + try { + if (!initializedCards.has(group.cardId)) { + await initializeCardGroups(ctx, ws, hulylake, group.cardId) + initializedCards.add(group.cardId) + } + + await migrateMessagesGroup(ctx, ws, db, group, storage, hulylake, accountClient, personUuidBySocialId) + } catch (e) { + ctx.error('Failed to migrate messages group', group) + ctx.error('error', { error: e }) + } + } + } +} + +async function initializeCardGroups ( + ctx: MeasureContext, + ws: Workspace, + hulylake: HulylakeClient, + cardId: CardID +): Promise { + const res = await hulylake.getJson(`${cardId}/messages/groups`, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 500 + } + }) + if (res.body != null) return + + await hulylake.putJson(`${cardId}/messages/groups`, {}, undefined, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + }) +} + +async function migrateMessagesGroup ( + ctx: MeasureContext, + ws: Workspace, + db: postgres.Sql, + group: MessagesGroup, + storage: StorageAdapter, + hulylake: HulylakeClient, + accountClient: AccountClient, + personUuidBySocialId: Map +): Promise { + const patches = await findMessageGroupPatches(ws.uuid, db, group) + const parsedBlob = await loadMessagesGroupBlob(ctx, ws, group, storage, patches) + if (parsedBlob == null) return + + const oldMessages = parsedBlob.messages + const newGroupDoc: MessagesGroupDoc = { + cardId: group.cardId, + blobId: group.blobId, + fromDate: group.fromDate.toISOString(), + toDate: group.toDate.toISOString(), + count: group.count + } + const newMessagesDoc: MessagesDoc = { + cardId: group.cardId, + fromDate: group.fromDate.toISOString(), + toDate: group.toDate.toISOString(), + messages: {}, + language: 'original' + } + + for (const oldMessage of oldMessages) { + newMessagesDoc.messages[oldMessage.id] = await oldMessageToNewMessageDoc( + oldMessage, + accountClient, + personUuidBySocialId + ) + } + + const jsonPatches: JsonPatch[] = [ + { + hop: 'add', + path: `/${group.blobId}`, + value: newGroupDoc, + safe: true + } as const + ] + + await hulylake.patchJson(`${group.cardId}/messages/groups`, jsonPatches, undefined, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + }) + await hulylake.putJson(`${group.cardId}/messages/${group.blobId}`, newMessagesDoc, undefined, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + }) +} + +async function oldMessageToNewMessageDoc ( + oldMessage: OldMessage, + accountClient: AccountClient, + personUuidBySocialId: Map +): Promise { + const reactions: Record> = {} + const attachments: Record = {} + const threads: Record = {} + + const reactionsByEmoji = groupByArray(oldMessage.reactions, (it) => it.reaction) + for (const [_emoji, oldReactions] of reactionsByEmoji.entries()) { + const emoji = _emoji as Emoji + if (reactions[emoji] == null) { + reactions[emoji] = {} + } + for (const oldReaction of oldReactions) { + const personUuid = await getPersonUuidBySocialId(accountClient, personUuidBySocialId, oldReaction.creator) + + if (personUuid == null) continue + if (reactions[emoji][personUuid] == null) { + reactions[emoji][personUuid] = { + count: 1, + date: oldReaction.created.toISOString() + } + } + } + } + + for (const oldAttachment of oldMessage.attachments) { + attachments[oldAttachment.id] = { + id: oldAttachment.id, + mimeType: + oldAttachment.mimeType ?? + (oldAttachment as any)?.params?.mimeType ?? + (oldAttachment as any).type ?? + (oldAttachment as any)?.params?.type ?? + '', + params: oldAttachment.params, + creator: oldAttachment.creator, + created: oldAttachment.created.toISOString(), + modified: oldAttachment.modified?.toISOString() ?? null + } + } + + if (oldMessage.thread != null) { + threads[oldMessage.thread.threadId] = { + threadId: oldMessage.thread.threadId, + threadType: oldMessage.thread.threadType, + repliesCount: oldMessage.thread.repliesCount, + lastReplyDate: oldMessage.thread.lastReply?.toISOString() ?? null, + repliedPersons: {} + } + } + return { + id: oldMessage.id, + cardId: oldMessage.cardId, + type: oldMessage.type === 'activity' ? MessageType.Activity : MessageType.Text, + content: oldMessage.content, + extra: oldMessage.extra ?? {}, + creator: oldMessage.creator, + created: oldMessage.created.toISOString(), + modified: oldMessage.edited?.toISOString() ?? null, + reactions, + attachments, + threads + } +} +async function getPersonUuidBySocialId ( + ccountClient: AccountClient, + personUuidBySocialId: Map, + socialId: PersonId +): Promise { + if (personUuidBySocialId.has(socialId)) { + return personUuidBySocialId.get(socialId) + } + const personUuid = await ccountClient.findPersonBySocialId(socialId) + if (personUuid != null) { + personUuidBySocialId.set(socialId, personUuid) + } + + return personUuid +} + +async function loadMessagesGroupBlob ( + ctx: MeasureContext, + ws: Workspace, + group: MessagesGroup, + storage: StorageAdapter, + patches: Patch[] +): Promise { + try { + const stream = await storage.get(ctx, ws, group.blobId) + const response = new Response(stream as any) + const text = await response.text() + const parsedFile = parseYaml(text) + + const patchesByMessageId = groupByArray(patches, (it) => it.messageId) + + return { + ...parsedFile, + messages: parsedFile.messages + .map((message) => { + const patches = patchesByMessageId.get(message.id) ?? [] + return patches.length > 0 ? applyPatches(message, patches) : message + }) + .filter(notEmpty) + } + } catch (e) { + ctx.error('Error:', { error: e }) + } +} + +async function getMessagesCards (ws: WorkspaceUuid, db: postgres.Sql): Promise { + const table = db(MESSAGES_TABLE) + const res = await db` + SELECT DISTINCT card_id + FROM ${table} + WHERE workspace_id = ${ws} + ` + + return res.map((r) => r.card_id) +} + +function getMessagesGroupsCursor (ws: WorkspaceUuid, db: postgres.Sql): AsyncIterable { + const table = db(MESSAGES_GROUP_TABLE) + return db`SELECT * FROM ${table} WHERE workspace_id = ${ws} ORDER BY from_date ASC`.cursor(500) +} + +function getMessagesCursor (ws: WorkspaceUuid, cardId: CardID, db: postgres.Sql): AsyncIterable { + const messagesTable = db(MESSAGES_TABLE) + const threadIndexTable = db(THREAD_INDEX_TABLE) + const attachmentTable = db(ATTACHMENT_TABLE) + const reactionTable = db(REACTION_TABLE) + const patchTable = db(PATCH_TABLE) + return db`WITH + limited_messages AS ( + SELECT * + FROM ${messagesTable} m + WHERE m.workspace_id = ${ws} + AND m.card_id = ${cardId} + ORDER BY m.created ASC + ), + + agg_attachments AS ( + SELECT + a.workspace_id, + a.card_id, + a.message_id, + jsonb_agg(jsonb_build_object( + 'id', a.id, + 'type', a.type, + 'params', a.params, + 'creator', a.creator, + 'created', a.created, + 'modified', a.modified + )) AS attachments + FROM ${attachmentTable} a + INNER JOIN limited_messages m + ON m.workspace_id = a.workspace_id + AND m.card_id = a.card_id + AND m.id = a.message_id + GROUP BY a.workspace_id, a.card_id, a.message_id + ), + + agg_reactions AS ( + SELECT + r.workspace_id, + r.card_id, + r.message_id, + jsonb_agg(jsonb_build_object( + 'reaction', r.reaction, + 'creator', r.creator, + 'created', r.created + )) AS reactions + FROM ${reactionTable} r + INNER JOIN limited_messages m + ON m.workspace_id = r.workspace_id + AND m.card_id = r.card_id + AND m.id = r.message_id + GROUP BY r.workspace_id, r.card_id, r.message_id + ), + + agg_patches AS ( + SELECT + p.workspace_id, + p.card_id, + p.message_id, + jsonb_agg( + jsonb_build_object( + 'type', p.type, + 'data', p.data, + 'creator', p.creator, + 'created', p.created + ) ORDER BY p.created ASC + ) AS patches + FROM ${patchTable} p + INNER JOIN limited_messages m + ON m.workspace_id = p.workspace_id + AND m.card_id = p.card_id + AND m.id = p.message_id + GROUP BY p.workspace_id, p.card_id, p.message_id + ) + + SELECT m.id::text, + m.card_id, + m.type, + m.content, + m.creator, + m.created, + m.data, + t.thread_id AS thread_id, + t.thread_type AS thread_type, + t.replies_count::int AS replies_count, + t.last_reply AS last_reply, + COALESCE(a.attachments, '[]'::jsonb) AS attachments, + COALESCE(r.reactions, '[]'::jsonb) AS reactions, + COALESCE(p.patches, '[]'::jsonb) AS patches + FROM limited_messages m + LEFT JOIN ${threadIndexTable} t + ON t.workspace_id = m.workspace_id + AND t.card_id = m.card_id + AND t.message_id = m.id + LEFT JOIN agg_attachments a + ON a.workspace_id = m.workspace_id + AND a.card_id = m.card_id + AND a.message_id = m.id + LEFT JOIN agg_reactions r + ON r.workspace_id = m.workspace_id + AND r.card_id = m.card_id + AND r.message_id = m.id + LEFT JOIN agg_patches p + ON p.workspace_id = m.workspace_id + AND p.card_id = m.card_id + AND p.message_id = m.id + ORDER BY m.created ASC;`.cursor(500) +} + +async function findMessageGroupPatches (ws: WorkspaceUuid, db: postgres.Sql, group: MessagesGroup): Promise { + const table = db(PATCH_TABLE) + const res = + await db`SELECT * FROM ${table} WHERE workspace_id = ${ws} AND message_created BETWEEN ${group.fromDate} AND ${group.toDate}` + + return res.map((r) => deserializePatch(r)) +} + +function deserializeMessagesGroup (group: any): MessagesGroup { + return { + cardId: group.card_id, + blobId: group.blob_id, + fromDate: new Date(group.from_date), + toDate: new Date(group.to_date), + count: Number(group.count) + } +} + +function deserializePatch (raw: any): Patch { + return { + type: raw.type, + messageId: String(raw.message_id) as MessageID, + data: raw.data, + creator: raw.creator, + created: new Date(raw.created) + } +} + +function deserializeOldMessage (raw: any): OldMessage | undefined { + const patches: Patch[] = (raw.patches ?? []).map((it: any) => deserializePatch(it)) + const rawMessage: OldMessage = { + id: String(raw.id) as MessageID, + type: raw.type, + cardId: raw.card_id, + content: raw.content, + creator: raw.creator, + created: new Date(raw.created), + removed: false, + extra: raw.data, + thread: + raw.thread_id != null && raw.thread_type != null + ? { + cardId: raw.card_id, + messageId: String(raw.id) as MessageID, + threadId: raw.thread_id, + threadType: raw.thread_type, + repliesCount: raw.replies_count != null ? Number(raw.replies_count) : 0, + lastReply: raw.last_reply ?? new Date() + } + : undefined, + reactions: (raw.reactions ?? []).map(deserializeReaction), + attachments: (raw.attachments ?? []).map(deserializeAttachment) + } + + if (patches.length === 0) { + return rawMessage + } + + return applyPatches( + rawMessage, + patches.filter((it) => it.type === PatchType.update || it.type === PatchType.remove) + ) +} + +export function deserializeReaction (raw: any): OldReaction { + return { + reaction: raw.reaction, + creator: raw.creator, + created: new Date(raw.created) + } +} + +export function deserializeAttachment (raw: any): Attachment { + return { + id: String(raw.id) as AttachmentID, + type: raw.type, + params: raw.params, + creator: raw.creator, + created: new Date(raw.created), + modified: raw.modified != null ? new Date(raw.modified) : undefined + } as any as Attachment +} + +function parseYaml (data: string): ParsedFile { + const [metadata, messages] = yaml.loadAll(data) as [FileMetadata, FileMessage[]] + + return { + ...metadata, + messages: messages.map((message) => ({ + id: message.id, + type: message.type, + cardId: metadata.cardId ?? (metadata as any).card, + content: message.content, + edited: message.edited, + creator: message.creator, + created: message.created, + removed: message.removed, + extra: message.extra ?? (message as any).data, + thread: + message.thread != null + ? { + cardId: metadata.cardId, + messageId: message.id, + threadId: message.thread.threadId ?? (message.thread as any).thread, + threadType: message.thread.threadType, + repliesCount: message.thread.repliesCount, + lastReply: message.thread.lastReply + } + : undefined, + attachments: parseAttachments(message), + reactions: message.reactions ?? [] + })) + } +} + +function parseAttachments (message: FileMessage): Attachment[] { + if (message.attachments != null) { + return message.attachments + } + + const oldMessage = message as any + + const attachments: Attachment[] = [] + + if ('files' in oldMessage && Array.isArray(oldMessage.files)) { + attachments.push( + ...oldMessage.files.map((it: any) => ({ + id: it.blobId, + type: it.type, + params: { + blobId: it.blobId, + mimeType: it.type, + fileName: it.filename, + size: it.size, + metadata: it.meta + }, + creator: it.creator, + created: new Date(it.created) + })) + ) + } else if ('blobs' in oldMessage && Array.isArray(oldMessage.blobs)) { + attachments.push( + ...oldMessage.blobs.map((it: any) => ({ + id: it.blobId, + type: it.mimeType ?? it.contentType, + params: { + blobId: it.blobId, + mimeType: it.mimeType ?? it.contentType, + fileName: it.fileName, + size: it.size, + metadata: it.metadata + }, + creator: it.creator, + created: new Date(it.created) + })) + ) + } + + if ('linkPreviews' in oldMessage && Array.isArray(oldMessage.linkPreviews)) { + attachments.push( + ...oldMessage.linkPreviews.map((it: any) => ({ + id: it.previewId, + type: linkPreviewType, + params: it, + creator: it.creator, + created: new Date(it.created) + })) + ) + } + + return attachments +} + +export function applyPatches (message: OldMessage, patches: Patch[]): OldMessage | undefined { + if (patches.length === 0) return message + + let result: OldMessage | undefined = message + for (const p of patches) { + result = applyPatch(message, p) + } + return result +} + +export function applyPatch (message: OldMessage, patch: Patch): OldMessage | undefined { + if (message.removed) { + return undefined + } + + switch (patch.type) { + case PatchType.update: { + if (patch.created.getTime() < (message.edited?.getTime() ?? 0)) { + return message + } + return { + ...message, + edited: patch.created, + content: patch.data.content ?? message.content, + extra: patch.data.extra ?? message.extra + } + } + case PatchType.remove: { + return undefined + } + case PatchType.reaction: + return patchReactions(message, patch) + case PatchType.attachment: + return patchAttachments(message, patch) + case PatchType.thread: + return patchThread(message, patch) + } +} + +function patchAttachments (message: OldMessage, patch: AttachmentPatch): OldMessage { + if (patch.data.operation === 'add') { + return addAttachments(message, patch.data.attachments, patch.created, patch.creator) + } else if (patch.data.operation === 'remove') { + return removeAttachments(message, patch.data.ids) + } else if (patch.data.operation === 'set') { + return setAttachments(message, patch.data.attachments, patch.created, patch.creator) + } else if (patch.data.operation === 'update') { + return updateAttachments(message, patch.data.attachments, patch.created) + } + return message +} + +function patchReactions (message: OldMessage, patch: ReactionPatch): OldMessage { + if (patch.data.operation === 'add') { + return setReaction(message, patch.data.reaction, patch.creator, patch.created) + } else if (patch.data.operation === 'remove') { + return removeReaction(message, patch.data.reaction, patch.creator) + } + return message +} + +function setReaction (message: OldMessage, reaction: string, creator: SocialID, created: Date): OldMessage { + const isExist = message.reactions.some((it) => it.reaction === reaction && it.creator === creator) + if (isExist) return message + message.reactions.push({ + reaction, + creator, + created + }) + return message +} + +function removeReaction (message: OldMessage, reaction: string, creator: SocialID): OldMessage { + const reactions = message.reactions.filter((it) => it.reaction !== reaction || it.creator !== creator) + if (reactions.length === message.reactions.length) return message + + return { + ...message, + reactions + } +} + +function addAttachments (message: OldMessage, data: AttachmentData[], created: Date, creator: SocialID): OldMessage { + const newAttachments: Attachment[] = [] + for (const attach of data) { + const isExists = message.attachments.some((it) => it.id === attach.id) + if (isExists === undefined) continue + const attachment: Attachment = { + ...attach, + created, + creator + } as any + newAttachments.push(attachment) + } + + if (newAttachments.length === 0) return message + return { + ...message, + attachments: [...message.attachments, ...newAttachments] + } +} + +function updateAttachments (message: OldMessage, updates: AttachmentUpdateData[], date: Date): OldMessage { + if (updates.length === 0) return message + const updatedAttachments: Attachment[] = [] + for (const attachment of message.attachments) { + const update = updates.find((it) => it.id === attachment.id) + if (update === undefined) { + updatedAttachments.push(attachment) + } else { + updatedAttachments.push({ + ...attachment, + params: { + ...attachment.params, + ...update.params + }, + modified: date.getTime() > (attachment.modified?.getTime() ?? 0) ? date : attachment.modified + } as any) + } + } + + return { + ...message, + attachments: updatedAttachments + } +} + +function removeAttachments (message: OldMessage, ids: AttachmentID[]): OldMessage { + const attachments = message.attachments.filter((it) => !ids.includes(it.id)) + if (attachments.length === message.attachments.length) return message + + return { + ...message, + attachments + } +} + +function setAttachments (message: OldMessage, data: AttachmentData[], created: Date, creator: SocialID): OldMessage { + if (data.length === 0) return message + return { + ...message, + attachments: data.map( + (it) => + ({ + ...it, + created, + creator + }) as any + ) + } +} + +function patchThread (message: OldMessage, patch: ThreadPatch): OldMessage { + if (patch.data.operation === 'attach') { + return attachThread(message, patch.data.threadId, patch.data.threadType) + } else if (patch.data.operation === 'update') { + return updateThread( + message, + patch.data.threadId, + patch.data.threadType, + patch.data.repliesCountOp, + patch.data.lastReply + ) + } + return message +} + +function attachThread (message: OldMessage, threadId: CardID, threadType: CardType): OldMessage { + if (message.thread !== undefined) return message + return { + ...message, + thread: { + cardId: message.cardId, + messageId: message.id, + threadId, + threadType, + repliesCount: 0, + lastReply: new Date() + } + } +} + +function updateThread ( + message: OldMessage, + threadId: CardID, + threadType?: CardType, + repliesCountOp?: 'increment' | 'decrement', + lastReply?: Date +): OldMessage { + if (repliesCountOp === undefined && lastReply === undefined) return message + if (message.thread === undefined) return message + if (message.thread.threadId !== threadId) return message + + let count = message.thread.repliesCount + if (repliesCountOp === 'increment') { + count = count + 1 + } + + if (repliesCountOp === 'decrement') { + count = Math.max(count - 1, 0) + } + + return { + ...message, + thread: { + ...message.thread, + repliesCount: count, + threadType: threadType ?? message.thread.threadType, + lastReply: lastReply ?? message.thread.lastReply + } + } +} + +export interface OldMessage { + id: MessageID + cardId: CardID + type: 'message' | 'activity' + content: Markdown + extra?: MessageExtra + creator: SocialID + created: Date + + removed: boolean + edited?: Date + + reactions: OldReaction[] + attachments: Attachment[] + thread?: OldThread +} + +export interface OldReaction { + reaction: string + creator: SocialID + created: Date +} + +export interface OldThread { + cardId: CardID + messageId: MessageID + threadId: CardID + threadType: CardType + repliesCount: number + lastReply: Date +} + +export type Patch = UpdatePatch | RemovePatch | ReactionPatch | ThreadPatch | AttachmentPatch + +export enum PatchType { + update = 'update', + remove = 'remove', + reaction = 'reaction', + attachment = 'attachment', + thread = 'thread' +} + +interface BasePatch { + messageId: MessageID + type: PatchType + creator: SocialID + created: Date + + data: Record +} + +export interface UpdatePatch extends BasePatch { + type: PatchType.update + data: UpdatePatchData +} + +export interface UpdatePatchData { + content?: Markdown + extra?: MessageExtra +} + +export interface RemovePatch extends BasePatch { + type: PatchType.remove + data: RemovePatchData +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RemovePatchData {} + +export interface ReactionPatch extends BasePatch { + type: PatchType.reaction + data: AddReactionPatchData | RemoveReactionPatchData +} + +export interface AddReactionPatchData { + operation: 'add' + reaction: string +} + +export interface RemoveReactionPatchData { + operation: 'remove' + reaction: string +} + +export interface AttachmentPatch extends BasePatch { + type: PatchType.attachment + data: AddAttachmentsPatchData | RemoveAttachmentsPatchData | SetAttachmentsPatchData | UpdateAttachmentsPatchData +} + +export interface AddAttachmentsPatchData { + operation: 'add' + attachments: AttachmentData[] +} + +export interface RemoveAttachmentsPatchData { + operation: 'remove' + ids: AttachmentID[] +} + +export interface SetAttachmentsPatchData { + operation: 'set' + attachments: AttachmentData[] +} + +export interface UpdateAttachmentsPatchData { + operation: 'update' + attachments: AttachmentUpdateData[] +} + +export interface ThreadPatch extends BasePatch { + type: PatchType.thread + data: AttachThreadPatchData | UpdateThreadPatchData +} + +export interface AttachThreadPatchData { + operation: 'attach' + threadId: CardID + threadType: CardType +} + +export interface UpdateThreadPatchData { + operation: 'update' + threadId: CardID + threadType?: CardType + repliesCountOp?: 'increment' | 'decrement' + lastReply?: Date +} + +export interface FileMetadata { + cardId: CardID + title: string + fromDate: Date + toDate: Date +} + +export interface FileMessage { + id: MessageID + type: 'message' | 'activity' + content: Markdown + extra?: MessageExtra + + creator: SocialID + created: Date + + removed: boolean + edited?: Date + + reactions: OldReaction[] + attachments: Attachment[] + thread?: FileThread +} + +export interface FileThread { + threadId: CardID + threadType: CardType + repliesCount: number + lastReply: Date +} + +export interface ParsedFile { + cardId: CardID + title: string + fromDate: Date + toDate: Date + messages: OldMessage[] +} diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 1126d9886c..0f3724715d 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -56,6 +56,14 @@ import { type Account as OldAccount, type Workspace as OldWorkspace } from '@hcengineering/account-service' +import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client' +import { + getDBClient, + createPostgreeDestroyAdapter, + createPostgresAdapter, + createPostgresTxAdapter, + shutdownPostgres +} from '@hcengineering/postgres' import { faker } from '@faker-js/faker' import { getPlatformQueue } from '@hcengineering/kafka' @@ -69,6 +77,7 @@ import { isDeletingMode, MeasureMetricsContext, metricsToString, + SocialId, SocialIdType, systemAccountEmail, systemAccountUuid, @@ -80,7 +89,8 @@ import { type Tx, type Version, type WorkspaceDataId, - type WorkspaceUuid + type WorkspaceUuid, + type PersonUuid } from '@hcengineering/core' import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model' import { @@ -92,12 +102,6 @@ import { } from '@hcengineering/mongo' import { getModelVersion } from '@hcengineering/model-all' -import { - createPostgreeDestroyAdapter, - createPostgresAdapter, - createPostgresTxAdapter, - shutdownPostgres -} from '@hcengineering/postgres' import { QueueTopic, workspaceEvents, @@ -131,6 +135,7 @@ import { mkdir, writeFile } from 'fs/promises' import { dirname } from 'path' import { restoreMarkupRefs } from './markup' import { restoreGithubIntegrations } from './restoreGithub' +import { migrateWorkspaceMessages } from './communication' const colorConstants = { colorRed: '\u001b[31m', @@ -2845,6 +2850,52 @@ export function devTool ( }, dbUrl) }) + program + .command('migrate-communication-to-hulylake') + .description('Migrate communication messages to hulylake') + .action(async () => { + const { dbUrl } = prepareTools() + const storageConfig = storageConfigFromEnv() + const hulylakeUrl = process.env.HULYLAKE_URL ?? '' + + if (hulylakeUrl === '') { + throw new Error('HULYLAKE_URL should be specified') + } + + const storage: StorageAdapter = buildStorageFromConfig(storageConfig) + const token = generateToken(systemAccountUuid, undefined, { + service: 'tool' + }) + const db = getDBClient(dbUrl, undefined, 'tool') + const dbClient = await db.getClient() + const accountClient = getAccountClient(token) + const personUuidBySocialId = new Map() + + await withAccountDatabase(async (accountDb) => { + const workspaces = await accountDb.workspace.find({}) + for (const ws of workspaces) { + try { + const hulylake = getHulylakeClient(hulylakeUrl, ws.uuid, token) + console.log('start workspace migration', ws.name) + await migrateWorkspaceMessages( + toolCtx.newChild(ws.name, {}), + ws, + dbClient, + storage, + hulylake, + accountClient, + personUuidBySocialId + ) + console.log('done workspace migration', ws.name) + } catch (err: any) { + console.error('failed to migrate workspace', ws.name) + console.error(err) + } + } + db.close() + }, dbUrl) + }) + extendProgram?.(program) process.on('unhandledRejection', (reason, promise) => { diff --git a/packages/presentation/package.json b/packages/presentation/package.json index 62bd5e93cc..0307123747 100644 --- a/packages/presentation/package.json +++ b/packages/presentation/package.json @@ -61,6 +61,7 @@ "@hcengineering/emoji": "^0.6.0", "@hcengineering/theme": "^0.6.5", "@hcengineering/retry": "^0.6.0", + "@hcengineering/hulylake-client": "^0.6.0", "fast-equals": "^5.2.2", "png-chunks-extract": "^1.0.0", "svelte": "^4.2.20", diff --git a/packages/presentation/src/communication.ts b/packages/presentation/src/communication.ts index 664818f37d..5a8c62206f 100644 --- a/packages/presentation/src/communication.ts +++ b/packages/presentation/src/communication.ts @@ -33,26 +33,24 @@ import { type UpdateAttachmentsOperation, type UpdateNotificationContextEvent, type UpdateNotificationEvent, - type UpdateNotificationQuery, + type NotificationQuery, type UpdatePatchEvent } from '@hcengineering/communication-sdk-types' import { - type AccountID, + type AccountUuid, type CardID, type CardType, type Collaborator, type ContextID, type FindCollaboratorsParams, type FindLabelsParams, - type FindMessagesGroupsParams, - type FindMessagesParams, type FindNotificationContextParams, type FindNotificationsParams, + type FindMessagesMetaParams, type Label, type Markdown, type Message, type MessageID, - type MessagesGroup, MessageType, type Notification, type NotificationContext, @@ -61,7 +59,10 @@ import { type AttachmentData, type AttachmentParams, type AttachmentUpdateData, - type WithTotal + type WithTotal, + type NotificationID, + type Emoji, + type MessageMeta } from '@hcengineering/communication-types' import core, { generateId, @@ -71,17 +72,19 @@ import core, { SocialIdType, type Tx, type TxDomainEvent, - AccountRole + AccountRole, + generateUuid } from '@hcengineering/core' import { onDestroy } from 'svelte' import { addNotification, NotificationSeverity, languageStore } from '@hcengineering/ui' -import { translate } from '@hcengineering/platform' +import { getMetadata, translate } from '@hcengineering/platform' import view from '@hcengineering/view' -import { v4 as uuid } from 'uuid' - -import { getCurrentWorkspaceUuid, getFilesUrl } from './file' -import { addTxListener, removeTxListener, type TxListener } from './utils' import { get } from 'svelte/store' +import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client' + +import { getCurrentWorkspaceUuid } from './file' +import { addTxListener, removeTxListener, type TxListener } from './utils' +import presentation from './plugin' export { createCollaboratorsQuery, @@ -107,7 +110,12 @@ export async function setCommunicationClient (platformClient: PlatformClient): P client.close() } const _client = new Client(platformClient) - initLiveQueries(_client, getCurrentWorkspaceUuid(), getFilesUrl(), onDestroy) + + const token = getMetadata(presentation.metadata.Token) ?? '' + const hulylakeUrl = getMetadata(presentation.metadata.HulylakeUrl) ?? '' + const hulylake = getHulylakeClient(hulylakeUrl, getCurrentWorkspaceUuid(), token) + + initLiveQueries(_client, hulylake, onDestroy) client = _client onClientListeners.forEach((fn) => { fn() @@ -161,7 +169,7 @@ class Client { async createMessage (cardId: CardID, cardType: CardType, content: Markdown): Promise { const event: CreateMessageEvent = { type: MessageEventType.CreateMessage, - messageType: MessageType.Message, + messageType: MessageType.Text, cardId, cardType, content, @@ -198,28 +206,28 @@ class Client { await this.sendEvent(event) } - async addReaction (cardId: CardID, messageId: MessageID, reaction: string): Promise { + async addReaction (cardId: CardID, messageId: MessageID, emoji: Emoji): Promise { const event: ReactionPatchEvent = { type: MessageEventType.ReactionPatch, cardId, messageId, operation: { opcode: 'add', - reaction + reaction: emoji }, socialId: this.getSocialId() } await this.sendEvent(event) } - async removeReaction (cardId: CardID, messageId: MessageID, reaction: string): Promise { + async removeReaction (cardId: CardID, messageId: MessageID, emoji: Emoji): Promise { const event: ReactionPatchEvent = { type: MessageEventType.ReactionPatch, cardId, messageId, operation: { opcode: 'remove', - reaction + reaction: emoji }, socialId: this.getSocialId() } @@ -245,7 +253,7 @@ class Client { opcode: 'add', attachments: ops.add.map((it) => ({ ...it, - id: it.id ?? (uuid() as AttachmentID) + id: it.id ?? (generateUuid() as AttachmentID) })) }) } @@ -262,7 +270,7 @@ class Client { opcode: 'set', attachments: ops.set.map((it) => ({ ...it, - id: it.id ?? (uuid() as AttachmentID) + id: it.id ?? (generateUuid() as AttachmentID) })) }) } @@ -286,7 +294,7 @@ class Client { await this.sendEvent(event) } - async addCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountID[]): Promise { + async addCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountUuid[]): Promise { const event: AddCollaboratorsEvent = { type: NotificationEventType.AddCollaborators, cardId, @@ -297,7 +305,7 @@ class Client { await this.sendEvent(event) } - async removeCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountID[]): Promise { + async removeCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountUuid[]): Promise { const event: RemoveCollaboratorsEvent = { type: NotificationEventType.RemoveCollaborators, cardId, @@ -329,7 +337,11 @@ class Client { await this.sendEvent(event) } - async updateNotifications (contextId: ContextID, query: UpdateNotificationQuery, read: boolean): Promise { + async updateNotifications ( + contextId: ContextID, + query: Pick & { id?: NotificationID }, + read: boolean + ): Promise { const event: UpdateNotificationEvent = { type: NotificationEventType.UpdateNotification, contextId, @@ -342,37 +354,32 @@ class Client { await this.sendEvent(event) } - async findMessages (params: FindMessagesParams, queryId?: number): Promise { + async findMessagesMeta (params: FindMessagesMetaParams): Promise { return ( - await this.connection.domainRequest(COMMUNICATION, { - findMessages: { params, queryId } - }) - ).value - } - - async findMessagesGroups (params: FindMessagesGroupsParams): Promise { - return ( - await this.connection.domainRequest(COMMUNICATION, { - findMessagesGroups: { params } + await this.connection.domainRequest(COMMUNICATION, { + findMessagesMeta: { params } }) ).value } async findNotificationContexts ( params: FindNotificationContextParams, - queryId?: number + subscription?: number | string ): Promise { return ( await this.connection.domainRequest(COMMUNICATION, { - findNotificationContexts: { params, queryId } + findNotificationContexts: { params, subscription } }) ).value } - async findNotifications (params: FindNotificationsParams, queryId?: number): Promise> { + async findNotifications ( + params: FindNotificationsParams, + subscription?: number | string + ): Promise> { return ( await this.connection.domainRequest>(COMMUNICATION, { - findNotifications: { params, queryId } + findNotifications: { params, subscription } }) ).value } @@ -393,9 +400,15 @@ class Client { ).value } - async unsubscribeQuery (id: number): Promise { + async subscribeCard (cardId: CardID, subscription: string | number): Promise { await this.connection.domainRequest(COMMUNICATION, { - unsubscribeQuery: id + subscribeCard: { cardId, subscription } + }) + } + + async unsubscribeCard (cardId: CardID, subscription: string | number): Promise { + await this.connection.domainRequest(COMMUNICATION, { + unsubscribeCard: { cardId, subscription } }) } @@ -438,7 +451,7 @@ class Client { return id } - private getAccount (): AccountID { + private getAccount (): AccountUuid { return getCurrentAccount().uuid } } diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index 295594569c..b52c3d7893 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -180,7 +180,8 @@ export default plugin(presentationId, { StatsUrl: '' as Metadata, MailUrl: '' as Metadata, PreviewUrl: '' as Metadata, - PulseUrl: '' as Metadata + PulseUrl: '' as Metadata, + HulylakeUrl: '' as Metadata }, status: { FileTooLarge: '' as StatusCode diff --git a/plugins/card-resources/src/components/CardWidget.svelte b/plugins/card-resources/src/components/CardWidget.svelte index 084190028f..e047c7c592 100644 --- a/plugins/card-resources/src/components/CardWidget.svelte +++ b/plugins/card-resources/src/components/CardWidget.svelte @@ -65,7 +65,7 @@ ) $: tab?.id && - contextsQuery.query({ card: tab.id as Ref, limit: 1 }, (res) => { + contextsQuery.query({ cardId: tab.id as Ref, limit: 1 }, (res) => { context = res.getResult()[0] isContextLoaded = true }) diff --git a/plugins/card-resources/src/components/CardWidgetTab.svelte b/plugins/card-resources/src/components/CardWidgetTab.svelte index 89a3bf5244..de01abaf16 100644 --- a/plugins/card-resources/src/components/CardWidgetTab.svelte +++ b/plugins/card-resources/src/components/CardWidgetTab.svelte @@ -48,7 +48,7 @@ }) $: notificationsQuery.query( - { card: cardId, limit: 1, read: false, order: SortingOrder.Descending, type: NotificationType.Message }, + { cardId, limit: 1, read: false, order: SortingOrder.Descending, type: NotificationType.Message }, (res) => { count = res.getResult().length } diff --git a/plugins/card-resources/src/components/EditCardNew.svelte b/plugins/card-resources/src/components/EditCardNew.svelte index ccf55689f0..a0715875b1 100644 --- a/plugins/card-resources/src/components/EditCardNew.svelte +++ b/plugins/card-resources/src/components/EditCardNew.svelte @@ -84,7 +84,7 @@ } }) - $: contextsQuery.query({ card: _id, limit: 1 }, (res) => { + $: contextsQuery.query({ cardId: _id, limit: 1 }, (res) => { context = res.getResult()[0] isContextLoaded = true }) diff --git a/plugins/card-resources/src/components/FeedCardPresenter.svelte b/plugins/card-resources/src/components/FeedCardPresenter.svelte index 2e26727338..ff26c11e53 100644 --- a/plugins/card-resources/src/components/FeedCardPresenter.svelte +++ b/plugins/card-resources/src/components/FeedCardPresenter.svelte @@ -42,10 +42,14 @@ let person: Person | undefined = undefined $: messagesQuery.query( - { card: card._id, strict: true, attachments: true, reactions: true, limit: 1, order: SortingOrder.Descending }, + { cardId: card._id, limit: 1, order: SortingOrder.Descending }, (res) => { const msgs = res.getResult().reverse() message = msgs[msgs.length - 1] + }, + { + attachments: true, + reactions: true } ) @@ -103,7 +107,7 @@ {#if isCompact} {:else} - + {/if} {/if} diff --git a/plugins/card-resources/src/components/NewCardForm.svelte b/plugins/card-resources/src/components/NewCardForm.svelte index 4aa207c44f..1cb8e478d4 100644 --- a/plugins/card-resources/src/components/NewCardForm.svelte +++ b/plugins/card-resources/src/components/NewCardForm.svelte @@ -93,7 +93,7 @@ console.error('Failed to create thread card') return } - const blobs: BlobParams[] = descriptionBox.getAttachments().map((attachment) => ({ + const blobs: (BlobParams & { mimeType: string })[] = descriptionBox.getAttachments().map((attachment) => ({ blobId: attachment.file, mimeType: attachment.type, fileName: attachment.name, @@ -113,7 +113,11 @@ } } - async function createMessage (card: Ref, markup: Markup, blobs: BlobParams[]): Promise { + async function createMessage ( + card: Ref, + markup: Markup, + blobs: (BlobParams & { mimeType: string })[] + ): Promise { const markdown = markupToMarkdown(markupToJSON(markup)) const { messageId } = await communicationClient.createMessage(card, type, markdown) @@ -121,7 +125,7 @@ void communicationClient.attachmentPatch(card, messageId, { add: blobs.map((it) => ({ id: it.blobId as any as AttachmentID, - type: it.mimeType, + mimeType: it.mimeType, params: it })) }) diff --git a/plugins/card-resources/src/components/navigator-next/NavigatorCards.svelte b/plugins/card-resources/src/components/navigator-next/NavigatorCards.svelte index 76336aa2f8..984c913dbc 100644 --- a/plugins/card-resources/src/components/navigator-next/NavigatorCards.svelte +++ b/plugins/card-resources/src/components/navigator-next/NavigatorCards.svelte @@ -70,7 +70,7 @@ $: if (favorites.length > 0) { contextsQuery.query( { - card: favorites.map((it) => it.attachedTo), + cardId: favorites.map((it) => it.attachedTo), notifications: { read: false, type: NotificationType.Message, diff --git a/plugins/card-resources/src/components/navigator-next/NavigatorCardsSection.svelte b/plugins/card-resources/src/components/navigator-next/NavigatorCardsSection.svelte index 60022b52c7..dc00b18395 100644 --- a/plugins/card-resources/src/components/navigator-next/NavigatorCardsSection.svelte +++ b/plugins/card-resources/src/components/navigator-next/NavigatorCardsSection.svelte @@ -105,7 +105,7 @@ $: if (cards.length > 0) { notificationContextsQuery.query( { - card: cards.map((it) => it._id), + cardId: cards.map((it) => it._id), notifications: { type: NotificationType.Message, order: SortingOrder.Descending, diff --git a/plugins/chat-resources/src/components/inbox/InboxCard.svelte b/plugins/chat-resources/src/components/inbox/InboxCard.svelte index eb32914676..9010576598 100644 --- a/plugins/chat-resources/src/components/inbox/InboxCard.svelte +++ b/plugins/chat-resources/src/components/inbox/InboxCard.svelte @@ -35,7 +35,7 @@ let total = 0 - notificationsQuery.query({ limit: 1, total: true, read: false, strict: true, context: context.id }, (res) => { + notificationsQuery.query({ limit: 1, total: true, read: false, strict: true, contextId: context.id }, (res) => { total = res.getTotal() }) diff --git a/plugins/chat-resources/src/components/inbox/InboxNavigation.svelte b/plugins/chat-resources/src/components/inbox/InboxNavigation.svelte index cef66fb99d..ed6d4cb8da 100644 --- a/plugins/chat-resources/src/components/inbox/InboxNavigation.svelte +++ b/plugins/chat-resources/src/components/inbox/InboxNavigation.svelte @@ -48,7 +48,6 @@ query.query( { notifications: { - message: true, order: SortingOrder.Descending, limit: 3 }, @@ -63,7 +62,8 @@ if (contexts.length < limit && window.hasPrevPage()) { void window.loadPrevPage() } - } + }, + { message: true } ) $: cardsQuery.query(cardPlugin.class.Card, { _id: { $in: contexts.map((c) => c.cardId) } }, (res) => { diff --git a/plugins/chat-resources/src/components/inbox/MessageNotification.svelte b/plugins/chat-resources/src/components/inbox/MessageNotification.svelte index ed55fe1f55..065a639c94 100644 --- a/plugins/chat-resources/src/components/inbox/MessageNotification.svelte +++ b/plugins/chat-resources/src/components/inbox/MessageNotification.svelte @@ -16,12 +16,11 @@ -{#if notification.message} - -{/if} + diff --git a/plugins/chat-resources/src/components/inbox/ReactionNotification.svelte b/plugins/chat-resources/src/components/inbox/ReactionNotification.svelte index a3dcd65de2..24155bf505 100644 --- a/plugins/chat-resources/src/components/inbox/ReactionNotification.svelte +++ b/plugins/chat-resources/src/components/inbox/ReactionNotification.svelte @@ -13,7 +13,7 @@ --> {#if notification.message} @@ -57,7 +60,8 @@ | undefined = undefined - $: void updatePerson(message.creator) + $: void updatePerson(creator) - function getTooltipLabel (message: Message): IntlString { + function getTooltipLabel (message: Message | undefined): IntlString { + if (message == null) return getEmbeddedLabel('') const text = markupToText(jsonToMarkup(markdownToMarkup(message.content))) if (text.length > tooltipLimit) { return getEmbeddedLabel(text.substring(0, tooltipLimit) + '...') @@ -56,20 +58,24 @@ {color} {person} {padding} - socialId={message.creator} + socialId={creator} {date} - fixHeight={message.type !== MessageType.Activity} + fixHeight={message == null || message.type !== MessageType.Activity} tooltipLabel={getTooltipLabel(message)} > - {#if isActivityMessage(message)} - - {:else} - + {#if message} + {#if isActivityMessage(message)} + + {:else} + + {/if} {/if} - + {#if message} + + {/if} diff --git a/plugins/communication-resources/src/actions.ts b/plugins/communication-resources/src/actions.ts index 86215b6200..2996337565 100644 --- a/plugins/communication-resources/src/actions.ts +++ b/plugins/communication-resources/src/actions.ts @@ -72,20 +72,27 @@ export const replyInThread: MessageActionFunction = async (message: Message, par await showForbidden() return } - await attachCardToMessage(message, parentCard, createThreadTitle(message, parentCard), chat.masterTag.Thread) + await attachCardToMessage( + message, + parentCard, + createThreadTitle(message, parentCard), + chat.masterTag.Thread, + `${parentCard._id}_${message.id}` as Ref + ) } export async function attachCardToMessage ( message: Message, parentCard: Card, title: string, - type: Ref + type: Ref, + _id?: Ref ): Promise { const client = getClient() const communicationClient = getCommunicationClient() const hierarchy = client.getHierarchy() - const thread = message.thread + const thread = _id != null ? message.threads.find((it) => it.threadId === _id) : undefined if (thread != null) { const _id = thread.threadId const card = await client.findOne(cardPlugin.class.Card, { _id: _id as Ref }) @@ -95,7 +102,7 @@ export async function attachCardToMessage ( return } - const threadCardID = generateId() + const threadCardID = _id ?? generateId() await communicationClient.attachThread(parentCard._id, message.id, threadCardID, type) @@ -140,11 +147,7 @@ function createThreadTitle (message: Message, parent: Card): string { } export const canReplyInThread: MessageActionVisibilityTester = (message: Message): boolean => { - return ( - message.type === MessageType.Message && - message.extra?.threadRoot !== true && - (!message.removed || message.thread != null) - ) + return message.type === MessageType.Text && message.extra?.threadRoot !== true } export const translateMessage: MessageActionFunction = async (message: Message): Promise => { @@ -183,7 +186,7 @@ export const translateMessage: MessageActionFunction = async (message: Message): export const canTranslateMessage: MessageActionVisibilityTester = (message: Message): boolean => { const url = getMetadata(aiBot.metadata.EndpointURL) ?? '' if (url === '') return false - return message.type === MessageType.Message && !message.removed + return message.type === MessageType.Text } export const showOriginalMessage: MessageActionFunction = async (message: Message): Promise => { @@ -205,19 +208,18 @@ export const editMessage: MessageActionFunction = async (message: Message): Prom } export const canEditMessage: MessageActionVisibilityTester = (message: Message): boolean => { - if (message.type !== MessageType.Message || message.removed) return false + if (message.type !== MessageType.Text) return false const me = getCurrentAccount() return me.socialIds.includes(message.creator) } export const removeMessage: MessageActionFunction = async (message: Message): Promise => { const communicationClient = getCommunicationClient() - message.removed = true await communicationClient.removeMessage(message.cardId, message.id) } export const canRemoveMessage: MessageActionVisibilityTester = (message: Message): boolean => { - if (message.type !== MessageType.Message || message.removed) return false + if (message.type !== MessageType.Text) return false const me = getCurrentAccount() return me.socialIds.includes(message.creator) } @@ -234,7 +236,7 @@ export const createCard: MessageActionFunction = async (message: Message, card: } export const canCreateCard: MessageActionVisibilityTester = (message: Message): boolean => { - return canReplyInThread(message) && message.thread == null + return canReplyInThread(message) } let allMessageActions: MessageAction[] | undefined diff --git a/plugins/communication-resources/src/components/AttachmentName.svelte b/plugins/communication-resources/src/components/AttachmentName.svelte index 8c55c868a9..2091dc170f 100644 --- a/plugins/communication-resources/src/components/AttachmentName.svelte +++ b/plugins/communication-resources/src/components/AttachmentName.svelte @@ -31,7 +31,7 @@ {/if} {#if isAppletAttachment(attachment)} - {@const applet = applets.find((it) => it.type === attachment.type)} + {@const applet = applets.find((it) => it.type === attachment.mimeType)} {#if applet}