diff --git a/.eslintrc.json b/.eslintrc.json index 588091d817..1242169e96 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -184,7 +184,7 @@ "no-underscore-dangle": "off", "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "off", - "no-unused-vars": "error", + "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "no-unused-expressions": "off", "no-use-before-define": "off", "no-useless-call": "error", diff --git a/.github/workflows/xmpp-notify.yml b/.github/workflows/xmpp-notify.yml new file mode 100644 index 0000000000..42bdf5e904 --- /dev/null +++ b/.github/workflows/xmpp-notify.yml @@ -0,0 +1,44 @@ +name: 'XMPP Notifier' + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + notif-script: + runs-on: ubuntu-latest + name: job that pushes repo news to xmpp + steps: + - name: push_info_step + id: push + uses: conversejs/github-action-xmpp-notifier@master + if: github.event_name == 'push' + with: # Set the secrets as inputs + # jid expects the bot's bare jid (user@domain) + jid: ${{ secrets.jid }} + password: ${{ secrets.password }} + server_host: ${{ secrets.server_host }} + recipient: ${{ secrets.recipient }} + server_port: ${{ secrets.server_port }} + message: | + *${{ github.actor }}* pushed commits to ${{ github.event.ref }} with message(s): + > ${{ join(github.event.commits.*.message, '\n\n> ') }} + + ${{ github.event.compare }} + recipient_is_room: true + + - name: pr_open_info_step + id: pull_request_open + uses: conversejs/github-action-xmpp-notifier@master + if: github.event_name == 'pull_request' && github.event.action == 'opened' + with: # Set the secrets as inputs + jid: ${{ secrets.jid }} + password: ${{ secrets.password }} + server_host: ${{ secrets.server_host }} + recipient: ${{ secrets.recipient }} + message: | + *${{ github.actor }}* opened a PR ${{ github.event.pull_request.html_url }} + > ${{ github.event.pull_request.title }} + recipient_is_room: true diff --git a/CHANGES.md b/CHANGES.md index b4d4310c24..1a10e569a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,11 +3,25 @@ ## 11.0.0 (Unreleased) - #2716: Fix issue with chat display when opening via URL +- #3033: Add the `muc_grouped_by_domain` option to display MUCs on the same domain in collapsible groups +- #3300: Adding the maxWait option for `debouncedPruneHistory` +- #3302: debounce MUC sidebar rendering +- #3307: Fix inconsistency between browsers on textarea outlines +- Add an occupants filter to the MUC sidebar +- Fix: MUC occupant list does not sort itself on nicknames or roles changes +- Fix: refresh the MUC sidebar when participants collection is sorted ### Breaking changes: - Remove the old `_converse.BootstrapModal` in favor of `_converse.BaseModal` which is a web component. - The connection is no longer available on the `_converse` object. Instead, use `api.connection.get()`. +- Add a new `exports` attribute on the `_converse` object which is meant for + providing access for 3rd party plugins to code (e.g. classes) from + converse.js. Some classes that were on the `_converse` object, like + `CustomElement` are not on `_converse.exports`. +- The `windowStateChanged` event has been removed. If you used it, rely on the + `visibilitychange` event on `document` instead. +- `api.modal.create` no longer takes a class, instead it takes the name of a custom DOM element. ## 10.1.6 (2023-08-31) diff --git a/docs/source/builds.rst b/docs/source/builds.rst index 416a390920..6d783cf885 100644 --- a/docs/source/builds.rst +++ b/docs/source/builds.rst @@ -48,8 +48,9 @@ One reason you might want to create your own bundles, is because you want to remove some of the core plugins of Converse, or perhaps you want to include your own. -To add or remove plugins from the build, you need to modify the -`src/converse.js `_ file. +To add or remove plugins from the build, you need to modify +`src/index.js `_ or +`src/headless/index.js `_. You'll find a section marked ``/* START: Removable components`` and ``/* END: Removable components */``. diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 1f4b6047d2..a5e93f3556 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -1278,6 +1278,14 @@ By fetching member lists, Converse.js will always show these users as participants of the MUC, giving them a permanent "presence" in the MUC. +muc_grouped_by_domain +--------------------- + +* Default: ``false`` + +If ``true``, displays MUCS of a same domain together, in collapsible groups. + + muc_history_max_stanzas ----------------------- diff --git a/docs/source/plugin_development.rst b/docs/source/plugin_development.rst index a6feae307d..81446cbe98 100644 --- a/docs/source/plugin_development.rst +++ b/docs/source/plugin_development.rst @@ -351,7 +351,7 @@ All configuration settings have default values which can be overridden when gets called. Plugins often need their own additional configuration settings and you can add -these settings with the `_converse.api.settings.update `_ +these settings with the `_converse.api.settings.extend `_ method. Exposing promises @@ -455,12 +455,13 @@ Hooks ----- Converse has the concept of ``hooks``, which are special events that allow you -to modify it's behaviour at runtime. +to modify behaviour at runtime. A hook is similar to an event, but it differs in two meaningful ways: 1. Converse will wait for all handlers of a hook to finish before continuing inside the function from where the hook was triggered. -2. Each hook contains a payload, which the handlers can modify or extend, before returning it (either to the function that triggered the hook or to subsequent handlers). +2. Each hook contains a payload, which the handlers can modify or extend, before returning it + (either to the function that triggered the hook or to subsequent handlers). These two properties of hooks makes it possible for 3rd party plugins to intercept and update data, allowing them to modify Converse without the need of diff --git a/karma.conf.js b/karma.conf.js index 5e5717fe6e..bd8db75e73 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -92,6 +92,7 @@ module.exports = function(config) { { pattern: "src/plugins/muc-views/tests/muc-registration.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/muc.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/nickname.js", type: 'module' }, + { pattern: "src/plugins/muc-views/tests/occupants-filter.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/occupants.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/rai.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/retractions.js", type: 'module' }, @@ -110,6 +111,7 @@ module.exports = function(config) { { pattern: "src/plugins/push/tests/push.js", type: 'module' }, { pattern: "src/plugins/register/tests/register.js", type: 'module' }, { pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' }, + { pattern: "src/plugins/roomslist/tests/grouplists.js", type: 'module' }, { pattern: "src/plugins/rootview/tests/root.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/add-contact-modal.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' }, diff --git a/package-lock.json b/package-lock.json index 5fcbab925f..f501739a6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@babel/core": "^7.18.5", "@babel/preset-env": "^7.18.2", "@converse/headless": "file:src/headless", + "@types/webappsec-credential-management": "^0.6.8", "@typescript-eslint/eslint-plugin": "^5.48.0", "autoprefixer": "^10.4.5", "babel-loader": "^9.1.0", @@ -63,7 +64,6 @@ "sass": "^1.51.0", "sass-loader": "^13.1.0", "style-loader": "^3.1.0", - "tsc": "^2.0.4", "typescript": "^4.9.5", "typescript-eslint-parser": "^22.0.0", "uglify-js": "^3.17.4", @@ -111,9 +111,9 @@ } }, "node_modules/@babel/cli": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.0.tgz", - "integrity": "sha512-17E1oSkGk2IwNILM4jtfAvgjt+ohmpfBky8aLerUfYZhiPNg7ca+CRCxZn8QDxwNhV/upsc2VHBCqGFIR+iBfA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.9.tgz", + "integrity": "sha512-vB1UXmGDNEhcf1jNAHKT9IlYk1R+hehVTLFlCLHBi8gfuHQGP6uRjgXVYU0EVlI/qwAWpstqkBdf2aez3/z/5Q==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -140,12 +140,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -153,30 +153,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", - "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -192,12 +192,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -231,14 +231,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -247,17 +247,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", + "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -287,9 +287,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -361,9 +361,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -471,9 +471,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -489,9 +489,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -512,23 +512,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", - "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -540,9 +540,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -552,9 +552,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -567,14 +567,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -583,6 +583,22 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -659,9 +675,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -674,9 +690,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -831,9 +847,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -846,14 +862,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -864,14 +880,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -881,9 +897,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -896,9 +912,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -911,12 +927,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -927,12 +943,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -944,18 +960,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -967,13 +982,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -983,9 +998,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -998,12 +1013,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1014,9 +1029,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1029,9 +1044,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1045,12 +1060,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1061,9 +1076,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1077,12 +1092,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1092,13 +1108,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1109,9 +1125,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1125,9 +1141,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1140,9 +1156,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1156,9 +1172,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1171,12 +1187,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1187,12 +1203,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1204,13 +1220,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, @@ -1222,12 +1238,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1254,9 +1270,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1269,9 +1285,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1285,9 +1301,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1301,16 +1317,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", + "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1320,13 +1336,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1336,9 +1352,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1352,9 +1368,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1369,9 +1385,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1384,12 +1400,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1400,13 +1416,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1418,9 +1434,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1433,9 +1449,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1449,9 +1465,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1464,9 +1480,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1479,9 +1495,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1495,9 +1511,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1510,9 +1526,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1525,9 +1541,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1540,9 +1556,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1555,12 +1571,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1571,12 +1587,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1587,12 +1603,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1603,25 +1619,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.20", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1633,59 +1650,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1717,9 +1733,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1729,40 +1745,40 @@ } }, "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", - "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1770,12 +1786,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -1835,8 +1851,8 @@ }, "node_modules/@converse/skeletor": { "version": "0.0.8", - "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#c845797101e21a163ac403fc65eac6db069a16b1", - "integrity": "sha512-lAMl9FCtNa2TXmp5+WQWZnIOa35+6MHRWx/PvZd4xQSHB1HOZqYxc00sBRh8E/XvpYvHwknDT7pkND8I+rL+UA==", + "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#4ff728207fa30721686021d11fb8b5245c54a6b4", + "integrity": "sha512-1IcP9VyfT75DzIcH4AG5csnSUGvOZdwHeSKXPs54E0IYUzJrMlExSsBKP24au9zyk/AJyhj+pBSMD2SY5zfppg==", "license": "MIT", "dependencies": { "@converse/localforage-getitems": "1.4.3", @@ -1873,18 +1889,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1905,9 +1921,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1920,9 +1936,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1973,13 +1989,13 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2000,9 +2016,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@iarna/toml": { @@ -2060,9 +2076,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2070,9 +2086,9 @@ } }, "node_modules/@jsdoc/salty": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", - "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz", + "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -2088,9 +2104,9 @@ "dev": true }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", - "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" }, "node_modules/@lit/reactive-element": { "version": "1.6.3", @@ -2155,9 +2171,9 @@ "dev": true }, "node_modules/@types/body-parser": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", - "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "dependencies": { "@types/connect": "*", @@ -2165,27 +2181,27 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.11.tgz", - "integrity": "sha512-isGhjmBtLIxdHBDl2xGwUzEM8AOyOvWsADWq7rqirdi/ZQoHnLWErHvsThcEzTX8juDRiZtzp2Qkv5bgNh6mAg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", - "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -2199,18 +2215,18 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.3", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", - "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -2218,9 +2234,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", - "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -2228,15 +2244,15 @@ } }, "node_modules/@types/estree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", - "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -2246,9 +2262,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.37", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", - "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -2264,30 +2280,30 @@ "dev": true }, "node_modules/@types/http-errors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", - "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.12", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz", - "integrity": "sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", "dev": true }, "node_modules/@types/markdown-it": { @@ -2301,39 +2317,51 @@ } }, "node_modules/@types/mdurl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.3.tgz", - "integrity": "sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", "dev": true }, "node_modules/@types/mime": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", - "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "devOptional": true + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "devOptional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, "node_modules/@types/qs": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", - "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", - "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, "node_modules/@types/retry": { @@ -2343,15 +2371,15 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", - "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -2359,18 +2387,18 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-asaEIoc6J+DbBKXtO7p2shWUpKacZOoMBEGBgPG91P8xhO53ohzHWGCs4ScZo5pQMf5ukQzVT9fhX1WzpHihig==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", - "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, "dependencies": { "@types/http-errors": "*", @@ -2379,29 +2407,35 @@ } }, "node_modules/@types/sockjs": { - "version": "0.3.34", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.34.tgz", - "integrity": "sha512-R+n7qBFnm/6jinlteC9DBL5dGiDGjWAvjo4viUanpnc/dG1y7uDoacXPIQ/PQEg1fI912SMHIa014ZjRpvDw4g==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/trusted-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", - "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/@types/unist": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", - "integrity": "sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/@types/webappsec-credential-management": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@types/webappsec-credential-management/-/webappsec-credential-management-0.6.8.tgz", + "integrity": "sha512-DES/SkK54U7AG8hmMkGCJkOSlywM3R+TzaWT+rBnX3lQTJ3K57jWr+UccWY8ImkuKekC9BjB+AH4zLJB4JKpvQ==", "dev": true }, "node_modules/@types/ws": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.6.tgz", - "integrity": "sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "devOptional": true, "dependencies": { "@types/node": "*" @@ -2454,9 +2488,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2599,9 +2633,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2658,9 +2692,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2695,6 +2729,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -2909,7 +2949,8 @@ "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead" }, "node_modules/accepts": { "version": "1.3.8", @@ -2925,9 +2966,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3105,9 +3146,9 @@ "dev": true }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, "node_modules/array-union": { @@ -3129,9 +3170,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "dev": true, "funding": [ { @@ -3148,9 +3189,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -3183,13 +3224,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -3197,25 +3238,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", - "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3337,13 +3378,11 @@ "dev": true }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -3413,9 +3452,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -3432,9 +3471,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -3460,13 +3499,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3504,9 +3548,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001543", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", - "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", + "version": "1.0.30001585", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz", + "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==", "dev": true, "funding": [ { @@ -3590,16 +3634,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3612,6 +3650,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -3657,9 +3698,9 @@ } }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -3669,13 +3710,13 @@ } }, "node_modules/clean-css-cli": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-5.6.2.tgz", - "integrity": "sha512-GDQkr6zVqHJhO3yWTy3sA22sMCT6iUqaJuBdqZMW6oI25MtiJ2iZXDmWzErpjoRotsB+TYPTpuZSNSgaC1n4lA==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-5.6.3.tgz", + "integrity": "sha512-MUAta8pEqA/d2DKQwtZU5nm0Og8TCyAglOx3GlWwjhGdKBwY4kVF6E5M6LU/jmmuswv+HbYqG/dKKkq5p1dD0A==", "dev": true, "dependencies": { "chokidar": "^3.5.2", - "clean-css": "^5.3.2", + "clean-css": "^5.3.3", "commander": "7.x", "glob": "^7.1.6" }, @@ -4019,12 +4060,12 @@ "hasInstallScript": true }, "node_modules/core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -4100,19 +4141,19 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -4122,7 +4163,16 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-loader/node_modules/lru-cache": { @@ -4138,9 +4188,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4274,6 +4324,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", + "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -4344,12 +4409,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -4524,9 +4583,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.539", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.539.tgz", - "integrity": "sha512-wRmWJ8F7rgmINuI32S6r2SLrw/h/bJQsDSvBiq9GBfvc2Lh73qTOwn73r3Cf67mjVgFGJYcYtmERzySa5jIWlg==", + "version": "1.4.665", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.665.tgz", + "integrity": "sha512-UpyCWObBoD+nSZgOC2ToaIdZB0r9GhqT2WahPKiSki6ckkSuKhQNso8V2PrFcHBMleI/eqbKgVQgVC4Wni4ilw==", "dev": true }, "node_modules/emoji-regex": { @@ -4575,9 +4634,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -4596,9 +4655,9 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -4633,9 +4692,9 @@ } }, "node_modules/envinfo": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", - "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -4653,16 +4712,25 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -4684,18 +4752,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -4861,9 +4930,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5062,12 +5131,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -5191,9 +5254,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5234,9 +5297,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -5272,9 +5335,9 @@ } }, "node_modules/filesize": { - "version": "10.0.12", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz", - "integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.0.tgz", + "integrity": "sha512-GTLKYyBSDz3nPhlLVPjPWZCnhkd9TrrRArNcy8Z+J2cqScB7h2McAzR6NBX6nYOoWafql0roY8hrocxnZBv9CQ==", "engines": { "node": ">= 10.4.0" } @@ -5374,18 +5437,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -5402,9 +5474,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -5431,9 +5503,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "engines": { "node": "*" @@ -5499,10 +5571,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -5523,15 +5598,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5677,6 +5756,18 @@ "node": ">=8" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5704,18 +5795,6 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -5725,6 +5804,18 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -5749,6 +5840,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -5889,9 +5992,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", - "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -5908,7 +6011,16 @@ "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/htmlparser2": { @@ -6148,9 +6260,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -6162,9 +6274,9 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/import-fresh": { @@ -6410,12 +6522,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6603,9 +6715,9 @@ } }, "node_modules/jasmine-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.1.tgz", - "integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", + "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true, "peer": true }, @@ -6665,9 +6777,9 @@ } }, "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -6913,22 +7025,46 @@ "dev": true }, "node_modules/karma-webpack": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", - "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", "dev": true, "dependencies": { "glob": "^7.1.3", - "minimatch": "^3.0.4", + "minimatch": "^9.0.3", "webpack-merge": "^4.1.5" }, "engines": { - "node": ">= 6" + "node": ">= 18" }, "peerDependencies": { "webpack": "^5.0.0" } }, + "node_modules/karma-webpack/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/karma-webpack/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/karma-webpack/node_modules/webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", @@ -6963,9 +7099,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -6990,13 +7126,13 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/leven": { @@ -7472,12 +7608,13 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz", + "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==", "dev": true, "dependencies": { - "schema-utils": "^4.0.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { "node": ">= 12.13.0" @@ -7558,9 +7695,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -7622,9 +7759,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -7679,9 +7816,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8229,9 +8366,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -8248,7 +8385,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -8269,14 +8406,14 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", - "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", "dev": true, "dependencies": { - "cosmiconfig": "^8.2.0", - "jiti": "^1.18.2", - "semver": "^7.3.8" + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { "node": ">= 14.15.0" @@ -8303,9 +8440,9 @@ } }, "node_modules/postcss-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -8342,9 +8479,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -8359,9 +8496,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -8405,9 +8542,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -8777,9 +8914,9 @@ } }, "node_modules/prettierx/node_modules/globby/node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -9004,9 +9141,9 @@ "dev": true }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -9300,9 +9437,9 @@ } }, "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -9366,9 +9503,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, "node_modules/rimraf": { @@ -9422,9 +9559,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.68.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.68.0.tgz", - "integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", + "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -9439,9 +9576,9 @@ } }, "node_modules/sass-loader": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", - "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -9541,11 +9678,12 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -9622,9 +9760,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -9714,6 +9852,23 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9763,14 +9918,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9809,9 +9968,9 @@ } }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", "dev": true, "dependencies": { "accepts": "~1.3.4", @@ -10042,8 +10201,8 @@ }, "node_modules/strophe.js": { "version": "2.0.0", - "resolved": "git+ssh://git@github.com/strophe/strophejs.git#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", - "integrity": "sha512-YIK1PUyJEwZgiPk30cEtxhN5ifLzyLOnch9T6tw7812pO2yuaGdBEQcqGd8NQjH3LzdcoS/DZJnXFxj+QKyviQ==", + "resolved": "git+ssh://git@github.com/strophe/strophejs.git#a75895308216e81b28f58d276395a01b15c9e645", + "integrity": "sha512-bI975yewZrq7r3Xbxc79HJxN2MLcwAxYpBGmI7pizZT7q2R0gd0ylBJBGcvXu87TvzkoLROAEOR5P+VJ3Pkp2Q==", "license": "MIT", "dependencies": { "abab": "^2.0.3" @@ -10055,9 +10214,9 @@ } }, "node_modules/style-loader": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", - "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -10104,9 +10263,9 @@ } }, "node_modules/terser": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", - "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -10122,16 +10281,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -10260,15 +10419,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/tsc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz", - "integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==", - "dev": true, - "bin": { - "tsc": "bin/tsc" - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10414,9 +10564,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.36", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.36.tgz", - "integrity": "sha512-CPPLoCts2p7D8VbybttE3P2ylv0OBZEAy7a12DsulIEcAiMtWJy+PBgMXgWDI80D5UwqE8oQPHYnk13tm38M2Q==", + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", "dev": true, "funding": [ { @@ -10460,6 +10610,12 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, "node_modules/unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -10844,19 +11000,19 @@ } }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -10870,7 +11026,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -11027,9 +11183,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -11048,12 +11204,13 @@ } }, "node_modules/webpack-merge": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", - "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", + "flat": "^5.0.2", "wildcard": "^2.0.0" }, "engines": { @@ -11331,12 +11488,11 @@ } }, "src/headless": { - "name": "@converse/headless", "version": "10.1.5", "license": "MPL-2.0", "dependencies": { "@converse/openpromise": "^0.0.1", - "@converse/skeletor": "conversejs/skeletor#c845797101e21a163ac403fc65eac6db069a16b1", + "@converse/skeletor": "conversejs/skeletor#4ff728207fa30721686021d11fb8b5245c54a6b4", "dayjs": "^1.11.8", "dompurify": "^2.3.1", "filesize": "^10.0.7", @@ -11345,7 +11501,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", + "strophe.js": "strophe/strophejs#a75895308216e81b28f58d276395a01b15c9e645", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/package.json b/package.json index b8cc9c2e76..ca432fc588 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@babel/core": "^7.18.5", "@babel/preset-env": "^7.18.2", "@converse/headless": "file:src/headless", + "@types/webappsec-credential-management": "^0.6.8", "@typescript-eslint/eslint-plugin": "^5.48.0", "autoprefixer": "^10.4.5", "babel-loader": "^9.1.0", @@ -103,7 +104,6 @@ "sass": "^1.51.0", "sass-loader": "^13.1.0", "style-loader": "^3.1.0", - "tsc": "^2.0.4", "typescript": "^4.9.5", "typescript-eslint-parser": "^22.0.0", "uglify-js": "^3.17.4", diff --git a/src/entry.js b/src/entry.js index e30a438df6..814ea7e7fd 100644 --- a/src/entry.js +++ b/src/entry.js @@ -55,6 +55,8 @@ const converse = { */ load (settings={}) { if (settings.assets_path) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore __webpack_public_path__ = settings.assets_path; // eslint-disable-line no-undef } require('./index.js'); @@ -63,7 +65,7 @@ const converse = { } } -window.converse = converse; +window['converse'] = converse; /** * Once Converse.js has loaded, it'll dispatch a custom event with the name `converse-loaded`. diff --git a/src/headless/index.js b/src/headless/index.js index 9e6f8f79e8..0737a0ca59 100644 --- a/src/headless/index.js +++ b/src/headless/index.js @@ -17,7 +17,7 @@ dayjs.extend(advancedFormat); */ import "./plugins/bookmarks/index.js"; // XEP-0199 XMPP Ping -import "./plugins/bosh.js"; // XEP-0206 BOSH +import "./plugins/bosh/index.js"; // XEP-0206 BOSH import "./plugins/caps/index.js"; // XEP-0115 Entity Capabilities import "./plugins/chat/index.js"; // RFC-6121 Instant messaging import "./plugins/chatboxes/index.js"; diff --git a/src/headless/log.js b/src/headless/log.js index 33342961d8..cbd2323701 100644 --- a/src/headless/log.js +++ b/src/headless/log.js @@ -43,8 +43,8 @@ export default { * When using the 'error' or 'warn' loglevels, a full stacktrace will be * logged as well. * @method log#log - * @param { string | Error } message - The message to be logged - * @param { string } level - The loglevel which allows for filtering of log messages + * @param {string|Element|Error} message - The message to be logged + * @param {string} level - The loglevel which allows for filtering of log messages */ log (message, level, style='') { if (LEVELS[level] < LEVELS[this.loglevel]) { @@ -59,7 +59,7 @@ export default { if (message instanceof Error) { message = message.stack; } else if (isElement(message)) { - message = message.outerHTML; + message = /** @type {Element} */(message).outerHTML; } const prefix = style ? '%c' : ''; if (level === 'error') { diff --git a/src/headless/package.json b/src/headless/package.json index 5c3273ab5e..9462a03975 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@converse/openpromise": "^0.0.1", - "@converse/skeletor": "conversejs/skeletor#c845797101e21a163ac403fc65eac6db069a16b1", + "@converse/skeletor": "conversejs/skeletor#4ff728207fa30721686021d11fb8b5245c54a6b4", "dayjs": "^1.11.8", "dompurify": "^2.3.1", "filesize": "^10.0.7", @@ -41,7 +41,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", + "strophe.js": "strophe/strophejs#a75895308216e81b28f58d276395a01b15c9e645", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/plugins/adhoc/api.js b/src/headless/plugins/adhoc/api.js index b92c120854..e64763c063 100644 --- a/src/headless/plugins/adhoc/api.js +++ b/src/headless/plugins/adhoc/api.js @@ -72,7 +72,7 @@ export default { * @param { String } sessionid * @param { 'execute' | 'cancel' | 'prev' | 'next' | 'complete' } action * @param { String } node - * @param { Array<{ string: string }> } inputs + * @param { Array<{ [k:string]: string }> } inputs */ async runCommand (jid, sessionid, node, action, inputs) { const iq = diff --git a/src/headless/plugins/bookmarks/collection.js b/src/headless/plugins/bookmarks/collection.js index e28ab009d8..de0054f580 100644 --- a/src/headless/plugins/bookmarks/collection.js +++ b/src/headless/plugins/bookmarks/collection.js @@ -3,7 +3,7 @@ import Bookmark from './model.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from "../../log.js"; -import { Collection } from "@converse/skeletor/src/collection.js"; +import { Collection } from "@converse/skeletor"; import { getOpenPromise } from '@converse/openpromise'; import { initStorage } from '../../utils/storage.js'; @@ -13,15 +13,10 @@ const { Strophe, $iq, sizzle } = converse.env; class Bookmarks extends Collection { constructor () { - super(); + super([], { comparator: (/** @type {Bookmark} */b) => b.get('name').toLowerCase() }); this.model = Bookmark; } - // eslint-disable-next-line class-methods-use-this - comparator (item) { - return item.get('name').toLowerCase(); - } - async initialize () { this.on('add', bm => this.openBookmarkedRoom(bm) .then(bm => this.markRoomAsBookmarked(bm)) @@ -31,7 +26,8 @@ class Bookmarks extends Collection { this.on('remove', this.markRoomAsUnbookmarked, this); this.on('remove', this.sendBookmarkStanza, this); - const cache_key = `converse.room-bookmarks${_converse.bare_jid}`; + const { session } = _converse; + const cache_key = `converse.room-bookmarks${session.get('bare_jid')}`; this.fetched_flag = cache_key+'fetched'; initStorage(this, cache_key); @@ -41,13 +37,15 @@ class Bookmarks extends Collection { * Triggered once the _converse.Bookmarks collection * has been created and cached bookmarks have been fetched. * @event _converse#bookmarksInitialized - * @type { _converse.Bookmarks } + * @type { Bookmarks } * @example _converse.api.listen.on('bookmarksInitialized', (bookmarks) => { ... }); */ api.trigger('bookmarksInitialized', this); } - // eslint-disable-next-line class-methods-use-this + /** + * @param {Bookmark} bookmark + */ async openBookmarkedRoom (bookmark) { if ( api.settings.get('muc_respect_autojoin') && bookmark.get('autojoin')) { const groupchat = await api.rooms.create( @@ -127,22 +125,31 @@ class Bookmarks extends Collection { ); } - // eslint-disable-next-line class-methods-use-this + /** + * @param {Bookmark} bookmark + */ markRoomAsBookmarked (bookmark) { - const groupchat = _converse.chatboxes.get(bookmark.get('jid')); + const { chatboxes } = _converse.state; + const groupchat = chatboxes.get(bookmark.get('jid')); groupchat?.save('bookmarked', true); } - // eslint-disable-next-line class-methods-use-this + /** + * @param {Bookmark} bookmark + */ markRoomAsUnbookmarked (bookmark) { - const groupchat = _converse.chatboxes.get(bookmark.get('jid')); + const { chatboxes } = _converse.state; + const groupchat = chatboxes.get(bookmark.get('jid')); groupchat?.save('bookmarked', false); } + /** + * @param {Element} stanza + */ createBookmarksFromStanza (stanza) { const xmlns = Strophe.NS.BOOKMARKS; const sel = `items[node="${xmlns}"] item storage[xmlns="${xmlns}"] conference`; - sizzle(sel, stanza).forEach(el => { + sizzle(sel, stanza).forEach(/** @type {Element} */(el) => { const jid = el.getAttribute('jid'); const bookmark = this.get(jid); const attrs = { @@ -157,7 +164,7 @@ class Bookmarks extends Collection { onBookmarksReceived (deferred, iq) { this.createBookmarksFromStanza(iq); - window.sessionStorage.setItem(this.fetched_flag, true); + window.sessionStorage.setItem(this.fetched_flag, 'true'); if (deferred !== undefined) { return deferred.resolve(); } @@ -174,7 +181,7 @@ class Bookmarks extends Collection { } else if (deferred) { if (iq.querySelector('error[type="cancel"] item-not-found')) { // Not an exception, the user simply doesn't have any bookmarks. - window.sessionStorage.setItem(this.fetched_flag, true); + window.sessionStorage.setItem(this.fetched_flag, 'true'); return deferred.resolve(); } else { log.error('Error while fetching bookmarks'); @@ -190,7 +197,8 @@ class Bookmarks extends Collection { async getUnopenedBookmarks () { await api.waitUntil('bookmarksInitialized') await api.waitUntil('chatBoxesFetched') - return this.filter(b => !_converse.chatboxes.get(b.get('jid'))); + const { chatboxes } = _converse.state; + return this.filter(b => !chatboxes.get(b.get('jid'))); } } diff --git a/src/headless/plugins/bookmarks/index.js b/src/headless/plugins/bookmarks/index.js index da6bb89d4f..adb211ba6b 100644 --- a/src/headless/plugins/bookmarks/index.js +++ b/src/headless/plugins/bookmarks/index.js @@ -53,8 +53,9 @@ converse.plugins.add('converse-bookmarks', { api.promises.add('bookmarksInitialized'); - _converse.Bookmark = Bookmark; - _converse.Bookmarks = Bookmarks; + const exports = { Bookmark, Bookmarks }; + Object.assign(_converse, exports); // TODO: DEPRECATED + Object.assign(_converse.exports, exports); api.listen.on('addClientFeatures', () => { if (api.settings.get('allow_bookmarks')) { @@ -63,16 +64,18 @@ converse.plugins.add('converse-bookmarks', { }) api.listen.on('clearSession', () => { - if (_converse.bookmarks) { - _converse.bookmarks.clearStore({'silent': true}); - window.sessionStorage.removeItem(_converse.bookmarks.fetched_flag); - delete _converse.bookmarks; + const { state } = _converse; + if (state.bookmarks) { + state.bookmarks.clearStore({'silent': true}); + window.sessionStorage.removeItem(state.bookmarks.fetched_flag); + delete state.bookmarks; } }); api.listen.on('connected', async () => { // Add a handler for bookmarks pushed from other connected clients - api.connection.get().addHandler(handleBookmarksPush, null, 'message', 'headline', null, _converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + api.connection.get().addHandler(handleBookmarksPush, null, 'message', 'headline', null, bare_jid); await Promise.all([api.waitUntil('chatBoxesFetched')]); initBookmarks(); }); diff --git a/src/headless/plugins/bookmarks/model.js b/src/headless/plugins/bookmarks/model.js index 60e54fcde8..d87c351671 100644 --- a/src/headless/plugins/bookmarks/model.js +++ b/src/headless/plugins/bookmarks/model.js @@ -1,5 +1,5 @@ import { converse } from '../../shared/api/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; const { Strophe } = converse.env; diff --git a/src/headless/plugins/bookmarks/utils.js b/src/headless/plugins/bookmarks/utils.js index 9b08a2b362..2b075b40e6 100644 --- a/src/headless/plugins/bookmarks/utils.js +++ b/src/headless/plugins/bookmarks/utils.js @@ -5,11 +5,14 @@ import log from "../../log.js"; const { Strophe, sizzle } = converse.env; export async function checkBookmarksSupport () { - const identity = await api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + if (!bare_jid) return false; + + const identity = await api.disco.getIdentity('pubsub', 'pep', bare_jid); if (api.settings.get('allow_public_bookmarks')) { return !!identity; } else { - return api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', _converse.bare_jid); + return api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', bare_jid); } } @@ -18,7 +21,8 @@ export async function initBookmarks () { return; } if (await checkBookmarksSupport()) { - _converse.bookmarks = new _converse.Bookmarks(); + _converse.state.bookmarks = new _converse.exports.Bookmarks(); + Object.assign(_converse, { bookmarks: _converse.state.bookmarks }); // TODO: DEPRECATED } } @@ -26,13 +30,13 @@ export function getNicknameFromBookmark (jid) { if (!api.settings.get('allow_bookmarks')) { return null; } - return _converse.bookmarks?.get(jid)?.get('nick'); + return _converse.state.bookmarks?.get(jid)?.get('nick'); } export function handleBookmarksPush (message) { if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"] items[node="${Strophe.NS.BOOKMARKS}"]`, message).length) { api.waitUntil('bookmarksInitialized') - .then(() => _converse.bookmarks.createBookmarksFromStanza(message)) + .then(() => _converse.state.bookmarks.createBookmarksFromStanza(message)) .catch(e => log.fatal(e)); } return true; diff --git a/src/headless/plugins/bosh.js b/src/headless/plugins/bosh.js deleted file mode 100644 index 85e892d4af..0000000000 --- a/src/headless/plugins/bosh.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @copyright The Converse.js contributors - * @license Mozilla Public License (MPLv2) - * @description Converse.js plugin which add support for XEP-0206: XMPP Over BOSH - */ -import 'strophe.js/src/bosh'; -import _converse from '../shared/_converse.js'; -import api, { converse } from '../shared/api/index.js'; -import log from "../log.js"; -import { BOSH_WAIT } from '../shared/constants.js'; -import { Model } from '@converse/skeletor/src/model.js'; -import { setUserJID, } from '../utils/init.js'; -import { isTestEnv } from '../utils/session.js'; -import { createStore } from '../utils/storage.js'; - -const { Strophe } = converse.env; - -const BOSH_SESSION_ID = 'converse.bosh-session'; - - -converse.plugins.add('converse-bosh', { - - enabled () { - return !_converse.api.settings.get("blacklisted_plugins").includes('converse-bosh'); - }, - - initialize () { - api.settings.extend({ - bosh_service_url: undefined, - prebind_url: null - }); - - - async function initBOSHSession () { - const id = BOSH_SESSION_ID; - if (!_converse.bosh_session) { - _converse.bosh_session = new Model({id}); - _converse.bosh_session.browserStorage = createStore(id, "session"); - await new Promise(resolve => _converse.bosh_session.fetch({'success': resolve, 'error': resolve})); - } - if (_converse.jid) { - if (_converse.bosh_session.get('jid') !== _converse.jid) { - const jid = await setUserJID(_converse.jid); - _converse.bosh_session.clear({'silent': true }); - _converse.bosh_session.save({jid}); - } - } else { // Keepalive - const jid = _converse.bosh_session.get('jid'); - jid && await setUserJID(jid); - } - return _converse.bosh_session; - } - - - _converse.startNewPreboundBOSHSession = function () { - if (!api.settings.get('prebind_url')) { - throw new Error("startNewPreboundBOSHSession: If you use prebind then you MUST supply a prebind_url"); - } - const connection = api.connection.get(); - const xhr = new XMLHttpRequest(); - xhr.open('GET', api.settings.get('prebind_url'), true); - xhr.setRequestHeader('Accept', 'application/json, text/javascript'); - xhr.onload = async function () { - if (xhr.status >= 200 && xhr.status < 400) { - const data = JSON.parse(xhr.responseText); - const jid = await setUserJID(data.jid); - connection.attach( - jid, - data.sid, - data.rid, - connection.onConnectStatusChanged, - BOSH_WAIT - ); - } else { - xhr.onerror(); - } - }; - xhr.onerror = function () { - api.connection.destroy(); - /** - * Triggered when fetching prebind tokens failed - * @event _converse#noResumeableBOSHSession - * @type { _converse } - * @example _converse.api.listen.on('noResumeableBOSHSession', _converse => { ... }); - */ - api.trigger('noResumeableBOSHSession', _converse); - }; - xhr.send(); - } - - - _converse.restoreBOSHSession = async function () { - const jid = (await initBOSHSession()).get('jid'); - const connection = api.connection.get(); - if (jid && (connection._proto instanceof Strophe.Bosh)) { - try { - connection.restore(jid, connection.onConnectStatusChanged); - return true; - } catch (e) { - !isTestEnv() && log.warn("Could not restore session for jid: "+jid+" Error message: "+e.message); - return false; - } - } - return false; - } - - - /************************ BEGIN Event Handlers ************************/ - api.listen.on('clearSession', () => { - if (_converse.bosh_session === undefined) { - // Remove manually, even if we don't have the corresponding - // model, to avoid trying to reconnect to a stale BOSH session - const id = BOSH_SESSION_ID; - sessionStorage.removeItem(id); - sessionStorage.removeItem(`${id}-${id}`); - } else { - _converse.bosh_session.destroy(); - delete _converse.bosh_session; - } - }); - - api.listen.on('setUserJID', () => { - if (_converse.bosh_session !== undefined) { - _converse.bosh_session.save({'jid': _converse.jid}); - } - }); - - api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.BOSH)); - - /************************ END Event Handlers ************************/ - - - /************************ BEGIN API ************************/ - Object.assign(api, { - /** - * This namespace lets you access the BOSH tokens - * - * @namespace api.tokens - * @memberOf api - */ - tokens: { - /** - * @method api.tokens.get - * @param { string } [id] The type of token to return ('rid' or 'sid'). - * @returns 'string' A token, either the RID or SID token depending on what's asked for. - * @example _converse.api.tokens.get('rid'); - */ - get (id) { - const connection = api.connection.get(); - if (!connection) { - return null; - } - if (id.toLowerCase() === 'rid') { - return connection.rid || connection._proto.rid; - } else if (id.toLowerCase() === 'sid') { - return connection.sid || connection._proto.sid; - } - } - } - }); - /************************ end api ************************/ - } -}); diff --git a/src/headless/plugins/bosh/api.js b/src/headless/plugins/bosh/api.js new file mode 100644 index 0000000000..d8b7728c9a --- /dev/null +++ b/src/headless/plugins/bosh/api.js @@ -0,0 +1,28 @@ +import api from '../../shared/api/index.js'; + +export default { + /** + * This API namespace lets you access the BOSH tokens + * @namespace api.tokens + * @memberOf api + */ + tokens: { + /** + * @method api.tokens.get + * @param {string} [id] The type of token to return ('rid' or 'sid'). + * @returns {string} A token, either the RID or SID token depending on what's asked for. + * @example _converse.api.tokens.get('rid'); + */ + get (id) { + const connection = api.connection.get(); + if (!connection) return null; + + if (id.toLowerCase() === 'rid') { + return connection.rid || connection._proto.rid; + } else if (id.toLowerCase() === 'sid') { + return connection.sid || connection._proto.sid; + } + } + } + +}; diff --git a/src/headless/plugins/bosh/index.js b/src/headless/plugins/bosh/index.js new file mode 100644 index 0000000000..f88eb0a75d --- /dev/null +++ b/src/headless/plugins/bosh/index.js @@ -0,0 +1,35 @@ +/** + * @copyright The Converse.js contributors + * @license Mozilla Public License (MPLv2) + * @description Converse.js plugin which add support for XEP-0206: XMPP Over BOSH + */ +import 'strophe.js/src/bosh'; +import { Strophe } from "strophe.js"; +import _converse from '../../shared/_converse.js'; +import api, { converse } from '../../shared/api/index.js'; +import bosh_api from './api.js'; +import { attemptPrebind, clearSession, saveJIDToSession} from './utils.js'; + + +converse.plugins.add('converse-bosh', { + + enabled () { + return !_converse.api.settings.get("blacklisted_plugins").includes('converse-bosh'); + }, + + initialize () { + api.settings.extend({ + bosh_service_url: undefined, + prebind_url: null + }); + + Object.assign(api, bosh_api); + + api.listen.on('clearSession', clearSession); + api.listen.on('setUserJID', saveJIDToSession); + api.listen.on('login', attemptPrebind); + api.listen.on('addClientFeatures', + () => api.disco.own.features.add(Strophe.NS.BOSH) + ); + } +}); diff --git a/src/headless/plugins/bosh/utils.js b/src/headless/plugins/bosh/utils.js new file mode 100644 index 0000000000..070515a9d1 --- /dev/null +++ b/src/headless/plugins/bosh/utils.js @@ -0,0 +1,128 @@ +/** + * @typedef {module:shared.api.user} LoginHookPayload + */ +import log from "../../log.js"; +import api from '../../shared/api/index.js'; +import _converse from "../../shared/_converse.js"; +import { Strophe } from "strophe.js"; +import { Model } from '@converse/skeletor'; +import { createStore } from '../../utils/storage.js'; +import { isTestEnv } from '../../utils/session.js'; +import { setUserJID, } from '../../utils/init.js'; +import { BOSH_WAIT, PREBIND } from '../../shared/constants.js'; + +const BOSH_SESSION_ID = 'converse.bosh-session'; + +let bosh_session; + +async function initBOSHSession () { + const id = BOSH_SESSION_ID; + if (!bosh_session) { + bosh_session = new Model({id}); + bosh_session.browserStorage = createStore(id, "session"); + await new Promise(resolve => bosh_session.fetch({'success': resolve, 'error': resolve})); + } + + let jid = _converse.session.get('jid'); + if (jid) { + if (bosh_session.get('jid') !== jid) { + jid = await setUserJID(jid); + bosh_session.clear({'silent': true }); + bosh_session.save({jid}); + } + } else { // Keepalive + const jid = bosh_session.get('jid'); + jid && await setUserJID(jid); + } + return bosh_session; +} + + +export function startNewPreboundBOSHSession () { + if (!api.settings.get('prebind_url')) { + throw new Error("startNewPreboundBOSHSession: If you use prebind then you MUST supply a prebind_url"); + } + const connection = api.connection.get(); + const xhr = new XMLHttpRequest(); + xhr.open('GET', api.settings.get('prebind_url'), true); + xhr.setRequestHeader('Accept', 'application/json, text/javascript'); + xhr.onload = async function (event) { + if (xhr.status >= 200 && xhr.status < 400) { + const data = JSON.parse(xhr.responseText); + const jid = await setUserJID(data.jid); + connection.attach( + jid, + data.sid, + data.rid, + connection.onConnectStatusChanged, + BOSH_WAIT + ); + } else { + xhr.onerror(event); + } + }; + xhr.onerror = function () { + api.connection.destroy(); + /** + * Triggered when fetching prebind tokens failed + * @event _converse#noResumeableBOSHSession + * @type { _converse } + * @example _converse.api.listen.on('noResumeableBOSHSession', _converse => { ... }); + */ + api.trigger('noResumeableBOSHSession', _converse); + }; + xhr.send(); +} + +/** + * @param {unknown} _ + * @param {LoginHookPayload} payload + */ +export async function attemptPrebind (_, payload) { + if (payload.success) return payload; + + const { automatic } = payload; + // See whether there is a BOSH session to re-attach to + if (await restoreBOSHSession()) { + return { ...payload, success: true }; + } else if (api.settings.get("authentication") === PREBIND && (!automatic || api.settings.get("auto_login"))) { + startNewPreboundBOSHSession(); + return { ...payload, success: true }; + } + return payload; +} + +export function saveJIDToSession() { + if (bosh_session !== undefined) { + bosh_session.save({'jid': _converse.session.get('jid')}); + } +} + +export function clearSession () { + if (bosh_session === undefined) { + // Remove manually, even if we don't have the corresponding + // model, to avoid trying to reconnect to a stale BOSH session + const id = BOSH_SESSION_ID; + sessionStorage.removeItem(id); + sessionStorage.removeItem(`${id}-${id}`); + } else { + bosh_session.destroy(); + bosh_session = undefined; + } +} + + +export async function restoreBOSHSession () { + const jid = (await initBOSHSession()).get('jid'); + const connection = api.connection.get(); + if (jid && (connection._proto instanceof Strophe.Bosh)) { + try { + connection.restore(jid, connection.onConnectStatusChanged); + return true; + } catch (e) { + !isTestEnv() && log.warn("Could not restore session for jid: "+jid+" Error message: "+e.message); + return false; + } + } + return false; +} diff --git a/src/headless/plugins/caps/tests/caps.js b/src/headless/plugins/caps/tests/caps.js index f12d0aa7dd..4af230d96b 100644 --- a/src/headless/plugins/caps/tests/caps.js +++ b/src/headless/plugins/caps/tests/caps.js @@ -8,7 +8,7 @@ describe("A sent presence stanza", function () { beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); - it("includes a entity capabilities node", + it("includes an entity capabilities node", mock.initConverse([], {}, async (_converse) => { await mock.waitForRoster(_converse, 'current', 0); diff --git a/src/headless/plugins/caps/utils.js b/src/headless/plugins/caps/utils.js index ec76604976..bbdbda6fec 100644 --- a/src/headless/plugins/caps/utils.js +++ b/src/headless/plugins/caps/utils.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder + */ import _converse from '../../shared/_converse.js'; import { converse } from '../../shared/api/index.js'; import { arrayBufferToBase64, stringToArrayBuffer } from '../../utils/arraybuffer.js'; @@ -38,7 +41,7 @@ async function createCapsNode () { /** * Given a stanza, adds a XEP-0115 CAPS element - * @param { Element } stanza + * @param {Strophe.Builder} stanza */ export async function addCapsNode (stanza) { const caps_el = await createCapsNode(); diff --git a/src/headless/plugins/chat/api.js b/src/headless/plugins/chat/api.js index c299a7b5a4..9a4836aaad 100644 --- a/src/headless/plugins/chat/api.js +++ b/src/headless/plugins/chat/api.js @@ -1,6 +1,10 @@ +/** + * @typedef {import('./model.js').default} ChatBox + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import log from "../../log.js"; +import { PRIVATE_CHAT_TYPE } from '../../shared/constants.js'; export default { @@ -13,8 +17,9 @@ export default { chats: { /** * @method api.chats.create - * @param {string|string[]} jid|jids An jid or array of jids - * @param { object } [attrs] An object containing configuration attributes. + * @param {string|string[]} jids An jid or array of jids + * @param {object} [attrs] An object containing configuration attributes. + * @returns {Promise} */ async create (jids, attrs) { if (typeof jids === 'string') { @@ -30,7 +35,7 @@ export default { return chatbox; } if (Array.isArray(jids)) { - return Promise.all(jids.forEach(async jid => { + return Promise.all(jids.map(async jid => { const contact = await api.contacts.get(jids); attrs.fullname = contact?.attributes?.fullname; return api.chats.get(jid, attrs, true).maybeShow(); @@ -44,10 +49,10 @@ export default { * Opens a new one-on-one chat. * * @method api.chats.open - * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] - * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model. - * @param { Boolean } [attrs.minimized] - Should the chat be created in minimized state. - * @param { Boolean } [force=false] - By default, a minimized + * @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] + * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model. + * @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state. + * @param {Boolean} [force=false] - By default, a minimized * chat won't be maximized (in `overlayed` view mode) and in * `fullscreen` view mode a newly opened chat won't replace * another chat already in the foreground. @@ -105,7 +110,7 @@ export default { * @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model. * @param { Boolean } [create=false] - Whether the chat should be created if it's not found. - * @returns { Promise<_converse.ChatBox> } + * @returns { Promise } * * @example * // To return a single chat, provide the JID of the contact you're chatting with in that chat: @@ -123,12 +128,13 @@ export default { async get (jids, attrs={}, create=false) { await api.waitUntil('chatBoxesFetched'); + /** @param {string} jid */ async function _get (jid) { let model = await api.chatboxes.get(jid); if (!model && create) { - model = await api.chatboxes.create(jid, attrs, _converse.ChatBox); + model = await api.chatboxes.create(jid, attrs, _converse.exports.ChatBox); } else { - model = (model && model.get('type') === _converse.PRIVATE_CHAT_TYPE) ? model : null; + model = (model && model.get('type') === PRIVATE_CHAT_TYPE) ? model : null; if (model && Object.keys(attrs).length) { model.save(attrs); } @@ -137,7 +143,7 @@ export default { } if (jids === undefined) { const chats = await api.chatboxes.get(); - return chats.filter(c => (c.get('type') === _converse.PRIVATE_CHAT_TYPE)); + return chats.filter(c => (c.get('type') === PRIVATE_CHAT_TYPE)); } else if (typeof jids === 'string') { return _get(jids); } diff --git a/src/headless/plugins/chat/index.js b/src/headless/plugins/chat/index.js index bcc5a3616f..5fd0ae541c 100644 --- a/src/headless/plugins/chat/index.js +++ b/src/headless/plugins/chat/index.js @@ -8,6 +8,7 @@ import Messages from './messages.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import chat_api from './api.js'; +import { PRIVATE_CHAT_TYPE } from '../../shared/constants.js'; import { autoJoinChats, enableCarbons, @@ -38,13 +39,13 @@ converse.plugins.add('converse-chat', { 'send_chat_state_notifications': true, }); - Object.assign(_converse, { ChatBox, Message, Messages, handleMessageStanza }); + const exports = { ChatBox, Message, Messages, handleMessageStanza }; + Object.assign(_converse, exports); // TODO: DEPRECATED + Object.assign(_converse.exports, exports); + Object.assign(api, chat_api); - api.chatboxes.registry.add( - _converse.PRIVATE_CHAT_TYPE, - ChatBox - ); + api.chatboxes.registry.add(PRIVATE_CHAT_TYPE, ChatBox); routeToChat(); addEventListener('hashchange', routeToChat); diff --git a/src/headless/plugins/chat/message.js b/src/headless/plugins/chat/message.js index 26637f3eb8..001f347a05 100644 --- a/src/headless/plugins/chat/message.js +++ b/src/headless/plugins/chat/message.js @@ -1,9 +1,13 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import ModelWithContact from './model-with-contact.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import dayjs from 'dayjs'; import log from '../../log.js'; import { getOpenPromise } from '@converse/openpromise'; +import { SUCCESS, FAILURE } from '../../shared/constants.js'; const { Strophe, sizzle, u } = converse.env; @@ -16,7 +20,7 @@ const { Strophe, sizzle, u } = converse.env; */ class Message extends ModelWithContact { - defaults () { // eslint-disable-line class-methods-use-this + defaults () { return { 'msgid': u.getUniqueId(), 'time': new Date().toISOString(), @@ -24,12 +28,19 @@ class Message extends ModelWithContact { }; } + /** + * @param {Model[]} [models] + * @param {object} [options] + */ + constructor (models, options) { + super(models, options); + this.file = null; + } + async initialize () { super.initialize(); + if (!this.checkValidity()) return; - if (!this.checkValidity()) { - return; - } this.initialized = getOpenPromise(); if (this.get('file')) { this.on('change:put', () => this.uploadFile()); @@ -41,9 +52,9 @@ class Message extends ModelWithContact { await this.setContact(); this.setTimerForEphemeralMessage(); /** - * Triggered once a {@link _converse.Message} has been created and initialized. + * Triggered once a {@link Message} has been created and initialized. * @event _converse#messageInitialized - * @type { _converse.Message} + * @type {Message} * @example _converse.api.listen.on('messageInitialized', model => { ... }); */ await api.trigger('messageInitialized', this, { 'Synchronous': true }); @@ -53,13 +64,12 @@ class Message extends ModelWithContact { setContact () { if (['chat', 'normal'].includes(this.get('type'))) { ModelWithContact.prototype.initialize.apply(this, arguments); - this.setRosterContact(Strophe.getBareJidFromJid(this.get('from'))); + return this.setRosterContact(Strophe.getBareJidFromJid(this.get('from'))); } } /** * Sets an auto-destruct timer for this message, if it's is_ephemeral. - * @private * @method _converse.Message#setTimerForEphemeralMessage */ setTimerForEphemeralMessage () { @@ -179,12 +189,11 @@ class Message extends ModelWithContact { * @method _converse.Message#sendSlotRequestStanza */ sendSlotRequestStanza () { - if (!this.file) { - return Promise.reject(new Error('file is undefined')); - } + if (!this.file) return Promise.reject(new Error('file is undefined')); + const iq = converse.env .$iq({ - 'from': _converse.jid, + 'from': _converse.session.get('jid'), 'to': this.get('slot_request_url'), 'type': 'get' }) @@ -241,12 +250,12 @@ class Message extends ModelWithContact { uploadFile () { const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = async () => { + xhr.onreadystatechange = async (event) => { if (xhr.readyState === XMLHttpRequest.DONE) { log.info('Status: ' + xhr.status); if (xhr.status === 200 || xhr.status === 201) { let attrs = { - 'upload': _converse.SUCCESS, + 'upload': SUCCESS, 'oob_url': this.get('get'), 'message': this.get('get'), 'body': this.get('get'), @@ -259,7 +268,8 @@ class Message extends ModelWithContact { attrs = await api.hook('afterFileUploaded', this, attrs); this.save(attrs); } else { - xhr.onerror(); + log.error(event); + xhr.onerror(new ProgressEvent(`Response status: ${xhr.status}`)); } } }; @@ -287,7 +297,7 @@ class Message extends ModelWithContact { } this.save({ 'type': 'error', - 'upload': _converse.FAILURE, + 'upload': FAILURE, 'message': message, 'is_ephemeral': true }); diff --git a/src/headless/plugins/chat/messages.js b/src/headless/plugins/chat/messages.js index 04a0ed9ad1..ef1afa1840 100644 --- a/src/headless/plugins/chat/messages.js +++ b/src/headless/plugins/chat/messages.js @@ -1,16 +1,14 @@ import Message from './message.js'; -import { Collection } from '@converse/skeletor/src/collection'; +import { Collection } from '@converse/skeletor'; class Messages extends Collection { - // eslint-disable-next-line class-methods-use-this - get comparator () { - return 'time'; - } - constructor () { super(); + this.comparator = 'time'; this.model = Message; + this.fetched = null; + this.chatbox = null; } } diff --git a/src/headless/plugins/chat/model-with-contact.js b/src/headless/plugins/chat/model-with-contact.js index abac7ab092..110d29a9f0 100644 --- a/src/headless/plugins/chat/model-with-contact.js +++ b/src/headless/plugins/chat/model-with-contact.js @@ -1,5 +1,5 @@ import api from "../../shared/api/index.js"; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; class ModelWithContact extends Model { @@ -7,8 +7,13 @@ class ModelWithContact extends Model { initialize () { super.initialize(); this.rosterContactAdded = getOpenPromise(); + this.contact = null; + this.vcard = null; } + /** + * @param {string} jid + */ async setRosterContact (jid) { const contact = await api.contacts.get(jid); if (contact) { diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index ca00e1525c..09280310d4 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -1,10 +1,18 @@ +/** + * @typedef {import('./message.js').default} Message + * @typedef {import('../muc/muc.js').default} MUC + * @typedef {import('../muc/message.js').default} MUCMessage + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder + */ import ModelWithContact from './model-with-contact.js'; +import _converse from '../../shared/_converse.js'; +import api, { converse } from '../../shared/api/index.js'; import isMatch from "lodash-es/isMatch"; import log from '../../log.js'; import pick from "lodash-es/pick"; -import _converse from '../../shared/_converse.js'; -import api, { converse } from '../../shared/api/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; +import { ACTIVE, PRIVATE_CHAT_TYPE, COMPOSING, INACTIVE, PAUSED, SUCCESS, GONE } from '@converse/headless/shared/constants.js'; import { TimeoutError } from '../../shared/errors.js'; import { debouncedPruneHistory, handleCorrection } from '../../shared/chat/utils.js'; import { filesize } from "filesize"; @@ -12,18 +20,17 @@ import { getMediaURLsMetadata } from '../../shared/parsers.js'; import { getOpenPromise } from '@converse/openpromise'; import { initStorage } from '../../utils/storage.js'; import { isEmptyMessage } from '../../utils/index.js'; +import { isNewMessage } from './utils.js'; import { isUniView } from '../../utils/session.js'; import { parseMessage } from './parsers.js'; import { sendMarker } from '../../shared/actions.js'; -import { isNewMessage } from './utils.js'; -const { Strophe, $msg } = converse.env; +const { Strophe, $msg, u } = converse.env; -const u = converse.env.utils; /** * Represents an open/ongoing chat conversation. - * @namespace _converse.ChatBox + * @namespace ChatBox * @memberOf _converse */ class ChatBox extends ModelWithContact { @@ -31,18 +38,20 @@ class ChatBox extends ModelWithContact { defaults () { return { 'bookmarked': false, - 'chat_state': undefined, 'hidden': isUniView() && !api.settings.get('singleton'), 'message_type': 'chat', - 'nickname': undefined, 'num_unread': 0, 'time_opened': this.get('time_opened') || (new Date()).getTime(), 'time_sent': (new Date(0)).toISOString(), - 'type': _converse.PRIVATE_CHAT_TYPE, - 'url': '' + 'type': PRIVATE_CHAT_TYPE, } } + constructor (attrs, options) { + super(attrs, options); + this.disable_mam = false; + } + async initialize () { super.initialize(); this.initialized = getOpenPromise(); @@ -62,8 +71,9 @@ class ChatBox extends ModelWithContact { this.initUI(); this.initMessages(); - if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) { - this.presence = _converse.presences.get(jid) || _converse.presences.create({ jid }); + if (this.get('type') === PRIVATE_CHAT_TYPE) { + const { presences } = _converse.state; + this.presence = presences.get(jid) || presences.create({ jid }); await this.setRosterContact(jid); this.presence.on('change:show', item => this.onPresenceChanged(item)); } @@ -72,9 +82,9 @@ class ChatBox extends ModelWithContact { await this.fetchMessages(); /** - * Triggered once a {@link _converse.ChatBox} has been created and initialized. + * Triggered once a {@link ChatBox} has been created and initialized. * @event _converse#chatBoxInitialized - * @type { _converse.ChatBox} + * @type { ChatBox} * @example _converse.api.listen.on('chatBoxInitialized', model => { ... }); */ await api.trigger('chatBoxInitialized', this, {'Synchronous': true}); @@ -82,11 +92,11 @@ class ChatBox extends ModelWithContact { } getMessagesCollection () { - return new _converse.Messages(); + return new _converse.exports.Messages(); } getMessagesCacheKey () { - return `converse.messages-${this.get('jid')}-${_converse.bare_jid}`; + return `converse.messages-${this.get('jid')}-${_converse.session.get('bare_jid')}`; } initMessages () { @@ -95,8 +105,8 @@ class ChatBox extends ModelWithContact { this.messages.chatbox = this; initStorage(this.messages, this.getMessagesCacheKey()); - this.listenTo(this.messages, 'change:upload', this.onMessageUploadChanged, this); - this.listenTo(this.messages, 'add', this.onMessageAdded, this); + this.listenTo(this.messages, 'change:upload', m => this.onMessageUploadChanged(m)); + this.listenTo(this.messages, 'add', m => this.onMessageAdded(m)); } initUI () { @@ -109,11 +119,11 @@ class ChatBox extends ModelWithContact { getNotificationsText () { const { __ } = _converse; - if (this.notifications?.get('chat_state') === _converse.COMPOSING) { + if (this.notifications?.get('chat_state') === COMPOSING) { return __('%1$s is typing', this.getDisplayName()); - } else if (this.notifications?.get('chat_state') === _converse.PAUSED) { + } else if (this.notifications?.get('chat_state') === PAUSED) { return __('%1$s has stopped typing', this.getDisplayName()); - } else if (this.notifications?.get('chat_state') === _converse.GONE) { + } else if (this.notifications?.get('chat_state') === GONE) { return __('%1$s has gone away', this.getDisplayName()); } else { return ''; @@ -123,10 +133,10 @@ class ChatBox extends ModelWithContact { afterMessagesFetched () { this.pruneHistoryWhenScrolledDown(); /** - * Triggered whenever a { @link _converse.ChatBox } or ${ @link _converse.ChatRoom } + * Triggered whenever a { @link ChatBox } or ${ @link MUC } * has fetched its messages from the local cache. * @event _converse#afterMessagesFetched - * @type { _converse.ChatBox| _converse.ChatRoom } + * @type { ChatBox| MUC } * @example _converse.api.listen.on('afterMessagesFetched', (chat) => { ... }); */ api.trigger('afterMessagesFetched', this); @@ -141,7 +151,7 @@ class ChatBox extends ModelWithContact { const resolve = this.messages.fetched.resolve; this.messages.fetch({ 'add': true, - 'success': msgs => { this.afterMessagesFetched(msgs); resolve() }, + 'success': () => { this.afterMessagesFetched(); resolve() }, 'error': () => { this.afterMessagesFetched(); resolve() } }); return this.messages.fetched; @@ -149,7 +159,7 @@ class ChatBox extends ModelWithContact { async handleErrorMessageStanza (stanza) { const { __ } = _converse; - const attrs = await parseMessage(stanza, _converse); + const attrs = await parseMessage(stanza); if (!await this.shouldShowErrorMessage(attrs)) { return; } @@ -188,9 +198,8 @@ class ChatBox extends ModelWithContact { /** * Queue an incoming `chat` message stanza for processing. * @async - * @private - * @method _converse.ChatBox#queueMessage - * @param { Promise } attrs - A promise which resolves to the message attributes + * @method ChatBox#queueMessage + * @param {MessageAttributes} attrs - A promise which resolves to the message attributes */ queueMessage (attrs) { this.msg_chain = (this.msg_chain || this.messages.fetched) @@ -201,12 +210,11 @@ class ChatBox extends ModelWithContact { /** * @async - * @private - * @method _converse.ChatBox#onMessage - * @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes. + * @method ChatBox#onMessage + * @param {Promise} attrs_promise - A promise which resolves to the message attributes. */ - async onMessage (attrs) { - attrs = await attrs; + async onMessage (attrs_promise) { + const attrs = await attrs_promise; if (u.isErrorObject(attrs)) { attrs.stanza && log.error(attrs.stanza); return log.error(attrs.message); @@ -233,7 +241,7 @@ class ChatBox extends ModelWithContact { } async onMessageUploadChanged (message) { - if (message.get('upload') === _converse.SUCCESS) { + if (message.get('upload') === SUCCESS) { const attrs = { 'body': message.get('body'), 'spoiler_hint': message.get('spoiler_hint'), @@ -271,7 +279,7 @@ class ChatBox extends ModelWithContact { if (api.connection.connected()) { // Immediately sending the chat state, because the // model is going to be destroyed afterwards. - this.setChatState(_converse.INACTIVE); + this.setChatState(INACTIVE); this.sendChatState(); } try { @@ -288,7 +296,7 @@ class ChatBox extends ModelWithContact { /** * Triggered once a chatbox has been closed. * @event _converse#chatBoxClosed - * @type {_converse.ChatBox | _converse.ChatRoom} + * @type {ChatBox | MUC} * @example _converse.api.listen.on('chatBoxClosed', chat => { ... }); */ api.trigger('chatBoxClosed', this); @@ -296,9 +304,9 @@ class ChatBox extends ModelWithContact { announceReconnection () { /** - * Triggered whenever a `_converse.ChatBox` instance has reconnected after an outage + * Triggered whenever a `ChatBox` instance has reconnected after an outage * @event _converse#onChatReconnected - * @type {_converse.ChatBox | _converse.ChatRoom} + * @type {ChatBox | MUC} * @example _converse.api.listen.on('onChatReconnected', chat => { ... }); */ api.trigger('chatReconnected', this); @@ -472,7 +480,7 @@ class ChatBox extends ModelWithContact { * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE. * See XEP-0085 Chat State Notifications. * @private - * @method _converse.ChatBox#setChatState + * @method ChatBox#setChatState * @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE) */ setChatState (state, options) { @@ -480,17 +488,17 @@ class ChatBox extends ModelWithContact { window.clearTimeout(this.chat_state_timeout); delete this.chat_state_timeout; } - if (state === _converse.COMPOSING) { + if (state === COMPOSING) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, - _converse.PAUSED + PAUSED ); - } else if (state === _converse.PAUSED) { + } else if (state === PAUSED) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, - _converse.INACTIVE + INACTIVE ); } this.set('chat_state', state, options); @@ -500,7 +508,7 @@ class ChatBox extends ModelWithContact { /** * Given an error `` stanza's attributes, find the saved message model which is * referenced by that error. - * @param { Object } attrs + * @param {object} attrs */ getMessageReferencedByError (attrs) { const id = attrs.msgid; @@ -508,9 +516,9 @@ class ChatBox extends ModelWithContact { } /** - * @private - * @method _converse.ChatBox#shouldShowErrorMessage - * @returns {boolean} + * @method ChatBox#shouldShowErrorMessage + * @param {object} attrs + * @returns {Promise} */ shouldShowErrorMessage (attrs) { const msg = this.getMessageReferencedByError(attrs); @@ -521,10 +529,15 @@ class ChatBox extends ModelWithContact { // See https://github.com/conversejs/converse.js/issues/1317 return; } - // Gets overridden in ChatRoom - return true; + // Gets overridden in MUC + // Return promise because subclasses need to return promises + return Promise.resolve(true); } + /** + * @param {string} jid1 + * @param {string} jid2 + */ isSameUser (jid1, jid2) { return u.isSameBareJID(jid1, jid2); } @@ -535,10 +548,10 @@ class ChatBox extends ModelWithContact { * probably hasn't been applied to anything yet, given that the * relevant message is only coming in now. * @private - * @method _converse.ChatBox#findDanglingRetraction + * @method ChatBox#findDanglingRetraction * @param { object } attrs - Attributes representing a received * message, as returned by {@link parseMessage} - * @returns { _converse.Message } + * @returns { Message } */ findDanglingRetraction (attrs) { if (!attrs.origin_id || !this.messages.length) { @@ -561,11 +574,10 @@ class ChatBox extends ModelWithContact { /** * Handles message retraction based on the passed in attributes. - * @private - * @method _converse.ChatBox#handleRetraction - * @param { object } attrs - Attributes representing a received + * @method ChatBox#handleRetraction + * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMessage} - * @returns { Boolean } Returns `true` or `false` depending on + * @returns {Promise} Returns `true` or `false` depending on * whether a message was retracted or not. */ async handleRetraction (attrs) { @@ -599,11 +611,10 @@ class ChatBox extends ModelWithContact { /** * Returns an already cached message (if it exists) based on the * passed in attributes map. - * @private - * @method _converse.ChatBox#getDuplicateMessage + * @method ChatBox#getDuplicateMessage * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMessage} - * @returns {Promise<_converse.Message>} + * @returns {Message} */ getDuplicateMessage (attrs) { const queries = [ @@ -647,9 +658,8 @@ class ChatBox extends ModelWithContact { /** * Retract one of your messages in this chat - * @private - * @method _converse.ChatBoxView#retractOwnMessage - * @param { _converse.Message } message - The message which we're retracting. + * @method ChatBoxView#retractOwnMessage + * @param { Message } message - The message which we're retracting. */ retractOwnMessage (message) { this.sendRetractionMessage(message) @@ -665,8 +675,8 @@ class ChatBox extends ModelWithContact { /** * Sends a message stanza to retract a message in this chat * @private - * @method _converse.ChatBox#sendRetractionMessage - * @param { _converse.Message } message - The message which we're retracting. + * @method ChatBox#sendRetractionMessage + * @param { Message } message - The message which we're retracting. */ sendRetractionMessage (message) { const origin_id = message.get('origin_id'); @@ -701,7 +711,7 @@ class ChatBox extends ModelWithContact { /** * Given the passed in message object, send a XEP-0333 chat marker. - * @param { _converse.Message } msg + * @param { Message } msg * @param { ('received'|'displayed'|'acknowledged') } [type='displayed'] * @param { Boolean } force - Whether a marker should be sent for the * message, even if it didn't include a `markable` element. @@ -718,7 +728,7 @@ class ChatBox extends ModelWithContact { handleChatMarker (attrs) { const to_bare_jid = Strophe.getBareJidFromJid(attrs.to); - if (to_bare_jid !== _converse.bare_jid) { + if (to_bare_jid !== _converse.session.get('bare_jid')) { return false; } if (attrs.is_markable) { @@ -763,9 +773,9 @@ class ChatBox extends ModelWithContact { } /** - * Given a {@link _converse.Message} return the XML stanza that represents it. + * Given a {@link Message} return the XML stanza that represents it. * @private - * @method _converse.ChatBox#createMessageStanza + * @method ChatBox#createMessageStanza * @param { Message } message - The message object */ async createMessageStanza (message) { @@ -775,7 +785,7 @@ class ChatBox extends ModelWithContact { 'type': this.get('message_type'), 'id': message.get('edited') && u.getUniqueId() || message.get('msgid'), }).c('body').t(message.get('body')).up() - .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root(); + .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root(); if (message.get('type') === 'chat') { stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root(); @@ -821,10 +831,10 @@ class ChatBox extends ModelWithContact { /** * *Hook* which allows plugins to update an outgoing message stanza * @event _converse#createMessageStanza - * @param { _converse.ChatBox | _converse.ChatRoom } - The chat from + * @param { ChatBox | MUC } chat - The chat from * which this message stanza is being sent. * @param { Object } data - Message data - * @param { _converse.Message | _converse.ChatRoomMessage } data.message + * @param { Message | MUCMessage } data.message * The message object from which the stanza is created and which gets persisted to storage. * @param { Strophe.Builder } data.stanza * The stanza that will be sent out, as a Strophe.Builder object. @@ -842,8 +852,8 @@ class ChatBox extends ModelWithContact { const text = attrs?.body; const body = text ? u.shortnamesToUnicode(text) : undefined; attrs = Object.assign({}, attrs, { - 'from': _converse.bare_jid, - 'fullname': _converse.xmppstatus.get('fullname'), + 'from': _converse.session.get('bare_jid'), + 'fullname': _converse.state.xmppstatus.get('fullname'), 'id': origin_id, 'is_only_emojis': text ? u.isOnlyEmojis(text) : false, 'jid': this.get('jid'), @@ -860,12 +870,12 @@ class ChatBox extends ModelWithContact { /** * *Hook* which allows plugins to update the attributes of an outgoing message. - * These attributes get set on the { @link _converse.Message } or - * { @link _converse.ChatRoomMessage } and persisted to storage. + * These attributes get set on the {@link Message} or + * {@link MUCMessage} and persisted to storage. * @event _converse#getOutgoingMessageAttributes - * @param { _converse.ChatBox | _converse.ChatRoom } chat + * @param {ChatBox|MUC} chat * The chat from which this message will be sent. - * @param { MessageAttributes } attrs + * @param {MessageAttributes} attrs * The message attributes, from which the stanza will be created. */ attrs = await api.hook('getOutgoingMessageAttributes', this, attrs); @@ -877,10 +887,10 @@ class ChatBox extends ModelWithContact { * If api.settings.get('allow_message_corrections') is "last", then only the last * message sent from me will be editable. If set to "all" all messages * will be editable. Otherwise no messages will be editable. - * @method _converse.ChatBox#setEditable - * @memberOf _converse.ChatBox - * @param { Object } attrs An object containing message attributes. - * @param { String } send_time - time when the message was sent + * @method ChatBox#setEditable + * @memberOf ChatBox + * @param {Object} attrs An object containing message attributes. + * @param {String} send_time - time when the message was sent */ setEditable (attrs, send_time) { if (attrs.is_headline || isEmptyMessage(attrs) || attrs.sender !== 'me') { @@ -899,10 +909,8 @@ class ChatBox extends ModelWithContact { * Queue the creation of a message, to make sure that we don't run * into a race condition whereby we're creating a new message * before the collection has been fetched. - * @async - * @private - * @method _converse.ChatBox#createMessage - * @param { Object } attrs + * @method ChatBox#createMessage + * @param {Object} attrs */ async createMessage (attrs, options) { attrs.time = attrs.time || (new Date()).toISOString(); @@ -912,11 +920,10 @@ class ChatBox extends ModelWithContact { /** * Responsible for sending off a text message inside an ongoing chat conversation. - * @private - * @method _converse.ChatBox#sendMessage - * @memberOf _converse.ChatBox - * @param { Object } [attrs] - A map of attributes to be saved on the message - * @returns { _converse.Message } + * @method ChatBox#sendMessage + * @memberOf ChatBox + * @param {Object} [attrs] - A map of attributes to be saved on the message + * @returns {Promise} * @example * const chat = api.chats.get('buddy1@example.org'); * chat.sendMessage({'body': 'hello world'}); @@ -961,8 +968,8 @@ class ChatBox extends ModelWithContact { * @event _converse#sendMessage * @type { Object } * @param { Object } data - * @property { (_converse.ChatBox | _converse.ChatRoom) } data.chatbox - * @property { (_converse.Message | _converse.ChatRoomMessage) } data.message + * @property { (ChatBox | MUC) } data.chatbox + * @property { (Message | MUCMessage) } data.message */ api.trigger('sendMessage', {'chatbox': this, message}); return message; @@ -970,9 +977,8 @@ class ChatBox extends ModelWithContact { /** * Sends a message with the current XEP-0085 chat state of the user - * as taken from the `chat_state` attribute of the {@link _converse.ChatBox}. - * @private - * @method _converse.ChatBox#sendChatState + * as taken from the `chat_state` attribute of the {@link ChatBox}. + * @method ChatBox#sendChatState */ sendChatState () { if (api.settings.get('send_chat_state_notifications') && this.get('chat_state')) { @@ -993,9 +999,12 @@ class ChatBox extends ModelWithContact { } + /** + * @param {File[]} files + */ async sendFiles (files) { - const { __ } = _converse; - const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain); + const { __, session } = _converse; + const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, session.get('domain')); const item = result.pop(); if (!item) { this.createMessage({ @@ -1022,14 +1031,12 @@ class ChatBox extends ModelWithContact { * *Hook* which allows plugins to transform files before they'll be * uploaded. The main use-case is to encrypt the files. * @event _converse#beforeFileUpload - * @param { _converse.ChatBox | _converse.ChatRoom } chat - * The chat from which this file will be uploaded. - * @param { File } file - * The file that will be uploaded + * @param {ChatBox|MUC} chat - The chat from which this file will be uploaded. + * @param {File} file - The file that will be uploaded */ file = await api.hook('beforeFileUpload', this, file); - if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) { + if (!window.isNaN(max_file_size) && file.size > max_file_size) { return this.createMessage({ 'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.', file.name, filesize(max_file_size)), @@ -1052,12 +1059,15 @@ class ChatBox extends ModelWithContact { }); } + /** + * @param {boolean} force + */ maybeShow (force) { if (isUniView()) { - const filter = c => !c.get('hidden') && + const filter = (c) => !c.get('hidden') && c.get('jid') !== this.get('jid') && c.get('id') !== 'controlbox'; - const other_chats = _converse.chatboxes.filter(filter); + const other_chats = _converse.state.chatboxes.filter(filter); if (force || other_chats.length === 0) { // We only have one chat visible at any one time. // So before opening a chat, we make sure all other chats are hidden. @@ -1078,16 +1088,14 @@ class ChatBox extends ModelWithContact { * @returns {boolean} */ isHidden () { - // Note: This methods gets overridden by converse-minimize - return this.get('hidden') || this.isScrolledUp() || _converse.windowState === 'hidden'; + return this.get('hidden') || this.isScrolledUp() || document.hidden; } /** - * Given a newly received {@link _converse.Message} instance, + * Given a newly received {@link Message} instance, * update the unread counter if necessary. - * @private - * @method _converse.ChatBox#handleUnreadMessage - * @param {_converse.Message} message + * @method ChatBox#handleUnreadMessage + * @param {Message} message */ handleUnreadMessage (message) { if (!message?.get('body')) { @@ -1108,6 +1116,9 @@ class ChatBox extends ModelWithContact { } } + /** + * @param {Message} message + */ incrementUnreadMsgsCounter (message) { const settings = { 'num_unread': this.get('num_unread') + 1 diff --git a/src/headless/plugins/chat/parsers.js b/src/headless/plugins/chat/parsers.js index 6fd8e0fa3a..0656ba7023 100644 --- a/src/headless/plugins/chat/parsers.js +++ b/src/headless/plugins/chat/parsers.js @@ -1,3 +1,7 @@ +/** + * @module:plugin-chat-parsers + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import dayjs from 'dayjs'; @@ -41,17 +45,19 @@ export async function parseMessage (stanza) { let to_jid = stanza.getAttribute('to'); const to_resource = Strophe.getResourceFromJid(to_jid); - if (api.settings.get('filter_by_resource') && to_resource && to_resource !== _converse.resource) { + const resource = _converse.session.get('resource'); + if (api.settings.get('filter_by_resource') && to_resource && to_resource !== resource) { return new StanzaParseError( `Ignoring incoming message intended for a different resource: ${to_jid}`, stanza ); } + const bare_jid = _converse.session.get('bare_jid'); const original_stanza = stanza; - let from_jid = stanza.getAttribute('from') || _converse.bare_jid; + let from_jid = stanza.getAttribute('from') || bare_jid; if (isCarbon(stanza)) { - if (from_jid === _converse.bare_jid) { + if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); @@ -65,7 +71,7 @@ export async function parseMessage (stanza) { const is_archived = isArchived(stanza); if (is_archived) { - if (from_jid === _converse.bare_jid) { + if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); @@ -79,7 +85,7 @@ export async function parseMessage (stanza) { } const from_bare_jid = Strophe.getBareJidFromJid(from_jid); - const is_me = from_bare_jid === _converse.bare_jid; + const is_me = from_bare_jid === bare_jid; if (is_me && to_jid === null) { return new StanzaParseError( `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, @@ -102,8 +108,8 @@ export async function parseMessage (stanza) { } } /** - * @typedef { Object } MessageAttributes * The object which {@link parseMessage} returns + * @typedef {Object} MessageAttributes * @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else * @property { Array } references - A list of objects representing XEP-0372 references * @property { Boolean } editable - Is this message editable via XEP-0308? @@ -186,12 +192,12 @@ export async function parseMessage (stanza) { getCorrectionAttributes(stanza, original_stanza), getStanzaIDs(stanza, original_stanza), getRetractionAttributes(stanza, original_stanza), - getEncryptionAttributes(stanza, _converse) + getEncryptionAttributes(stanza) ); if (attrs.is_archived) { const from = original_stanza.getAttribute('from'); - if (from && from !== _converse.bare_jid) { + if (from && from !== bare_jid) { return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza); } } diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js index fba8b96d49..7684d5bf13 100644 --- a/src/headless/plugins/chat/utils.js +++ b/src/headless/plugins/chat/utils.js @@ -1,11 +1,18 @@ +/** + * @module:headless-plugins-chat-utils + * @typedef {import('./model.js').default} ChatBox + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + * @typedef {import('strophe.js').Builder} Builder + */ import sizzle from "sizzle"; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from '../../log.js'; import { isArchived, isHeadline, isServerMessage, } from '../../shared/parsers'; import { parseMessage } from './parsers.js'; import { shouldClearCache } from '../../utils/session.js'; +import { CONTROLBOX_TYPE, PRIVATE_CHAT_TYPE } from "../../shared/constants.js"; const { Strophe, u } = converse.env; @@ -23,18 +30,23 @@ export function routeToChat (event) { export async function onClearSession () { if (shouldClearCache()) { + const { chatboxes } = _converse.state; await Promise.all( - _converse.chatboxes.map(c => c.messages && c.messages.clearStore({ 'silent': true })) + chatboxes.map(/** @param {ChatBox} c */(c) => c.messages?.clearStore({ 'silent': true })) ); - const filter = o => o.get('type') !== _converse.CONTROLBOX_TYPE; - _converse.chatboxes.clearStore({ 'silent': true }, filter); + chatboxes.clearStore( + { 'silent': true }, + /** @param {Model} o */(o) => o.get('type') !== CONTROLBOX_TYPE); } } + +/** + * Given a stanza, determine whether it's a new + * message, i.e. not a MAM archived one. + * @param {Element|Model|object} message + */ export function isNewMessage (message) { - /* Given a stanza, determine whether it's a new - * message, i.e. not a MAM archived one. - */ if (message instanceof Element) { return !( sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length && @@ -47,13 +59,17 @@ export function isNewMessage (message) { } +/** + * @param {Element} stanza + */ async function handleErrorMessage (stanza) { const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); - if (u.isSameBareJID(from_jid, _converse.bare_jid)) { + const bare_jid = _converse.session.get('bare_jid'); + if (u.isSameBareJID(from_jid, bare_jid)) { return; } const chatbox = await api.chatboxes.get(from_jid); - if (chatbox?.get('type') === _converse.PRIVATE_CHAT_TYPE) { + if (chatbox?.get('type') === PRIVATE_CHAT_TYPE) { chatbox?.handleErrorMessageStanza(stanza); } } @@ -61,8 +77,8 @@ async function handleErrorMessage (stanza) { export function autoJoinChats () { // Automatically join private chats, based on the // "auto_join_private_chats" configuration setting. - api.settings.get('auto_join_private_chats').forEach(jid => { - if (_converse.chatboxes.where({ 'jid': jid }).length) { + api.settings.get('auto_join_private_chats').forEach(/** @param {string} jid */(jid) => { + if (_converse.state.chatboxes.where({ 'jid': jid }).length) { return; } if (typeof jid === 'string') { @@ -84,7 +100,8 @@ export function autoJoinChats () { export function registerMessageHandlers () { api.connection.get().addHandler( - stanza => { + /** @param {Element} stanza */ + (stanza) => { if ( ['groupchat', 'error'].includes(stanza.getAttribute('type')) || isHeadline(stanza) || @@ -93,14 +110,18 @@ export function registerMessageHandlers () { ) { return true; } - return _converse.handleMessageStanza(stanza) || true; + return _converse.exports.handleMessageStanza(stanza) || true; }, null, 'message', ); api.connection.get().addHandler( - stanza => handleErrorMessage(stanza) || true, + /** @param {Element} stanza */ + (stanza) => { + handleErrorMessage(stanza); + return true; + }, null, 'message', 'error' @@ -110,10 +131,10 @@ export function registerMessageHandlers () { /** * Handler method for all incoming single-user chat "message" stanzas. - * @param { MessageAttributes } attrs - The message attributes + * @param {Element|Builder} stanza */ export async function handleMessageStanza (stanza) { - stanza = stanza.tree?.() ?? stanza; + stanza = (stanza instanceof Element) ? stanza : stanza.tree(); if (isServerMessage(stanza)) { // Prosody sends headline messages with type `chat`, so we need to filter them out here. @@ -135,19 +156,18 @@ export async function handleMessageStanza (stanza) { const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body); await chatbox?.queueMessage(attrs); /** - * @typedef { Object } MessageData + * @typedef {Object} MessageData * An object containing the original message stanza, as well as the * parsed attributes. - * @property { Element } stanza - * @property { MessageAttributes } stanza - * @property { ChatBox } chatbox + * @property {Element} stanza + * @property {MessageAttributes} stanza + * @property {ChatBox} chatbox */ const data = { stanza, attrs, chatbox }; /** * Triggered when a message stanza is been received and processed. * @event _converse#message - * @type { object } - * @property { module:converse-chat~MessageData } data + * @type {MessageData} data */ api.trigger('message', data); } @@ -155,10 +175,10 @@ export async function handleMessageStanza (stanza) { /** * Ask the XMPP server to enable Message Carbons * See [XEP-0280](https://xmpp.org/extensions/xep-0280.html#enabling) - * @param { Boolean } reconnecting */ export async function enableCarbons () { - const domain = Strophe.getDomainFromJid(_converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + const domain = Strophe.getDomainFromJid(bare_jid); const supported = await api.disco.supports(Strophe.NS.CARBONS, domain); if (!supported) { diff --git a/src/headless/plugins/chatboxes/api.js b/src/headless/plugins/chatboxes/api.js index d79aff6e40..0f5e3842d8 100644 --- a/src/headless/plugins/chatboxes/api.js +++ b/src/headless/plugins/chatboxes/api.js @@ -1,10 +1,13 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + * @typedef {import('../chat/model.js').default} ChatBox + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { createChatBox } from './utils.js'; const _chatBoxTypes = {}; -/** @typedef {import('@converse/skeletor').Model} Model */ /** * The "chatboxes" namespace. @@ -17,7 +20,7 @@ export default { * @method api.chatboxes.create * @param {string|string[]} jids - A JID or array of JIDs * @param {Object} attrs An object containing configuration attributes - * @param {Model} model - The type of chatbox that should be created + * @param {new (attrs: object, options: object) => ChatBox} model - The type of chatbox that should be created */ async create (jids=[], attrs={}, model) { await api.waitUntil('chatBoxesFetched'); @@ -34,13 +37,14 @@ export default { */ async get (jids) { await api.waitUntil('chatBoxesFetched'); + const { chatboxes } = _converse.state; if (jids === undefined) { - return _converse.chatboxes.models; + return chatboxes.models; } else if (typeof jids === 'string') { - return _converse.chatboxes.get(jids.toLowerCase()); + return chatboxes.get(jids.toLowerCase()); } else { jids = jids.map(j => j.toLowerCase()); - return _converse.chatboxes.models.filter(m => jids.includes(m.get('jid'))); + return chatboxes.models.filter(m => jids.includes(m.get('jid'))); } }, diff --git a/src/headless/plugins/chatboxes/chatboxes.js b/src/headless/plugins/chatboxes/chatboxes.js index 79ea7a5082..a7a5c61e9d 100644 --- a/src/headless/plugins/chatboxes/chatboxes.js +++ b/src/headless/plugins/chatboxes/chatboxes.js @@ -1,13 +1,24 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; import { initStorage } from '../../utils/storage.js'; class ChatBoxes extends Collection { - get comparator () { - return 'time_opened'; + + /** + * @param {Model[]} models + * @param {object} options + */ + constructor (models, options) { + super(models, Object.assign({ comparator: 'time_opened' }, options)); } + /** + * @param {Collection} collection + */ onChatBoxesFetched (collection) { collection.filter(c => !c.isValid()).forEach(c => c.destroy()); /** @@ -22,15 +33,24 @@ class ChatBoxes extends Collection { api.trigger('chatBoxesFetched'); } + /** + * @param {boolean} reconnecting + */ onConnected (reconnecting) { - if (reconnecting) { return; } - initStorage(this, `converse.chatboxes-${_converse.bare_jid}`); + if (reconnecting) return; + + const bare_jid = _converse.session.get('bare_jid'); + initStorage(this, `converse.chatboxes-${bare_jid}`); this.fetch({ 'add': true, 'success': c => this.onChatBoxesFetched(c) }); } + /** + * @param {object} attrs + * @param {object} options + */ createModel (attrs, options) { if (!attrs.type) { throw new Error("You need to specify a type of chatbox to be created"); diff --git a/src/headless/plugins/chatboxes/index.js b/src/headless/plugins/chatboxes/index.js index 5b6fd1e40e..5b2077fb1f 100644 --- a/src/headless/plugins/chatboxes/index.js +++ b/src/headless/plugins/chatboxes/index.js @@ -23,10 +23,10 @@ converse.plugins.add('converse-chatboxes', { 'privateChatsAutoJoined' ]); - Object.assign(api, { 'chatboxes': chatboxes_api}); - - _converse.ChatBoxes = ChatBoxes; + Object.assign(api, { chatboxes: chatboxes_api}); + Object.assign(_converse, { ChatBoxes }); // TODO: DEPRECATED + Object.assign(_converse.exports, { ChatBoxes }); api.listen.on('addClientFeatures', () => { api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT); @@ -34,8 +34,13 @@ converse.plugins.add('converse-chatboxes', { api.disco.own.features.add(Strophe.NS.OUTOFBAND); }); + let chatboxes; + api.listen.on('pluginsInitialized', () => { - _converse.chatboxes = new _converse.ChatBoxes(); + chatboxes = new _converse.exports.ChatBoxes(); + Object.assign(_converse, { chatboxes }); // TODO: DEPRECATED + Object.assign(_converse.state, { chatboxes }); + /** * Triggered once the _converse.ChatBoxes collection has been initialized. * @event _converse#chatBoxesInitialized @@ -45,7 +50,7 @@ converse.plugins.add('converse-chatboxes', { api.trigger('chatBoxesInitialized'); }); - api.listen.on('presencesInitialized', (reconnecting) => _converse.chatboxes.onConnected(reconnecting)); - api.listen.on('reconnected', () => _converse.chatboxes.forEach(m => m.onReconnection())); + api.listen.on('presencesInitialized', (reconnecting) => chatboxes.onConnected(reconnecting)); + api.listen.on('reconnected', () => chatboxes.forEach(m => m.onReconnection())); } }); diff --git a/src/headless/plugins/chatboxes/utils.js b/src/headless/plugins/chatboxes/utils.js index 5f8d71c63e..351328df97 100644 --- a/src/headless/plugins/chatboxes/utils.js +++ b/src/headless/plugins/chatboxes/utils.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('../chat/model.js').default} ChatBox + */ import _converse from '../../shared/_converse.js'; import { converse } from '../../shared/api/index.js'; import log from "../../log"; @@ -5,12 +8,17 @@ import log from "../../log"; const { Strophe } = converse.env; +/** + * @param {string} jid + * @param {object} attrs + * @param {new (attrs: object, options: object) => ChatBox} Model + */ export async function createChatBox (jid, attrs, Model) { jid = Strophe.getBareJidFromJid(jid.toLowerCase()); Object.assign(attrs, {'jid': jid, 'id': jid}); let chatbox; try { - chatbox = new Model(attrs, {'collection': _converse.chatboxes}); + chatbox = new Model(attrs, {'collection': _converse.state.chatboxes}); } catch (e) { log.error(e); return null; @@ -20,6 +28,6 @@ export async function createChatBox (jid, attrs, Model) { chatbox.destroy(); return null; } - _converse.chatboxes.add(chatbox); + _converse.state.chatboxes.add(chatbox); return chatbox; } diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 7dea0e5d6f..610681ef6d 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -30,16 +30,18 @@ export default { */ async getFeature (name, xmlns) { await api.waitUntil('streamFeaturesAdded'); + + const { stream_features } = _converse.state; if (!name || !xmlns) { throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature"); } - if (_converse.stream_features === undefined && !api.connection.connected()) { + if (stream_features === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. - const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`; + const msg = `Tried to get feature ${name} ${xmlns} but stream_features has been torn down`; log.warn(msg); return; } - return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns}); + return stream_features.findWhere({'name': name, 'xmlns': xmlns}); } }, @@ -65,15 +67,16 @@ export default { * @example _converse.api.disco.own.identities.clear(); */ add (category, type, name, lang) { - for (var i=0; i<_converse.disco._identities.length; i++) { - if (_converse.disco._identities[i].category == category && - _converse.disco._identities[i].type == type && - _converse.disco._identities[i].name == name && - _converse.disco._identities[i].lang == lang) { + const { disco } = _converse.state; + for (var i=0; i e.get('parent_jids')?.includes(jid)); + return _converse.state.disco_entities.filter(e => e.get('parent_jids')?.includes(jid)); }, /** @@ -235,7 +240,7 @@ export default { * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true}); */ create (data, options) { - return _converse.disco_entities.create(data, options); + return _converse.state.disco_entities.create(data, options); } }, @@ -266,7 +271,7 @@ export default { const entity = await api.disco.entities.get(jid, true); - if (_converse.disco_entities === undefined && !api.connection.connected()) { + if (_converse.state.disco_entities === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. log.warn(`Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`); return []; @@ -300,7 +305,7 @@ export default { const entity = await api.disco.entities.get(jid, true); - if (_converse.disco_entities === undefined && !api.connection.connected()) { + if (_converse.state.disco_entities === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. log.warn(`Tried to check if ${jid} supports feature ${feature}`); return false; @@ -424,15 +429,15 @@ export default { * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support * * @method api.disco.getIdentity - * @param { string } The identity category. + * @param {string} category -The identity category. * In the XML stanza, this is the `category` * attribute of the `` element. * For example: 'pubsub' - * @param { string } type The identity type. + * @param {string} type - The identity type. * In the XML stanza, this is the `type` * attribute of the `` element. * For example: 'pep' - * @param { string } jid The JID of the entity which might have the identity + * @param {string} jid - The JID of the entity which might have the identity * @returns {promise} A promise which resolves with a map indicating * whether an identity with a given type is provided by the entity. * @example diff --git a/src/headless/plugins/disco/entities.js b/src/headless/plugins/disco/entities.js index 95c98c31b3..431496f4d4 100644 --- a/src/headless/plugins/disco/entities.js +++ b/src/headless/plugins/disco/entities.js @@ -1,6 +1,6 @@ import DiscoEntity from './entity.js'; import log from "../../log.js"; -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; class DiscoEntities extends Collection { diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js index 4cecf887b5..3ed10fe906 100644 --- a/src/headless/plugins/disco/entity.js +++ b/src/headless/plugins/disco/entity.js @@ -2,8 +2,7 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from '../../log.js'; import sizzle from 'sizzle'; -import { Collection } from '@converse/skeletor/src/collection'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Collection, Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; import { createStore } from '../../utils/storage.js'; @@ -50,7 +49,6 @@ class DiscoEntity extends Model { /** * Returns a Promise which resolves with a map indicating * whether a given identity is provided by this entity. - * @private * @method _converse.DiscoEntity#getIdentity * @param { String } category - The identity category * @param { String } type - The identity type @@ -66,7 +64,6 @@ class DiscoEntity extends Model { /** * Returns a Promise which resolves with a map indicating * whether a given feature is supported. - * @private * @method _converse.DiscoEntity#getFeature * @param { String } feature - The feature that might be supported. */ @@ -133,6 +130,9 @@ class DiscoEntity extends Model { this.onInfo(stanza); } + /** + * @param {Element} stanza + */ onDiscoItems (stanza) { sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => { if (item.getAttribute('node')) { @@ -141,7 +141,7 @@ class DiscoEntity extends Model { return; } const jid = item.getAttribute('jid'); - const entity = _converse.disco_entities.get(jid); + const entity = _converse.state.disco_entities.get(jid); if (entity) { entity.set({ parent_jids: [this.get('jid')] }); } else { @@ -155,7 +155,7 @@ class DiscoEntity extends Model { } async queryForItems () { - if (this.identities.where({ 'category': 'server' }).length === 0) { + if (this.identities.where({ category: 'server' }).length === 0) { // Don't fetch features and items if this is not a // server or a conference component. return; @@ -164,6 +164,9 @@ class DiscoEntity extends Model { this.onDiscoItems(stanza); } + /** + * @param {Element} stanza + */ async onInfo (stanza) { Array.from(stanza.querySelectorAll('identity')).forEach(identity => { this.identities.create({ diff --git a/src/headless/plugins/disco/index.js b/src/headless/plugins/disco/index.js index 2b45bf583f..d1e0113204 100644 --- a/src/headless/plugins/disco/index.js +++ b/src/headless/plugins/disco/index.js @@ -25,19 +25,23 @@ converse.plugins.add('converse-disco', { api.promises.add('discoInitialized'); api.promises.add('streamFeaturesAdded'); - _converse.DiscoEntity = DiscoEntity; - _converse.DiscoEntities = DiscoEntities; + const exports = { DiscoEntity, DiscoEntities }; - _converse.disco = { + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); + + const disco = { _identities: [], _features: [] }; + Object.assign(_converse, { disco }); // XXX: DEPRECATED + Object.assign(_converse.state, { disco }); api.listen.on('userSessionInitialized', async () => { initStreamFeatures(); - if (_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) { + if (_converse.state.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) { // When re-attaching to a BOSH session, we fetch the stream features from the cache. - await new Promise((success, error) => _converse.stream_features.fetch({ success, error })); + await new Promise((success, error) => _converse.state.stream_features.fetch({ success, error })); notifyStreamFeaturesAdded(); } }); @@ -47,9 +51,12 @@ converse.plugins.add('converse-disco', { api.listen.on('beforeTearDown', async () => { api.promises.add('streamFeaturesAdded'); - if (_converse.stream_features) { - await _converse.stream_features.clearStore(); - delete _converse.stream_features; + + const { stream_features } = _converse.state; + if (stream_features) { + await stream_features.clearStore(); + delete _converse.state.stream_features; + Object.assign(_converse, { stream_features: undefined }); // XXX: DEPRECATED } }); diff --git a/src/headless/plugins/disco/utils.js b/src/headless/plugins/disco/utils.js index 8dccc2fdf3..c252b07736 100644 --- a/src/headless/plugins/disco/utils.js +++ b/src/headless/plugins/disco/utils.js @@ -1,6 +1,6 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; import { createStore } from '../../utils/storage.js'; const { Strophe, $iq } = converse.env; @@ -18,7 +18,7 @@ function onDiscoInfoRequest (stanza) { } iqresult.c('query', attrs); - _converse.disco._identities.forEach(identity => { + _converse.state.disco._identities.forEach(identity => { const attrs = { 'category': identity.category, 'type': identity.type @@ -31,7 +31,7 @@ function onDiscoInfoRequest (stanza) { } iqresult.c('identity', attrs).up(); }); - _converse.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up()); + _converse.state.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up()); api.send(iqresult.tree()); return true; } @@ -64,14 +64,22 @@ export async function initializeDisco () { 'iq', 'get', null, null ); - _converse.disco_entities = new _converse.DiscoEntities(); - const id = `converse.disco-entities-${_converse.bare_jid}`; - _converse.disco_entities.browserStorage = createStore(id, 'session'); + const disco_entities = new _converse.exports.DiscoEntities(); - const collection = await _converse.disco_entities.fetchEntities(); - if (collection.length === 0 || !collection.get(_converse.domain)) { + Object.assign(_converse, { disco_entities }); // XXX: DEPRECATED + Object.assign(_converse.state, { disco_entities }); + + const bare_jid = _converse.session.get('bare_jid'); + const id = `converse.disco-entities-${bare_jid}`; + + disco_entities.browserStorage = createStore(id, 'session'); + const collection = await disco_entities.fetchEntities(); + + const domain = _converse.session.get('domain'); + + if (collection.length === 0 || !collection.get(domain)) { // If we don't have an entity for our own XMPP server, create one. - api.disco.entities.create({'jid': _converse.domain}, {'ignore_cache': true}); + api.disco.entities.create({'jid': domain}, {'ignore_cache': true}); } /** * Triggered once the `converse-disco` plugin has been initialized and the @@ -89,12 +97,15 @@ export function initStreamFeatures () { // features from cache. // Otherwise the features will be created once we've received them // from the server (see populateStreamFeatures). - if (!_converse.stream_features) { - const bare_jid = Strophe.getBareJidFromJid(_converse.jid); + if (!_converse.state.stream_features) { + const bare_jid = _converse.session.get('bare_jid'); const id = `converse.stream-features-${bare_jid}`; api.promises.add('streamFeaturesAdded'); - _converse.stream_features = new Collection(); - _converse.stream_features.browserStorage = createStore(id, "session"); + + const stream_features = new Collection(); + stream_features.browserStorage = createStore(id, "session"); + Object.assign(_converse, { stream_features }); // XXX: DEPRECATED + Object.assign(_converse.state, { stream_features }); } } @@ -113,11 +124,11 @@ export function populateStreamFeatures () { // Strophe.js sets the element on the // Strophe.Connection instance. // - // Once this is we populate the _converse.stream_features collection + // Once this is we populate the stream_features collection // and trigger streamFeaturesAdded. initStreamFeatures(); Array.from(api.connection.get().features.childNodes).forEach(feature => { - _converse.stream_features.create({ + _converse.state.stream_features.create({ 'name': feature.nodeName, 'xmlns': feature.getAttribute('xmlns') }); @@ -126,10 +137,12 @@ export function populateStreamFeatures () { } export function clearSession () { - _converse.disco_entities?.forEach(e => e.features.clearStore()); - _converse.disco_entities?.forEach(e => e.identities.clearStore()); - _converse.disco_entities?.forEach(e => e.dataforms.clearStore()); - _converse.disco_entities?.forEach(e => e.fields.clearStore()); - _converse.disco_entities?.clearStore(); - delete _converse.disco_entities; + const { disco_entities } = _converse.state; + disco_entities?.forEach(e => e.features.clearStore()); + disco_entities?.forEach(e => e.identities.clearStore()); + disco_entities?.forEach(e => e.dataforms.clearStore()); + disco_entities?.forEach(e => e.fields.clearStore()); + disco_entities?.clearStore(); + delete _converse.state.disco_entities; + Object.assign(_converse, { disco_entities: undefined }); } diff --git a/src/headless/plugins/emoji/index.js b/src/headless/plugins/emoji/index.js index 4d881fefd5..1367a27e3c 100644 --- a/src/headless/plugins/emoji/index.js +++ b/src/headless/plugins/emoji/index.js @@ -6,7 +6,7 @@ import './utils.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; @@ -73,7 +73,9 @@ converse.plugins.add('converse-emoji', { } } - _converse.EmojiPicker = EmojiPicker; + const exports = { EmojiPicker }; + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); // We extend the default converse.js API to add methods specific to MUC groupchats. Object.assign(api, { diff --git a/src/headless/plugins/emoji/utils.js b/src/headless/plugins/emoji/utils.js index 4900562d73..7ca2ce62fe 100644 --- a/src/headless/plugins/emoji/utils.js +++ b/src/headless/plugins/emoji/utils.js @@ -56,27 +56,33 @@ function fromCodePoint (codepoint) { } +/** + * Converts unicode code points and code pairs to their respective characters + * @param {string} unicode + */ function convert (unicode) { - // Converts unicode code points and code pairs to their respective characters if (unicode.indexOf("-") > -1) { - const parts = [], - s = unicode.split('-'); + const parts = []; + const s = unicode.split('-'); + for (let i = 0; i < s.length; i++) { - let part = parseInt(s[i], 16); + const part = parseInt(s[i], 16); if (part >= 0x10000 && part <= 0x10FFFF) { const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800; const lo = ((part - 0x10000) % 0x400) + 0xDC00; - part = (String.fromCharCode(hi) + String.fromCharCode(lo)); + parts.push(String.fromCharCode(hi) + String.fromCharCode(lo)); } else { - part = String.fromCharCode(part); + parts.push(String.fromCharCode(part)); } - parts.push(part); } return parts.join(''); } return fromCodePoint(unicode); } +/** + * @param {string} str + */ export function convertASCII2Emoji (str) { // Replace ASCII smileys return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => { @@ -90,6 +96,9 @@ export function convertASCII2Emoji (str) { }); } +/** + * @param {string} text + */ export function getShortnameReferences (text) { if (!converse.emojis.initialized) { throw new Error( @@ -111,16 +120,24 @@ export function getShortnameReferences (text) { } +/** + * @param {string} str + * @param {Function} callback + */ function parseStringForEmojis(str, callback) { const UFE0Fg = /\uFE0F/g; const U200D = String.fromCharCode(0x200D); return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => { const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji); if (icon_id) callback(icon_id, emoji, offset); + return emoji; }); } +/** + * @param {string} text + */ export function getCodePointReferences (text) { const references = []; parseStringForEmojis(text, (icon_id, emoji, offset) => { diff --git a/src/headless/plugins/headlines/api.js b/src/headless/plugins/headlines/api.js index cc9ccc7807..251b9645ba 100644 --- a/src/headless/plugins/headlines/api.js +++ b/src/headless/plugins/headlines/api.js @@ -1,5 +1,9 @@ +/** + * @typedef {import('./feed.js').default} HeadlinesFeed + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; +import { HEADLINES_TYPE } from '../../shared/constants.js'; export default { /** @@ -18,24 +22,30 @@ export default { * @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model. * @param { Boolean } [create=false] - Whether the chat should be created if it's not found. - * @returns { Promise<_converse.HeadlinesFeed> } + * @returns { Promise } */ async get (jids, attrs={}, create=false) { + /** + * @param {string} jid + * @returns {Promise} + */ async function _get (jid) { let model = await api.chatboxes.get(jid); if (!model && create) { - model = await api.chatboxes.create(jid, attrs, _converse.HeadlinesFeed); + const { HeadlinesFeed } = _converse.exports; + model = await api.chatboxes.create(jid, attrs, HeadlinesFeed); } else { - model = (model && model.get('type') === _converse.HEADLINES_TYPE) ? model : null; + model = (model && model.get('type') === HEADLINES_TYPE) ? model : null; if (model && Object.keys(attrs).length) { model.save(attrs); } } return model; } + if (jids === undefined) { const chats = await api.chatboxes.get(); - return chats.filter(c => (c.get('type') === _converse.HEADLINES_TYPE)); + return chats.filter(c => (c.get('type') === HEADLINES_TYPE)); } else if (typeof jids === 'string') { return _get(jids); } diff --git a/src/headless/plugins/headlines/feed.js b/src/headless/plugins/headlines/feed.js index 9706bbce95..d383ea53f5 100644 --- a/src/headless/plugins/headlines/feed.js +++ b/src/headless/plugins/headlines/feed.js @@ -1,21 +1,33 @@ import ChatBox from '../../plugins/chat/model.js'; -import _converse from '../../shared/_converse.js'; import api from "../../shared/api/index.js"; +import { HEADLINES_TYPE } from '../../shared/constants.js'; +/** + * Shows headline messages + * @class + * @namespace _converse.HeadlinesFeed + * @memberOf _converse + */ export default class HeadlinesFeed extends ChatBox { - defaults () { // eslint-disable-line class-methods-use-this + defaults () { return { 'bookmarked': false, 'hidden': ['mobile', 'fullscreen'].includes(api.settings.get("view_mode")), 'message_type': 'headline', 'num_unread': 0, 'time_opened': this.get('time_opened') || (new Date()).getTime(), - 'type': _converse.HEADLINES_TYPE + 'time_sent': undefined, + 'type': HEADLINES_TYPE } } + constructor (attrs, options) { + super(attrs, options); + this.disable_mam = true; // Don't do MAM queries for this box + } + async initialize () { super.initialize(); this.set({'box_id': `box-${this.get('jid')}`}); diff --git a/src/headless/plugins/headlines/index.js b/src/headless/plugins/headlines/index.js index 3e4a050008..6e274cd1a7 100644 --- a/src/headless/plugins/headlines/index.js +++ b/src/headless/plugins/headlines/index.js @@ -7,18 +7,15 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import headlines_api from './api.js'; import { onHeadlineMessage } from './utils.js'; +import { HEADLINES_TYPE } from '../../shared/constants.js'; converse.plugins.add('converse-headlines', { dependencies: ["converse-chat"], initialize () { - /** - * Shows headline messages - * @class - * @namespace _converse.HeadlinesFeed - * @memberOf _converse - */ - _converse.HeadlinesFeed = HeadlinesFeed; + const exports = { HeadlinesFeed }; + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); function registerHeadlineHandler () { api.connection.get()?.addHandler(m => { @@ -31,9 +28,6 @@ converse.plugins.add('converse-headlines', { Object.assign(api, headlines_api); - api.chatboxes.registry.add( - _converse.HEADLINES_TYPE, - HeadlinesFeed - ); + api.chatboxes.registry.add(HEADLINES_TYPE, HeadlinesFeed); } }); diff --git a/src/headless/plugins/headlines/utils.js b/src/headless/plugins/headlines/utils.js index 5a5a6175f2..c20979775d 100644 --- a/src/headless/plugins/headlines/utils.js +++ b/src/headless/plugins/headlines/utils.js @@ -15,7 +15,7 @@ export async function onHeadlineMessage (stanza) { await api.waitUntil('rosterInitialized') if (from_jid.includes('@') && - !_converse.roster.get(from_jid) && + !_converse.state.roster.get(from_jid) && !api.settings.get("allow_non_roster_messaging")) { return; } diff --git a/src/headless/plugins/mam/api.js b/src/headless/plugins/mam/api.js index c8eb1300c1..f26f9840d7 100644 --- a/src/headless/plugins/mam/api.js +++ b/src/headless/plugins/mam/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {module:converse-rsm.RSMQueryParameters} RSMQueryParameters + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import dayjs from 'dayjs'; @@ -26,11 +29,11 @@ export default { */ archive: { /** - * @typedef { module:converse-rsm~RSMQueryParameters } MAMFilterParameters - * Filter parameters which can be used to filter a MAM XEP-0313 archive - * @property { String } [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging. - * @property { String } [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging. - * @property { String } [with] - A JID against which to match messages, according to either their `to` or `from` attributes. + * @typedef {RSMQueryParameters} MAMFilterParameters + * Filter parmeters which can be used to filter a MAM XEP-0313 archive + * @property String} [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging. + * @property {String} [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging. + * @property {String} [with] - A JID against which to match messages, according to either their `to` or `from` attributes. * An item in a MUC archive matches if the publisher of the item matches the JID. * If `with` is omitted, all messages that match the rest of the query will be returned, regardless of to/from * addresses of each message. @@ -38,8 +41,8 @@ export default { /** * The options that can be passed in to the {@link _converse.api.archive.query } method - * @typedef { module:converse-mam~MAMFilterParameters } ArchiveQueryOptions - * @property { Boolean } [groupchat=false] - Whether the MAM archive is for a groupchat. + * @typedef {MAMFilterParameters} ArchiveQueryOptions + * @property {boolean} [groupchat=false] - Whether the MAM archive is for a groupchat. */ /** @@ -49,10 +52,9 @@ export default { * RSM to enable easy querying between results pages. * * @method _converse.api.archive.query - * @param { module:converse-mam~ArchiveQueryOptions } options - An object containing query parameters + * @param {ArchiveQueryOptions} options - An object containing query parameters * @throws {Error} An error is thrown if the XMPP server responds with an error. - * @returns { Promise } A promise which resolves - * to a {@link module:converse-mam~MAMQueryResult } object. + * @returns {Promise} A promise which resolves to a {@link MAMQueryResult} object. * * @example * // Requesting all archived messages @@ -211,7 +213,8 @@ export default { attrs.to = options['with']; } - const jid = attrs.to || _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + const jid = attrs.to || bare_jid; const supported = await api.disco.supports(NS.MAM, jid); if (!supported) { log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${NS.MAM}`); @@ -249,18 +252,18 @@ export default { const connection = api.connection.get(); const messages = []; - const message_handler = connection.addHandler(stanza => { + const message_handler = connection.addHandler(/** @param {Element} stanza */(stanza) => { const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop(); if (result === undefined || result.getAttribute('queryid') !== queryid) { return true; } - const from = stanza.getAttribute('from') || _converse.bare_jid; + const from = stanza.getAttribute('from') || bare_jid; if (options.groupchat) { if (from !== options['with']) { log.warn(`Ignoring alleged groupchat MAM message from ${stanza.getAttribute('from')}`); return true; } - } else if (from !== _converse.bare_jid) { + } else if (from !== bare_jid) { log.warn(`Ignoring alleged MAM message from ${stanza.getAttribute('from')}`); return true; } @@ -296,14 +299,14 @@ export default { rsm = new RSM({...options, 'xml': set}); } /** - * @typedef { Object } MAMQueryResult - * @property { Array } messages - * @property { RSM } [rsm] - An instance of {@link RSM}. + * @typedef {Object} MAMQueryResult + * @property {Array} messages + * @property {RSM} [rsm] - An instance of {@link RSM}. * You can call `next()` or `previous()` on this instance, * to get the RSM query parameters for the next or previous * page in the result set. - * @property { Boolean } complete - * @property { Error } [error] + * @property {boolean} [complete] + * @property {Error} [error] */ return { messages, rsm, complete }; } diff --git a/src/headless/plugins/mam/index.js b/src/headless/plugins/mam/index.js index 51a72a486f..995fde8d64 100644 --- a/src/headless/plugins/mam/index.js +++ b/src/headless/plugins/mam/index.js @@ -8,6 +8,7 @@ import MAMPlaceholderMessage from './placeholder.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import mam_api from './api.js'; +import { PRIVATE_CHAT_TYPE } from '../..//shared/constants.js'; import { Strophe } from 'strophe.js'; import { onMAMError, @@ -33,7 +34,9 @@ converse.plugins.add('converse-mam', { Object.assign(api, mam_api); // This is mainly done to aid with tests - Object.assign(_converse, { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage }); + const exports = { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage }; + Object.assign(_converse, exports); // XXX DEPRECATED + Object.assign(_converse.exports, exports); /************************ Event Handlers ************************/ api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM)); @@ -49,13 +52,13 @@ converse.plugins.add('converse-mam', { api.listen.on('enteredNewRoom', muc => muc.features.get('mam_enabled') && fetchNewestMessages(muc)); api.listen.on('chatReconnected', chat => { - if (chat.get('type') === _converse.PRIVATE_CHAT_TYPE) { + if (chat.get('type') === PRIVATE_CHAT_TYPE) { fetchNewestMessages(chat); } }); api.listen.on('afterMessagesFetched', chat => { - if (chat.get('type') === _converse.PRIVATE_CHAT_TYPE) { + if (chat.get('type') === PRIVATE_CHAT_TYPE) { fetchNewestMessages(chat); } }); diff --git a/src/headless/plugins/mam/placeholder.js b/src/headless/plugins/mam/placeholder.js index 699cdb9e58..1d1cf0886f 100644 --- a/src/headless/plugins/mam/placeholder.js +++ b/src/headless/plugins/mam/placeholder.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { getUniqueId } from '../../utils/index.js'; export default class MAMPlaceholderMessage extends Model { diff --git a/src/headless/plugins/mam/utils.js b/src/headless/plugins/mam/utils.js index 72ae385d54..c5b8ef3e73 100644 --- a/src/headless/plugins/mam/utils.js +++ b/src/headless/plugins/mam/utils.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../muc/muc.js').default} MUC + * @typedef {import('../chat/model.js').default} ChatBox + */ import MAMPlaceholderMessage from './placeholder.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; @@ -6,10 +10,14 @@ import sizzle from 'sizzle'; import { Strophe, $iq } from 'strophe.js'; import { parseMUCMessage } from '../../plugins/muc/parsers'; import { parseMessage } from '../../plugins/chat/parsers'; +import { CHATROOMS_TYPE } from '../../shared/constants.js'; const { NS } = Strophe; const u = converse.env.utils; +/** + * @param {Element} iq + */ export function onMAMError (iq) { if (iq?.querySelectorAll('feature-not-implemented').length) { log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`); @@ -45,7 +53,7 @@ export function onMAMPreferences (iq, feature) { // but Prosody doesn't do this, so we don't rely on it. api.sendIQ(stanza) .then(() => feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } })) - .catch(_converse.onMAMError); + .catch(_converse.exports.onMAMError); } else { feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } }); } @@ -58,8 +66,8 @@ export function getMAMPrefsFromFeature (feature) { } if (prefs['default'] !== api.settings.get('message_archiving')) { api.sendIQ($iq({ 'type': 'get' }).c('prefs', { 'xmlns': NS.MAM })) - .then(iq => _converse.onMAMPreferences(iq, feature)) - .catch(_converse.onMAMError); + .then(iq => _converse.exports.onMAMPreferences(iq, feature)) + .catch(_converse.exports.onMAMError); } } @@ -77,7 +85,7 @@ export function preMUCJoinMAMFetch (muc) { export async function handleMAMResult (model, result, query, options, should_page) { await api.emojis.initialize(); - const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; + const is_muc = model.get('type') === CHATROOMS_TYPE; const doParseMessage = s => is_muc ? parseMUCMessage(s, model) : parseMessage(s); const messages = await Promise.all(result.messages.map(doParseMessage)); result.messages = messages; @@ -99,28 +107,28 @@ export async function handleMAMResult (model, result, query, options, should_pag } /** - * @typedef { Object } MAMOptions + * @typedef {Object} MAMOptions * A map of MAM related options that may be passed to fetchArchivedMessages - * @param { number } [options.max] - The maximum number of items to return. + * @param {number} [options.max] - The maximum number of items to return. * Defaults to "archived_messages_page_size" - * @param { string } [options.after] - The XEP-0359 stanza ID of a message + * @param {string} [options.after] - The XEP-0359 stanza ID of a message * after which messages should be returned. Implies forward paging. - * @param { string } [options.before] - The XEP-0359 stanza ID of a message + * @param {string} [options.before] - The XEP-0359 stanza ID of a message * before which messages should be returned. Implies backward paging. - * @param { string } [options.end] - A date string in ISO-8601 format, + * @param {string} [options.end] - A date string in ISO-8601 format, * before which messages should be returned. Implies backward paging. - * @param { string } [options.start] - A date string in ISO-8601 format, + * @param {string} [options.start] - A date string in ISO-8601 format, * after which messages should be returned. Implies forward paging. - * @param { string } [options.with] - The JID of the entity with + * @param {string} [options.with] - The JID of the entity with * which messages were exchanged. - * @param { boolean } [options.groupchat] - True if archive in groupchat. + * @param {boolean} [options.groupchat] - True if archive in groupchat. */ /** * Fetch XEP-0313 archived messages based on the passed in criteria. - * @param { ChatBox | ChatRoom } model - * @param { MAMOptions } [options] - * @param { ('forwards'|'backwards'|null)} [should_page=null] - Determines whether + * @param {ChatBox} model + * @param {MAMOptions} [options] + * @param {('forwards'|'backwards'|null)} [should_page=null] - Determines whether * this function should recursively page through the entire result set if a limited * number of results were returned. */ @@ -128,8 +136,9 @@ export async function fetchArchivedMessages (model, options = {}, should_page = if (model.disable_mam) { return; } - const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; - const mam_jid = is_muc ? model.get('jid') : _converse.bare_jid; + const is_muc = model.get('type') === CHATROOMS_TYPE; + const bare_jid = _converse.session.get('bare_jid'); + const mam_jid = is_muc ? model.get('jid') : bare_jid; if (!(await api.disco.supports(NS.MAM, mam_jid))) { return; } @@ -162,9 +171,9 @@ export async function fetchArchivedMessages (model, options = {}, should_page = /** * Create a placeholder message which is used to indicate gaps in the history. - * @param { _converse.ChatBox | _converse.ChatRoom } model - * @param { MAMOptions } options - * @param { object } result - The RSM result object + * @param {ChatBox} model + * @param {MAMOptions} options + * @param {object} result - The RSM result object */ async function createPlaceholder (model, options, result) { if (options.before == '' && (model.messages.length === 0 || !options.start)) { @@ -185,9 +194,11 @@ async function createPlaceholder (model, options, result) { const { rsm } = result; const key = `stanza_id ${model.get('jid')}`; const adjacent_message = msgs.find(m => m[key] === rsm.result.first); + const adjacent_message_date = new Date(adjacent_message['time']); + const msg_data = { 'template_hook': 'getMessageTemplate', - 'time': new Date(new Date(adjacent_message['time']) - 1).toISOString(), + 'time': new Date(adjacent_message_date.getTime() - 1).toISOString(), 'before': rsm.result.first, 'start': options.start } @@ -197,7 +208,7 @@ async function createPlaceholder (model, options, result) { /** * Fetches messages that might have been archived *after* * the last archived message in our local cache. - * @param { _converse.ChatBox | _converse.ChatRoom } + * @param {ChatBox} model */ export function fetchNewestMessages (model) { if (model.disable_mam) { diff --git a/src/headless/plugins/muc/affiliations/api.js b/src/headless/plugins/muc/affiliations/api.js index 5f5c2f5c1c..25543538c3 100644 --- a/src/headless/plugins/muc/affiliations/api.js +++ b/src/headless/plugins/muc/affiliations/api.js @@ -1,3 +1,6 @@ +/** + * @module:plugin-muc-affiliations-api + */ import { setAffiliations } from './utils.js'; export default { @@ -11,15 +14,16 @@ export default { affiliations: { /** * Set the given affliation for the given JIDs in the specified MUCs + * @typedef {Object} User + * @property {string} User.jid - The JID of the user whose affiliation will change + * @property {Array} User.affiliation - The new affiliation for this user + * @property {string} [User.reason] - An optional reason for the affiliation change * - * @param { String|Array } muc_jids - The JIDs of the MUCs in + * @param {String|Array} muc_jids - The JIDs of the MUCs in * which the affiliation should be set. - * @param { Object[] } users - An array of objects representing users + * @param {User[]} users - An array of objects representing users * for whom the affiliation is to be set. - * @param { String } users[].jid - The JID of the user whose affiliation will change - * @param { ('outcast'|'member'|'admin'|'owner') } users[].affiliation - The new affiliation for this user - * @param { String } [users[].reason] - An optional reason for the affiliation change - * @returns { Promise } + * @returns {Promise} * * @example * api.rooms.affiliations.set( diff --git a/src/headless/plugins/muc/affiliations/utils.js b/src/headless/plugins/muc/affiliations/utils.js index ce0b203593..5dff7870e8 100644 --- a/src/headless/plugins/muc/affiliations/utils.js +++ b/src/headless/plugins/muc/affiliations/utils.js @@ -1,6 +1,10 @@ /** * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) + * @module:muc-affiliations-utils + * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem + * @typedef {module:plugin-muc-affiliations-api.User} User + * @typedef {import('@converse/skeletor').Model} Model */ import _converse from '../../../shared/_converse.js'; import api, { converse } from '../../../shared/api/index.js'; @@ -15,9 +19,10 @@ const { Strophe, $iq, u } = converse.env; * Returns an array of {@link MemberListItem} objects, representing occupants * that have the given affiliation. * See: https://xmpp.org/extensions/xep-0045.html#modifymember - * @param { ("admin"|"owner"|"member") } affiliation - * @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched - * @returns { Promise } + * @typedef {("admin"|"owner"|"member")} NonOutcastAffiliation + * @param {NonOutcastAffiliation} affiliation + * @param {string} muc_jid - The JID of the MUC for which the affiliation list should be fetched + * @returns {Promise} */ export async function getAffiliationList (affiliation, muc_jid) { const { __ } = _converse; @@ -45,8 +50,8 @@ export async function getAffiliationList (affiliation, muc_jid) { /** * Given an occupant model, see which affiliations may be assigned by that user - * @param { Model } occupant - * @returns { Array<('owner'|'admin'|'member'|'outcast'|'none')> } - An array of assignable affiliations + * @param {Model} occupant + * @returns {typeof AFFILIATIONS} An array of assignable affiliations */ export function getAssignableAffiliations (occupant) { let disabled = api.settings.get('modtools_disable_assign'); @@ -62,17 +67,12 @@ export function getAssignableAffiliations (occupant) { } } -// Necessary for tests -_converse.getAssignableAffiliations = getAssignableAffiliations; - /** * Send IQ stanzas to the server to modify affiliations for users in this groupchat. * See: https://xmpp.org/extensions/xep-0045.html#modifymember - * @param { Array } users - * @param { string } users[].jid - The JID of the user whose affiliation will change - * @param { Array } users[].affiliation - The new affiliation for this user - * @param { string } [users[].reason] - An optional reason for the affiliation change - * @returns { Promise } + * @param {String|Array} muc_jid - The JID(s) of the MUCs in which the + * @param {Array} users + * @returns {Promise} */ export function setAffiliations (muc_jid, users) { const affiliations = [...new Set(users.map(u => u.affiliation))]; @@ -89,13 +89,13 @@ export function setAffiliations (muc_jid, users) { * a separate stanza for each JID. * Related ticket: https://issues.prosody.im/345 * - * @param { ('outcast'|'member'|'admin'|'owner') } affiliation - The affiliation to be set - * @param { String|Array } muc_jids - The JID(s) of the MUCs in which the + * @param {typeof AFFILIATIONS[number]} affiliation - The affiliation to be set + * @param {String|Array} muc_jids - The JID(s) of the MUCs in which the * affiliations need to be set. - * @param { object } members - A map of jids, affiliations and + * @param {object} members - A map of jids, affiliations and * optionally reasons. Only those entries with the * same affiliation as being currently set will be considered. - * @returns { Promise } A promise which resolves and fails depending on the XMPP server response. + * @returns {Promise} A promise which resolves and fails depending on the XMPP server response. */ export function setAffiliation (affiliation, muc_jids, members) { if (!Array.isArray(muc_jids)) { @@ -109,10 +109,9 @@ export function setAffiliation (affiliation, muc_jids, members) { /** * Send an IQ stanza specifying an affiliation change. - * @private - * @param { String } affiliation: affiliation (could also be stored on the member object). - * @param { String } muc_jid: The JID of the MUC in which the affiliation should be set. - * @param { Object } member: Map containing the member's jid and optionally a reason and affiliation. + * @param {typeof AFFILIATIONS[number]} affiliation: affiliation (could also be stored on the member object). + * @param {string} muc_jid: The JID of the MUC in which the affiliation should be set. + * @param {object} member: Map containing the member's jid and optionally a reason and affiliation. */ function sendAffiliationIQ (affiliation, muc_jid, member) { const iq = $iq({ to: muc_jid, type: 'set' }) @@ -139,20 +138,20 @@ function sendAffiliationIQ (affiliation, muc_jid, member) { * * The 'reason' property is not taken into account when * comparing whether affiliations have been changed. - * @param { boolean } exclude_existing - Indicates whether JIDs from + * @param {boolean} exclude_existing - Indicates whether JIDs from * the new list which are also in the old list * (regardless of affiliation) should be excluded * from the delta. One reason to do this * would be when you want to add a JID only if it * doesn't have *any* existing affiliation at all. - * @param { boolean } remove_absentees - Indicates whether JIDs + * @param {boolean} remove_absentees - Indicates whether JIDs * from the old list which are not in the new list * should be considered removed and therefore be * included in the delta with affiliation set * to 'none'. - * @param { array } new_list - Array containing the new affiliations - * @param { array } old_list - Array containing the old affiliations - * @returns { array } + * @param {array} new_list - Array containing the new affiliations + * @param {array} old_list - Array containing the old affiliations + * @returns {array} */ export function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) { const new_jids = new_list.map(o => o.jid); diff --git a/src/headless/plugins/muc/api.js b/src/headless/plugins/muc/api.js index b97bcc451a..3ae0041321 100644 --- a/src/headless/plugins/muc/api.js +++ b/src/headless/plugins/muc/api.js @@ -1,8 +1,12 @@ +/** + * @typedef {import('./muc.js').default} MUC + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import log from '../../log'; import { Strophe } from 'strophe.js'; import { getJIDFromURI } from '../../utils/jid.js'; +import { CHATROOMS_TYPE } from '../../shared/constants.js'; export default { @@ -21,15 +25,16 @@ export default { * the chatroom in the background (i.e. doesn't cause a view to open). * * @method api.rooms.create - * @param {(string[]|string)} jid|jids The JID or array of + * @param {(string[]|string)} jids The JID or array of * JIDs of the chatroom(s) to create - * @param { object } [attrs] attrs The room attributes - * @returns {Promise} Promise which resolves with the Model representing the chat. + * @param {object} [attrs] attrs The room attributes + * @returns {Promise[]} Promise which resolves with the Model representing the chat. */ create (jids, attrs = {}) { attrs = typeof attrs === 'string' ? { 'nick': attrs } : attrs || {}; if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) { - attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + attrs.nick = Strophe.getNodeFromJid(bare_jid); } if (jids === undefined) { throw new TypeError('rooms.create: You need to provide at least one JID'); @@ -45,30 +50,31 @@ export default { * Similar to {@link api.chats.open}, but for groupchats. * * @method api.rooms.open - * @param { string } jid The room JID or JIDs (if not specified, all + * @param {string|string[]} jids The room JID or JIDs (if not specified, all * currently open rooms will be returned). - * @param { string } attrs A map containing any extra room attributes. - * @param { string } [attrs.nick] The current user's nickname for the MUC - * @param { boolean } [attrs.auto_configure] A boolean, indicating + * @param {object} attrs A map containing any extra room attributes. + * @param {string} [attrs.nick] The current user's nickname for the MUC + * @param {boolean} [attrs.hidden] + * @param {boolean} [attrs.auto_configure] A boolean, indicating * whether the room should be configured automatically or not. * If set to `true`, then it makes sense to pass in configuration settings. - * @param { object } [attrs.roomconfig] A map of configuration settings to be used when the room gets + * @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets * configured automatically. Currently it doesn't make sense to specify * `roomconfig` values if `auto_configure` is set to `false`. * For a list of configuration values that can be passed in, refer to these values * in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner). * The values should be named without the `muc#roomconfig_` prefix. - * @param { boolean } [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not. - * @param { boolean } [attrs.bring_to_foreground] A boolean indicating whether the room should be + * @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not. + * @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be * brought to the foreground and therefore replace the currently shown chat. * If there is no chat currently open, then this option is ineffective. - * @param { Boolean } [force=false] - By default, a minimized + * @param {boolean} [force=false] - By default, a minimized * room won't be maximized (in `overlayed` view mode) and in * `fullscreen` view mode a newly opened room won't replace * another chat already in the foreground. * Set `force` to `true` if you want to force the room to be * maximized or shown. - * @returns {Promise} Promise which resolves with the Model representing the chat. + * @returns {Promise} Promise which resolves with the Model representing the chat. * * @example * api.rooms.open('group@muc.example.com') @@ -119,14 +125,14 @@ export default { * Fetches the object representing a MUC chatroom (aka groupchat) * * @method api.rooms.get - * @param { String } [jid] The room JID (if not specified, all rooms will be returned). - * @param { Object } [attrs] A map containing any extra room attributes + * @param {string|string[]} [jids] The room JID (if not specified, all rooms will be returned). + * @param {object} [attrs] A map containing any extra room attributes * to be set if `create` is set to `true` - * @param { String } [attrs.nick] Specify the nickname - * @param { String } [attrs.password ] Specify a password if needed to enter a new room - * @param { Boolean } create A boolean indicating whether the room should be created + * @param {string} [attrs.nick] Specify the nickname + * @param {string} [attrs.password ] Specify a password if needed to enter a new room + * @param {boolean} create A boolean indicating whether the room should be created * if not found (default: `false`) - * @returns { Promise<_converse.ChatRoom> } + * @returns {Promise} * @example * api.waitUntil('roomsAutoJoined').then(() => { * const create_if_not_found = true; @@ -144,9 +150,9 @@ export default { jid = getJIDFromURI(jid); let model = await api.chatboxes.get(jid); if (!model && create) { - model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom); + model = await api.chatboxes.create(jid, attrs, _converse.exports.MUC); } else { - model = model && model.get('type') === _converse.CHATROOMS_TYPE ? model : null; + model = model && model.get('type') === CHATROOMS_TYPE ? model : null; if (model && Object.keys(attrs).length) { model.save(attrs); } @@ -155,7 +161,7 @@ export default { } if (jids === undefined) { const chats = await api.chatboxes.get(); - return chats.filter(c => c.get('type') === _converse.CHATROOMS_TYPE); + return chats.filter(c => c.get('type') === CHATROOMS_TYPE); } else if (typeof jids === 'string') { return _get(jids); } diff --git a/src/headless/plugins/muc/index.js b/src/headless/plugins/muc/index.js index 44e03daa33..007bc7e352 100644 --- a/src/headless/plugins/muc/index.js +++ b/src/headless/plugins/muc/index.js @@ -15,6 +15,7 @@ import affiliations_api from './affiliations/api.js'; import muc_api from './api.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; +import { CHATROOMS_TYPE } from '../../shared/constants.js'; import { autoJoinRooms, disconnectChatRooms, @@ -29,7 +30,7 @@ import { registerDirectInvitationHandler, routeToRoom, } from './utils.js'; -import { computeAffiliationsDelta } from './affiliations/utils.js'; +import { computeAffiliationsDelta, getAssignableAffiliations } from './affiliations/utils.js'; import { AFFILIATION_CHANGES, AFFILIATION_CHANGES_LIST, @@ -144,7 +145,7 @@ converse.plugins.add('converse-muc', { * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown */ - _converse.muc = { + const MUC_FEEDBACK_MESSAGES = { info_messages: { 100: __('This groupchat is not anonymous'), 102: __('This groupchat now shows unavailable members'), @@ -176,27 +177,36 @@ converse.plugins.add('converse-muc', { }, }; + const labels = { muc: MUC_FEEDBACK_MESSAGES }; + Object.assign(_converse.labels, labels); + Object.assign(_converse, labels); // XXX DEPRECATED + routeToRoom(); addEventListener('hashchange', routeToRoom); - _converse.ChatRoom = MUC; - _converse.ChatRoomMessage = MUCMessage; - _converse.ChatRoomOccupants = ChatRoomOccupants; - _converse.ChatRoomOccupant = ChatRoomOccupant; - - api.chatboxes.registry.add( - _converse.CHATROOMS_TYPE, - MUC - ); - - Object.assign(_converse, { + // TODO: DEPRECATED + const legacy_exports = { + ChatRoom: MUC, + ChatRoomMessage: MUCMessage, + }; + Object.assign(_converse, legacy_exports); + + const exports = { + MUC, + MUCMessage, + ChatRoomOccupants, + ChatRoomOccupant, + getAssignableAffiliations, getDefaultMUCNickname, isInfoVisible, onDirectMUCInvitation, ChatRoomMessages: MUCMessages, - }); + }; + Object.assign(_converse.exports, exports); + Object.assign(_converse, exports); // XXX DEPRECATED + + /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC); - /************************ BEGIN Event Handlers ************************/ if (api.settings.get('allow_muc_invitations')) { api.listen.on('connected', registerDirectInvitationHandler); @@ -210,6 +220,7 @@ converse.plugins.add('converse-muc', { api.listen.on('chatBoxesFetched', autoJoinRooms); api.listen.on('disconnected', disconnectChatRooms); api.listen.on('statusInitialized', onStatusInitialized); - api.listen.on('windowStateChanged', onWindowStateChanged); + + document.addEventListener('visibilitychange', onWindowStateChanged); }, }); diff --git a/src/headless/plugins/muc/message.js b/src/headless/plugins/muc/message.js index eb232a9f88..2fb0c65abf 100644 --- a/src/headless/plugins/muc/message.js +++ b/src/headless/plugins/muc/message.js @@ -3,16 +3,13 @@ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { Strophe } from 'strophe.js'; -/** - * @namespace _converse.ChatRoomMessage - * @memberOf _converse - */ + class MUCMessage extends Message { - initialize () { - if (!this.checkValidity()) { - return; - } + async initialize () { // eslint-disable-line require-await + this.chatbox = this.collection?.chatbox; + if (!this.checkValidity()) return; + if (this.get('file')) { this.on('change:put', () => this.uploadFile()); } @@ -20,13 +17,12 @@ class MUCMessage extends Message { this.on('change:type', () => this.setOccupant()); this.on('change:is_ephemeral', () => this.setTimerForEphemeralMessage()); - this.chatbox = this.collection?.chatbox; this.setTimerForEphemeralMessage(); this.setOccupant(); /** - * Triggered once a { @link _converse.ChatRoomMessage } has been created and initialized. + * Triggered once a { @link MUCMessage} has been created and initialized. * @event _converse#chatRoomMessageInitialized - * @type { _converse.ChatRoomMessages} + * @type {MUCMessage} * @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... }); */ api.trigger('chatRoomMessageInitialized', this); @@ -41,7 +37,7 @@ class MUCMessage extends Message { * based on configuration settings and server support. * @async * @method _converse.ChatRoomMessages#mayBeModerated - * @returns { Boolean } + * @returns {boolean} */ mayBeModerated () { if (typeof this.get('from_muc') === 'undefined') { @@ -57,7 +53,7 @@ class MUCMessage extends Message { } checkValidity () { - const result = _converse.Message.prototype.checkValidity.call(this); + const result = _converse.exports.Message.prototype.checkValidity.call(this); !result && this.chatbox.debouncedRejoin(); return result; } diff --git a/src/headless/plugins/muc/messages.js b/src/headless/plugins/muc/messages.js index d8aed0ba0d..ea811d2a62 100644 --- a/src/headless/plugins/muc/messages.js +++ b/src/headless/plugins/muc/messages.js @@ -1,19 +1,13 @@ import MUCMessage from './message'; -import { Collection } from '@converse/skeletor/src/collection'; +import { Collection } from '@converse/skeletor'; /** * Collection which stores MUC messages - * @namespace _converse.ChatRoomMessages - * @memberOf _converse */ class MUCMessages extends Collection { - get comparator () { - return 'time'; - } - - constructor () { - super(); + constructor (attrs, options={}) { + super(attrs, Object.assign({ comparator: 'time' }, options)); this.model = MUCMessage; } } diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 6777df3713..ea7e4603e3 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -1,3 +1,13 @@ +/** + * @module:headless-plugins-muc-muc + * @typedef {import('./message.js').default} MUCMessage + * @typedef {import('./occupant.js').default} ChatRoomOccupant + * @typedef {import('./affiliations/utils.js').NonOutcastAffiliation} NonOutcastAffiliation + * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import ChatBox from '../chat/model'; @@ -6,8 +16,9 @@ import log from '../../log'; import p from '../../utils/parse-helpers'; import pick from 'lodash-es/pick'; import sizzle from 'sizzle'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { ROOMSTATUS } from './constants.js'; +import { CHATROOMS_TYPE, GONE } from '../../shared/constants.js'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js'; import { TimeoutError } from '../../shared/errors.js'; import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js'; @@ -15,11 +26,11 @@ import { getOpenPromise } from '@converse/openpromise'; import { handleCorrection } from '../../shared/chat/utils.js'; import { initStorage, createStore } from '../../utils/storage.js'; import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js'; -import { getUniqueId, safeSave } from '../../utils/index.js'; +import { getUniqueId, isErrorObject, safeSave } from '../../utils/index.js'; import { isUniView } from '../../utils/session.js'; import { parseMUCMessage, parseMUCPresence } from './parsers.js'; import { sendMarker } from '../../shared/actions.js'; -import { shouldCreateGroupchatMessage } from './utils.js'; +import { shouldCreateGroupchatMessage, isInfoVisible } from './utils.js'; const { u } = converse.env; @@ -50,7 +61,7 @@ const METADATA_ATTRIBUTES = [ const ACTION_INFO_CODES = ['301', '303', '333', '307', '321', '322']; class MUCSession extends Model { - defaults () { // eslint-disable-line class-methods-use-this + defaults () { return { 'connection_status': ROOMSTATUS.DISCONNECTED }; @@ -59,12 +70,12 @@ class MUCSession extends Model { /** * Represents an open/ongoing groupchat conversation. - * @namespace _converse.ChatRoom + * @namespace MUC * @memberOf _converse */ class MUC extends ChatBox { - defaults () { // eslint-disable-line class-methods-use-this + defaults () { return { 'bookmarked': false, 'chat_state': undefined, @@ -78,7 +89,7 @@ class MUC extends ChatBox { // user. // // To keep things simple, we reuse `num_unread` from - // _converse.ChatBox to indicate unread messages which + // ChatBox to indicate unread messages which // mention the user and `num_unread_general` to indicate // generally unread messages (which *includes* mentions!). 'num_unread_general': 0, @@ -86,7 +97,7 @@ class MUC extends ChatBox { 'roomconfig': {}, 'time_opened': this.get('time_opened') || new Date().getTime(), 'time_sent': new Date(0).toISOString(), - 'type': _converse.CHATROOMS_TYPE + 'type': CHATROOMS_TYPE }; } @@ -121,9 +132,9 @@ class MUC extends ChatBox { this.join(); } /** - * Triggered once a {@link _converse.ChatRoom} has been created and initialized. + * Triggered once a {@link MUC} has been created and initialized. * @event _converse#chatRoomInitialized - * @type { _converse.ChatRoom } + * @type { MUC } * @example _converse.api.listen.on('chatRoomInitialized', model => { ... }); */ await api.trigger('chatRoomInitialized', this, { 'Synchronous': true }); @@ -136,7 +147,7 @@ class MUC extends ChatBox { /** * Checks whether this MUC qualifies for subscribing to XEP-0437 Room Activity Indicators (RAI) - * @method _converse.ChatRoom#isRAICandidate + * @method MUC#isRAICandidate * @returns { Boolean } */ isRAICandidate () { @@ -146,8 +157,8 @@ class MUC extends ChatBox { /** * Checks whether we're still joined and if so, restores the MUC state from cache. * @private - * @method _converse.ChatRoom#restoreFromCache - * @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`. + * @method MUC#restoreFromCache + * @returns {Promise} Returns `true` if we're still joined, otherwise returns `false`. */ async restoreFromCache () { if (this.isEntered()) { @@ -172,8 +183,8 @@ class MUC extends ChatBox { /** * Join the MUC * @private - * @method _converse.ChatRoom#join - * @param { String } nick - The user's nickname + * @method MUC#join + * @param { String } [nick] - The user's nickname * @param { String } [password] - Optional password, if required by the groupchat. * Will fall back to the `password` value stored in the room * model (if available). @@ -202,7 +213,7 @@ class MUC extends ChatBox { /** * Clear stale cache and re-join a MUC we've been in before. * @private - * @method _converse.ChatRoom#rejoin + * @method MUC#rejoin */ rejoin () { this.session.save('connection_status', ROOMSTATUS.DISCONNECTED); @@ -211,6 +222,9 @@ class MUC extends ChatBox { return this.join(); } + /** + * @param {string} password + */ async constructJoinPresence (password) { let stanza = $pres({ 'id': getUniqueId(), @@ -229,8 +243,7 @@ class MUC extends ChatBox { /** * *Hook* which allows plugins to update an outgoing MUC join presence stanza * @event _converse#constructedMUCPresence - * @param { _converse.ChatRoom } - The MUC from which this message stanza is being sent. - * @param { Element } stanza - The stanza which will be sent out + * @type {Element} The stanza which will be sent out */ stanza = await api.hook('constructedMUCPresence', this, stanza); return stanza; @@ -248,7 +261,7 @@ class MUC extends ChatBox { /** * Given the passed in MUC message, send a XEP-0333 chat marker. - * @param { _converse.MUCMessage } msg + * @param { MUCMessage } msg * @param { ('received'|'displayed'|'acknowledged') } [type='displayed'] * @param { Boolean } force - Whether a marker should be sent for the * message, even if it didn't include a `markable` element. @@ -277,7 +290,7 @@ class MUC extends ChatBox { * called after the MUC has been left and we don't have that information * anymore. * @private - * @method _converse.ChatRoom#enableRAI + * @method MUC#enableRAI */ enableRAI () { if (api.settings.get('muc_subscribe_to_rai')) { @@ -289,7 +302,7 @@ class MUC extends ChatBox { /** * Handler that gets called when the 'hidden' flag is toggled. * @private - * @method _converse.ChatRoom#onHiddenChange + * @method MUC#onHiddenChange */ async onHiddenChange () { const roomstatus = ROOMSTATUS; @@ -310,7 +323,7 @@ class MUC extends ChatBox { onOccupantAdded (occupant) { if ( - _converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.ENTERED) && + isInfoVisible(converse.MUC_TRAFFIC_STATES.ENTERED) && this.session.get('connection_status') === ROOMSTATUS.ENTERED && occupant.get('show') === 'online' ) { @@ -320,7 +333,7 @@ class MUC extends ChatBox { onOccupantRemoved (occupant) { if ( - _converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED) && + isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED) && this.isEntered() && occupant.get('show') === 'online' ) { @@ -332,9 +345,9 @@ class MUC extends ChatBox { if (occupant.get('states').includes('303')) { return; } - if (occupant.get('show') === 'offline' && _converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED)) { + if (occupant.get('show') === 'offline' && isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED)) { this.updateNotifications(occupant.get('nick'), converse.MUC_TRAFFIC_STATES.EXITED); - } else if (occupant.get('show') === 'online' && _converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.ENTERED)) { + } else if (occupant.get('show') === 'online' && isInfoVisible(converse.MUC_TRAFFIC_STATES.ENTERED)) { this.updateNotifications(occupant.get('nick'), converse.MUC_TRAFFIC_STATES.ENTERED); } } @@ -382,18 +395,20 @@ class MUC extends ChatBox { } getMessagesCollection () { - return new _converse.ChatRoomMessages(); + return new _converse.exports.ChatRoomMessages(); } restoreSession () { - const id = `muc.session-${_converse.bare_jid}-${this.get('jid')}`; + const bare_jid = _converse.session.get('bare_jid'); + const id = `muc.session-${bare_jid}-${this.get('jid')}`; this.session = new MUCSession({ id }); initStorage(this.session, id, 'session'); return new Promise(r => this.session.fetch({ 'success': r, 'error': r })); } initDiscoModels () { - let id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`; + const bare_jid = _converse.session.get('bare_jid'); + let id = `converse.muc-features-${bare_jid}-${this.get('jid')}`; this.features = new Model( Object.assign( { id }, @@ -406,15 +421,16 @@ class MUC extends ChatBox { this.features.browserStorage = createStore(id, 'session'); this.features.listenTo(_converse, 'beforeLogout', () => this.features.browserStorage.flush()); - id = `converse.muc-config-${_converse.bare_jid}-${this.get('jid')}`; + id = `converse.muc-config-${bare_jid}-${this.get('jid')}`; this.config = new Model({ id }); this.config.browserStorage = createStore(id, 'session'); this.config.listenTo(_converse, 'beforeLogout', () => this.config.browserStorage.flush()); } initOccupants () { - this.occupants = new _converse.ChatRoomOccupants(); - const id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`; + this.occupants = new _converse.exports.ChatRoomOccupants(); + const bare_jid = _converse.session.get('bare_jid'); + const id = `converse.occupants-${bare_jid}${this.get('jid')}`; this.occupants.browserStorage = createStore(id, 'session'); this.occupants.chatroom = this; this.occupants.listenTo(_converse, 'beforeLogout', () => this.occupants.browserStorage.flush()); @@ -458,6 +474,9 @@ class MUC extends ChatBox { } } + /** + * @param {Element} stanza + */ async handleErrorMessageStanza (stanza) { const { __ } = _converse; const attrs = await parseMUCMessage(stanza, this); @@ -510,7 +529,7 @@ class MUC extends ChatBox { /** * Handles incoming message stanzas from the service that hosts this MUC * @private - * @method _converse.ChatRoom#handleMessageFromMUCHost + * @method MUC#handleMessageFromMUCHost * @param { Element } stanza */ handleMessageFromMUCHost (stanza) { @@ -531,7 +550,7 @@ class MUC extends ChatBox { /** * Handles XEP-0452 MUC Mention Notification messages * @private - * @method _converse.ChatRoom#handleForwardedMentions + * @method MUC#handleForwardedMentions * @param { Element } stanza */ handleForwardedMentions (stanza) { @@ -561,8 +580,8 @@ class MUC extends ChatBox { /** * Parses an incoming message stanza and queues it for processing. * @private - * @method _converse.ChatRoom#handleMessageStanza - * @param { Element } stanza + * @method MUC#handleMessageStanza + * @param {Strophe.Builder|Element} stanza */ async handleMessageStanza (stanza) { stanza = stanza.tree?.() ?? stanza; @@ -583,13 +602,6 @@ class MUC extends ChatBox { } else if (!type) { return this.handleForwardedMentions(stanza); } - /** - * @typedef { Object } MUCMessageData - * An object containing the parsed {@link MUCMessageAttributes} and - * current {@link ChatRoom}. - * @property { MUCMessageAttributes } attrs - * @property { ChatRoom } chatbox - */ let attrs; try { attrs = await parseMUCMessage(stanza, this); @@ -598,10 +610,15 @@ class MUC extends ChatBox { } const data = { stanza, attrs, 'chatbox': this }; /** + * An object containing the parsed {@link MUCMessageAttributes} and current {@link MUC}. + * @typedef {object} MUCMessageData + * @property {MUCMessageAttributes} attrs + * @property {MUC} chatbox + * * Triggered when a groupchat message stanza has been received and parsed. * @event _converse#message - * @type { object } - * @property { module:converse-muc~MUCMessageData } data + * @type {object} + * @property {MUCMessageData} data */ api.trigger('message', data); return attrs && this.queueMessage(attrs); @@ -610,7 +627,7 @@ class MUC extends ChatBox { /** * Register presence and message handlers relevant to this groupchat * @private - * @method _converse.ChatRoom#registerHandlers + * @method MUC#registerHandlers */ registerHandlers () { const muc_jid = this.get('jid'); @@ -618,7 +635,10 @@ class MUC extends ChatBox { this.removeHandlers(); const connection = api.connection.get(); this.presence_handler = connection.addHandler( - stanza => this.onPresence(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.onPresence(stanza); + return true; + }, null, 'presence', null, @@ -628,7 +648,10 @@ class MUC extends ChatBox { ); this.domain_presence_handler = connection.addHandler( - stanza => this.onPresenceFromMUCHost(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.onPresenceFromMUCHost(stanza); + return true; + }, null, 'presence', null, @@ -637,7 +660,10 @@ class MUC extends ChatBox { ); this.message_handler = connection.addHandler( - stanza => !!this.handleMessageStanza(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.handleMessageStanza(stanza); + return true; + }, null, 'message', null, @@ -647,7 +673,10 @@ class MUC extends ChatBox { ); this.domain_message_handler = connection.addHandler( - stanza => this.handleMessageFromMUCHost(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.handleMessageFromMUCHost(stanza); + return true; + }, null, 'message', null, @@ -656,7 +685,10 @@ class MUC extends ChatBox { ); this.affiliation_message_handler = connection.addHandler( - stanza => this.handleAffiliationChangedMessage(stanza) || true, + (stanza) => { + this.handleAffiliationChangedMessage(stanza); + return true; + }, Strophe.NS.MUC_USER, 'message', null, @@ -714,19 +746,18 @@ class MUC extends ChatBox { * Sends a message stanza to the XMPP server and expects a reflection * or error message within a specific timeout period. * @private - * @method _converse.ChatRoom#sendTimedMessage - * @param { _converse.Message|Element } message + * @method MUC#sendTimedMessage + * @param {Strophe.Builder|Element } message * @returns { Promise|Promise } Returns a promise - * which resolves with the reflected message stanza or with an error stanza or {@link TimeoutError}. + * which resolves with the reflected message stanza or with an error stanza or + * {@link TimeoutError}. */ - sendTimedMessage (el) { - if (typeof el.tree === 'function') { - el = el.tree(); - } + sendTimedMessage (message) { + const el = message instanceof Element ? message : message.tree(); let id = el.getAttribute('id'); if (!id) { // inject id if not found - id = this.getUniqueId('sendIQ'); + id = getUniqueId('sendIQ'); el.setAttribute('id', id); } const promise = getOpenPromise(); @@ -749,9 +780,8 @@ class MUC extends ChatBox { /** * Retract one of your messages in this groupchat - * @private - * @method _converse.ChatRoom#retractOwnMessage - * @param { _converse.Message } message - The message which we're retracting. + * @method MUC#retractOwnMessage + * @param {MUCMessage} message - The message which we're retracting. */ async retractOwnMessage (message) { const __ = _converse.__; @@ -799,10 +829,9 @@ class MUC extends ChatBox { /** * Retract someone else's message in this groupchat. - * @private - * @method _converse.ChatRoom#retractOtherMessage - * @param { _converse.ChatRoomMessage } message - The message which we're retracting. - * @param { string } [reason] - The reason for retracting the message. + * @method MUC#retractOtherMessage + * @param {MUCMessage} message - The message which we're retracting. + * @param {string} [reason] - The reason for retracting the message. * @example * const room = await api.rooms.get(jid); * const message = room.messages.findWhere({'body': 'Get rich quick!'}); @@ -810,10 +839,11 @@ class MUC extends ChatBox { */ async retractOtherMessage (message, reason) { const editable = message.get('editable'); + const bare_jid = _converse.session.get('bare_jid'); // Optimistic save message.save({ 'moderated': 'retracted', - 'moderated_by': _converse.bare_jid, + 'moderated_by': bare_jid, 'moderated_id': message.get('msgid'), 'moderation_reason': reason, 'editable': false @@ -835,9 +865,9 @@ class MUC extends ChatBox { /** * Sends an IQ stanza to the XMPP server to retract a message in this groupchat. * @private - * @method _converse.ChatRoom#sendRetractionIQ - * @param { _converse.ChatRoomMessage } message - The message which we're retracting. - * @param { string } [reason] - The reason for retracting the message. + * @method MUC#sendRetractionIQ + * @param {MUCMessage} message - The message which we're retracting. + * @param {string} [reason] - The reason for retracting the message. */ sendRetractionIQ (message, reason) { const iq = $iq({ 'to': this.get('jid'), 'type': 'set' }) @@ -855,10 +885,10 @@ class MUC extends ChatBox { /** * Sends an IQ stanza to the XMPP server to destroy this groupchat. Not - * to be confused with the {@link _converse.ChatRoom#destroy} + * to be confused with the {@link MUC#destroy} * method, which simply removes the room from the local browser storage cache. * @private - * @method _converse.ChatRoom#sendDestroyIQ + * @method MUC#sendDestroyIQ * @param { string } [reason] - The reason for destroying the groupchat. * @param { string } [new_jid] - The JID of the new groupchat which replaces this one. */ @@ -882,7 +912,7 @@ class MUC extends ChatBox { /** * Leave the groupchat. * @private - * @method _converse.ChatRoom#leave + * @method MUC#leave * @param { string } [exit_msg] - Message to indicate your reason for leaving */ async leave (exit_msg) { @@ -898,7 +928,7 @@ class MUC extends ChatBox { ); } // Delete disco entity - const disco_entity = _converse.disco_entities?.get(this.get('jid')); + const disco_entity = _converse.state.disco_entities?.get(this.get('jid')); if (disco_entity) { await new Promise(resolve => disco_entity.destroy({ 'success': resolve, @@ -929,7 +959,7 @@ class MUC extends ChatBox { 'error': (_, e) => { log.error(e); resolve(); } }) ); - return _converse.ChatBox.prototype.close.call(this); + return _converse.exports.ChatBox.prototype.close.call(this); } canModerateMessages () { @@ -940,7 +970,7 @@ class MUC extends ChatBox { /** * Return an array of unique nicknames based on all occupants and messages in this MUC. * @private - * @method _converse.ChatRoom#getAllKnownNicknames + * @method MUC#getAllKnownNicknames * @returns { String[] } */ getAllKnownNicknames () { @@ -1043,7 +1073,7 @@ class MUC extends ChatBox { /** * Utility method to construct the JID for the current user as occupant of the groupchat. * @private - * @method _converse.ChatRoom#getRoomJIDAndNick + * @method MUC#getRoomJIDAndNick * @returns {string} - The groupchat JID with the user's nickname added at the end. * @example groupchat@conference.example.org/nickname */ @@ -1055,9 +1085,8 @@ class MUC extends ChatBox { /** * Sends a message with the current XEP-0085 chat state of the user - * as taken from the `chat_state` attribute of the {@link _converse.ChatRoom}. - * @private - * @method _converse.ChatRoom#sendChatState + * as taken from the `chat_state` attribute of the {@link MUC}. + * @method MUC#sendChatState */ sendChatState () { if ( @@ -1073,7 +1102,7 @@ class MUC extends ChatBox { return; } const chat_state = this.get('chat_state'); - if (chat_state === _converse.GONE) { + if (chat_state === GONE) { // is not applicable within MUC context return; } @@ -1090,7 +1119,7 @@ class MUC extends ChatBox { /** * Send a direct invitation as per XEP-0249 * @private - * @method _converse.ChatRoom#directInvite + * @method MUC#directInvite * @param { String } recipient - JID of the person being invited * @param { String } [reason] - Reason for the invitation */ @@ -1122,7 +1151,7 @@ class MUC extends ChatBox { * to a roster contact, asking them to join a room. * @event _converse#chatBoxMaximized * @type {object} - * @property {_converse.ChatRoom} room + * @property {MUC} room * @property {string} recipient - The JID of the person being invited * @property {string} reason - The original reason for the invitation * @example _converse.api.listen.on('chatBoxMaximized', view => { ... }); @@ -1135,9 +1164,9 @@ class MUC extends ChatBox { } /** - * Refresh the disco identity, features and fields for this {@link _converse.ChatRoom}. - * *features* are stored on the features {@link Model} attribute on this {@link _converse.ChatRoom}. - * *fields* are stored on the config {@link Model} attribute on this {@link _converse.ChatRoom}. + * Refresh the disco identity, features and fields for this {@link MUC}. + * *features* are stored on the features {@link Model} attribute on this {@link MUC}. + * *fields* are stored on the config {@link Model} attribute on this {@link MUC}. * @private * @returns {Promise} */ @@ -1152,7 +1181,7 @@ class MUC extends ChatBox { * Fetch the *extended* MUC info from the server and cache it locally * https://xmpp.org/extensions/xep-0045.html#disco-roominfo * @private - * @method _converse.ChatRoom#getDiscoInfo + * @method MUC#getDiscoInfo * @returns {Promise} */ getDiscoInfo () { @@ -1169,7 +1198,7 @@ class MUC extends ChatBox { * in the `config` {@link Model} attribute. * See: https://xmpp.org/extensions/xep-0045.html#disco-roominfo * @private - * @method _converse.ChatRoom#getDiscoInfoFields + * @method MUC#getDiscoInfoFields * @returns {Promise} */ async getDiscoInfoFields () { @@ -1186,9 +1215,9 @@ class MUC extends ChatBox { /** * Use converse-disco to populate the features {@link Model} which - * is stored as an attibute on this {@link _converse.ChatRoom}. + * is stored as an attibute on this {@link MUC}. * The results may be cached. If you want to force fetching the features from the - * server, call {@link _converse.ChatRoom#refreshDiscoInfo} instead. + * server, call {@link MUC#refreshDiscoInfo} instead. * @private * @returns {Promise} */ @@ -1219,8 +1248,9 @@ class MUC extends ChatBox { * Given a element, return a copy with a child if * we can find a value for it in this rooms config. * @private - * @method _converse.ChatRoom#addFieldValue - * @returns { Element } + * @method MUC#addFieldValue + * @param {Element} field + * @returns {Element} */ addFieldValue (field) { const type = field.getAttribute('type'); @@ -1250,8 +1280,8 @@ class MUC extends ChatBox { * Automatically configure the groupchat based on this model's * 'roomconfig' data. * @private - * @method _converse.ChatRoom#autoConfigureChatRoom - * @returns { Promise } + * @method MUC#autoConfigureChatRoom + * @returns {Promise} * Returns a promise which resolves once a response IQ has * been received. */ @@ -1269,7 +1299,7 @@ class MUC extends ChatBox { * Returns a promise which resolves once the response IQ * has been received. * @private - * @method _converse.ChatRoom#fetchRoomConfiguration + * @method MUC#fetchRoomConfiguration * @returns { Promise } */ fetchRoomConfiguration () { @@ -1279,7 +1309,7 @@ class MUC extends ChatBox { /** * Sends an IQ stanza with the groupchat configuration. * @private - * @method _converse.ChatRoom#sendConfiguration + * @method MUC#sendConfiguration * @param { Array } config - The groupchat configuration * @returns { Promise } - A promise which resolves with * the `result` stanza received from the XMPP server. @@ -1310,7 +1340,8 @@ class MUC extends ChatBox { if (!args.startsWith('@')) { args = '@' + args; } - const [_text, references] = this.parseTextForReferences(args); // eslint-disable-line no-unused-vars + const result = this.parseTextForReferences(args); + const references = result[1]; if (!references.length) { const message = __("Error: couldn't find a groupchat participant based on your arguments"); this.createMessage({ message, 'type': 'error' }); @@ -1349,7 +1380,8 @@ class MUC extends ChatBox { if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) { allowed_commands = [...allowed_commands, ...['subject', 'topic']]; } - const occupant = this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + const occupant = this.occupants.findWhere({ 'jid': bare_jid }); if (this.verifyAffiliations(['owner'], occupant, false)) { allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS); } else if (this.verifyAffiliations(['admin'], occupant, false)) { @@ -1377,7 +1409,8 @@ class MUC extends ChatBox { if (!affiliations.length) { return true; } - occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid }); if (occupant) { const a = occupant.get('affiliation'); if (affiliations.includes(a)) { @@ -1399,7 +1432,8 @@ class MUC extends ChatBox { if (!roles.length) { return true; } - occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid }); if (occupant) { const role = occupant.get('role'); if (roles.includes(role)) { @@ -1416,28 +1450,28 @@ class MUC extends ChatBox { /** * Returns the `role` which the current user has in this MUC * @private - * @method _converse.ChatRoom#getOwnRole + * @method MUC#getOwnRole * @returns { ('none'|'visitor'|'participant'|'moderator') } */ getOwnRole () { - return this.getOwnOccupant()?.attributes?.role; + return this.getOwnOccupant()?.get('role'); } /** * Returns the `affiliation` which the current user has in this MUC * @private - * @method _converse.ChatRoom#getOwnAffiliation + * @method MUC#getOwnAffiliation * @returns { ('none'|'outcast'|'member'|'admin'|'owner') } */ getOwnAffiliation () { - return this.getOwnOccupant()?.attributes?.affiliation || 'none'; + return this.getOwnOccupant()?.get('affiliation') || 'none'; } /** - * Get the {@link _converse.ChatRoomOccupant} instance which + * Get the {@link ChatRoomOccupant} instance which * represents the current user. - * @method _converse.ChatRoom#getOwnOccupant - * @returns { _converse.ChatRoomOccupant } + * @method MUC#getOwnOccupant + * @returns {ChatRoomOccupant} */ getOwnOccupant () { return this.occupants.getOwnOccupant(); @@ -1477,13 +1511,12 @@ class MUC extends ChatBox { /** * Send an IQ stanza to modify an occupant's role - * @private - * @method _converse.ChatRoom#setRole - * @param { _converse.ChatRoomOccupant } occupant - * @param { String } role - * @param { String } reason - * @param { function } onSuccess - callback for a succesful response - * @param { function } onError - callback for an error response + * @method MUC#setRole + * @param {ChatRoomOccupant} occupant + * @param {string} role + * @param {string} reason + * @param {function} onSuccess - callback for a succesful response + * @param {function} onError - callback for an error response */ setRole (occupant, role, reason, onSuccess, onError) { const item = $build('item', { @@ -1506,10 +1539,9 @@ class MUC extends ChatBox { } /** - * @private - * @method _converse.ChatRoom#getOccupant - * @param { String } nickname_or_jid - The nickname or JID of the occupant to be returned - * @returns { _converse.ChatRoomOccupant } + * @method MUC#getOccupant + * @param {string} nickname_or_jid - The nickname or JID of the occupant to be returned + * @returns {ChatRoomOccupant} */ getOccupant (nickname_or_jid) { return u.isValidJID(nickname_or_jid) @@ -1519,38 +1551,36 @@ class MUC extends ChatBox { /** * Return an array of occupant models that have the required role - * @private - * @method _converse.ChatRoom#getOccupantsWithRole - * @param { String } role - * @returns { _converse.ChatRoomOccupant[] } + * @method MUC#getOccupantsWithRole + * @param {string} role + * @returns {{jid: string, nick: string, role: string}[]} */ getOccupantsWithRole (role) { return this.getOccupantsSortedBy('nick') .filter(o => o.get('role') === role) .map(item => { return { - 'jid': item.get('jid'), - 'nick': item.get('nick'), - 'role': item.get('role') + jid: /** @type {string} */item.get('jid'), + nick: /** @type {string} */item.get('nick'), + role: /** @type {string} */item.get('role') }; }); } /** * Return an array of occupant models that have the required affiliation - * @private - * @method _converse.ChatRoom#getOccupantsWithAffiliation - * @param { String } affiliation - * @returns { _converse.ChatRoomOccupant[] } + * @method MUC#getOccupantsWithAffiliation + * @param {string} affiliation + * @returns {{jid: string, nick: string, affiliation: string}[]} */ getOccupantsWithAffiliation (affiliation) { return this.getOccupantsSortedBy('nick') .filter(o => o.get('affiliation') === affiliation) .map(item => { return { - 'jid': item.get('jid'), - 'nick': item.get('nick'), - 'affiliation': item.get('affiliation') + jid: /** @type {string} */item.get('jid'), + nick: /** @type {string} */item.get('nick'), + affiliation: /** @type {string} */item.get('affiliation') }; }); } @@ -1558,9 +1588,9 @@ class MUC extends ChatBox { /** * Return an array of occupant models, sorted according to the passed-in attribute. * @private - * @method _converse.ChatRoom#getOccupantsSortedBy - * @param { String } attr - The attribute to sort the returned array by - * @returns { _converse.ChatRoomOccupant[] } + * @method MUC#getOccupantsSortedBy + * @param {string} attr - The attribute to sort the returned array by + * @returns {ChatRoomOccupant[]} */ getOccupantsSortedBy (attr) { return Array.from(this.occupants.models).sort((a, b) => @@ -1574,19 +1604,38 @@ class MUC extends ChatBox { * the passed in members, and if it exists, send the delta * to the XMPP server to update the member list. * @private - * @method _converse.ChatRoom#updateMemberLists - * @param { object } members - Map of member jids and affiliations. - * @returns { Promise } + * @method MUC#updateMemberLists + * @param {object} members - Map of member jids and affiliations. + * @returns {Promise} * A promise which is resolved once the list has been * updated or once it's been established there's no need * to update the list. */ async updateMemberLists (members) { const muc_jid = this.get('jid'); + /** @type {Array} */ const all_affiliations = ['member', 'admin', 'owner']; - const aff_lists = await Promise.all(all_affiliations.map(a => getAffiliationList(a, muc_jid))); - const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []); - await setAffiliations(muc_jid, computeAffiliationsDelta(true, false, members, old_members)); + const aff_lists = await Promise.all(all_affiliations.map((a) => getAffiliationList(a, muc_jid))); + + const old_members = aff_lists.reduce( + /** + * @param {MemberListItem[]} acc + * @param {MemberListItem[]|Error} val + * @returns {MemberListItem[]} + */ + (acc, val) => { + if (val instanceof Error) { + log.error(val); + return acc; + } + return [...val, ...acc]; + }, [] + ); + + await setAffiliations( + muc_jid, + computeAffiliationsDelta(true, false, members, /** @type {MemberListItem[]} */(old_members)) + ); await this.occupants.fetchMembers(); } @@ -1594,12 +1643,12 @@ class MUC extends ChatBox { * Given a nick name, save it to the model state, otherwise, look * for a server-side reserved nickname or default configured * nickname and if found, persist that to the model state. - * @private - * @method _converse.ChatRoom#getAndPersistNickname - * @returns { Promise } A promise which resolves with the nickname + * @method MUC#getAndPersistNickname + * @param {string} nick + * @returns {Promise} A promise which resolves with the nickname */ async getAndPersistNickname (nick) { - nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.getDefaultMUCNickname(); + nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.exports.getDefaultMUCNickname(); if (nick) safeSave(this, { nick }, { 'silent': true }); return nick; } @@ -1609,7 +1658,7 @@ class MUC extends ChatBox { * this user has a reserved nickname for this groupchat. * If so, we'll use that, otherwise we render the nickname form. * @private - * @method _converse.ChatRoom#getReservedNick + * @method MUC#getReservedNick * @returns { Promise } A promise which resolves with the reserved nick or null */ async getReservedNick () { @@ -1622,7 +1671,7 @@ class MUC extends ChatBox { 'node': 'x-roomuser-item' }); const result = await api.sendIQ(stanza, null, false); - if (u.isErrorObject(result)) { + if (isErrorObject(result)) { throw result; } // Result might be undefined due to a timeout @@ -1637,7 +1686,7 @@ class MUC extends ChatBox { * users from using it in this MUC. * See https://xmpp.org/extensions/xep-0045.html#register * @private - * @method _converse.ChatRoom#registerNickname + * @method MUC#registerNickname */ async registerNickname () { const { __ } = _converse; @@ -1690,8 +1739,8 @@ class MUC extends ChatBox { /** * Check whether we should unregister the user from this MUC, and if so, - * call { @link _converse.ChatRoom#sendUnregistrationIQ } - * @method _converse.ChatRoom#unregisterNickname + * call { @link MUC#sendUnregistrationIQ } + * @method MUC#unregisterNickname */ async unregisterNickname () { if (api.settings.get('auto_register_muc_nickname') === 'unregister') { @@ -1710,7 +1759,7 @@ class MUC extends ChatBox { * If the user had a 'member' affiliation, it'll be removed and their * nickname will no longer be reserved and can instead be used (and * registered) by other users. - * @method _converse.ChatRoom#sendUnregistrationIQ + * @method MUC#sendUnregistrationIQ */ sendUnregistrationIQ () { const iq = $iq({ 'to': this.get('jid'), 'type': 'set' }) @@ -1722,7 +1771,7 @@ class MUC extends ChatBox { /** * Given a presence stanza, update the occupant model based on its contents. * @private - * @method _converse.ChatRoom#updateOccupantsOnPresence + * @method MUC#updateOccupantsOnPresence * @param { Element } pres - The presence stanza */ updateOccupantsOnPresence (pres) { @@ -1788,8 +1837,7 @@ class MUC extends ChatBox { /** * Given two JIDs, which can be either user JIDs or MUC occupant JIDs, * determine whether they belong to the same user. - * @private - * @method _converse.ChatRoom#isSameUser + * @method MUC#isSameUser * @param { String } jid1 * @param { String } jid2 * @returns { Boolean } @@ -1841,7 +1889,7 @@ class MUC extends ChatBox { /** * Handle a possible subject change and return `true` if so. * @private - * @method _converse.ChatRoom#handleSubjectChange + * @method MUC#handleSubjectChange * @param { object } attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} */ @@ -1876,9 +1924,9 @@ class MUC extends ChatBox { } /** - * Set the subject for this {@link _converse.ChatRoom} + * Set the subject for this {@link MUC} * @private - * @method _converse.ChatRoom#setSubject + * @method MUC#setSubject * @param { String } value */ setSubject (value = '') { @@ -1898,7 +1946,7 @@ class MUC extends ChatBox { * Is this a chat state notification that can be ignored, * because it's old or because it's from us. * @private - * @method _converse.ChatRoom#ignorableCSN + * @method MUC#ignorableCSN * @param { Object } attrs - The message attributes */ ignorableCSN (attrs) { @@ -1908,15 +1956,15 @@ class MUC extends ChatBox { /** * Determines whether the message is from ourselves by checking * the `from` attribute. Doesn't check the `type` attribute. - * @method _converse.ChatRoom#isOwnMessage - * @param {Object|Element|_converse.Message} msg + * @method MUC#isOwnMessage + * @param {Object|Element|MUCMessage} msg * @returns {boolean} */ isOwnMessage (msg) { let from; if (msg instanceof Element) { from = msg.getAttribute('from'); - } else if (msg instanceof _converse.Message) { + } else if (msg instanceof _converse.exports.MUCMessage) { from = msg.get('from'); } else { from = msg.from; @@ -1926,7 +1974,7 @@ class MUC extends ChatBox { getUpdatedMessageAttributes (message, attrs) { const new_attrs = { - ..._converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs), + ..._converse.exports.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs), ...pick(attrs, ['from_muc', 'occupant_id']), } @@ -1945,7 +1993,7 @@ class MUC extends ChatBox { * whether we're still joined. * @async * @private - * @method _converse.ChatRoom#isJoined + * @method MUC#isJoined * @returns {Promise} */ async isJoined () { @@ -1961,7 +2009,7 @@ class MUC extends ChatBox { /** * Sends a status update presence (i.e. based on the `` element) - * @method _converse.ChatRoom#sendStatusPresence + * @method MUC#sendStatusPresence * @param { String } type * @param { String } [status] - An optional status message * @param { Element[]|Strophe.Builder[]|Element|Strophe.Builder } [child_nodes] @@ -1969,7 +2017,7 @@ class MUC extends ChatBox { */ async sendStatusPresence (type, status, child_nodes) { if (this.session.get('connection_status') === ROOMSTATUS.ENTERED) { - const presence = await _converse.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status); + const presence = await _converse.state.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status); child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up()); api.send(presence); } @@ -1977,7 +2025,7 @@ class MUC extends ChatBox { /** * Check whether we're still joined and re-join if not - * @method _converse.ChatRoom#rejoinIfNecessary + * @method MUC#rejoinIfNecessary */ async rejoinIfNecessary () { if (this.isRAICandidate()) { @@ -1992,8 +2040,8 @@ class MUC extends ChatBox { } /** - * @private - * @method _converse.ChatRoom#shouldShowErrorMessage + * @method MUC#shouldShowErrorMessage + * @param {object} attrs * @returns {Promise} */ async shouldShowErrorMessage (attrs) { @@ -2007,7 +2055,7 @@ class MUC extends ChatBox { } else if (attrs.error_condition === 'not-acceptable' && (await this.rejoinIfNecessary())) { return false; } - return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs); + return _converse.exports.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs); } /** @@ -2016,10 +2064,10 @@ class MUC extends ChatBox { * it probably hasn't been applied to anything yet, given that * the relevant message is only coming in now. * @private - * @method _converse.ChatRoom#findDanglingModeration + * @method MUC#findDanglingModeration * @param { object } attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} - * @returns { _converse.ChatRoomMessage } + * @returns {MUCMessage} */ findDanglingModeration (attrs) { if (!this.messages.length) { @@ -2047,7 +2095,7 @@ class MUC extends ChatBox { /** * Handles message moderation based on the passed in attributes. * @private - * @method _converse.ChatRoom#handleModeration + * @method MUC#handleModeration * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} * @returns {Promise} Returns `true` or `false` depending on @@ -2106,7 +2154,7 @@ class MUC extends ChatBox { return `${result}${__('%1$s is typing', actors[0])}\n`; } else if (state === 'paused') { return `${result}${__('%1$s has stopped typing', actors[0])}\n`; - } else if (state === _converse.GONE) { + } else if (state === GONE) { return `${result}${__('%1$s has gone away', actors[0])}\n`; } else if (state === 'entered') { return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`; @@ -2136,7 +2184,7 @@ class MUC extends ChatBox { return `${result}${__('%1$s are typing', actors_str)}\n`; } else if (state === 'paused') { return `${result}${__('%1$s have stopped typing', actors_str)}\n`; - } else if (state === _converse.GONE) { + } else if (state === GONE) { return `${result}${__('%1$s have gone away', actors_str)}\n`; } else if (state === 'entered') { return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`; @@ -2230,7 +2278,7 @@ class MUC extends ChatBox { /** * Given {@link MessageAttributes} look for XEP-0316 Room Notifications and create info * messages for them. - * @param { Element } stanza + * @param {MessageAttributes} attrs */ handleMEPNotification (attrs) { if (attrs.from !== this.get('jid') || !attrs.activities) { @@ -2248,30 +2296,30 @@ class MUC extends ChatBox { /** * Returns an already cached message (if it exists) based on the * passed in attributes map. - * @method _converse.ChatRoom#getDuplicateMessage - * @param { object } attrs - Attributes representing a received + * @method MUC#getDuplicateMessage + * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} - * @returns {Promise<_converse.Message>} + * @returns {MUCMessage} */ getDuplicateMessage (attrs) { if (attrs.activities?.length) { return this.messages.findWhere({'type': 'mep', 'msgid': attrs.msgid}); } else { - return _converse.ChatBox.prototype.getDuplicateMessage.call(this, attrs); + return _converse.exports.ChatBox.prototype.getDuplicateMessage.call(this, attrs); } } /** * Handler for all MUC messages sent to this groupchat. This method - * shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage} + * shouldn't be called directly, instead {@link MUC#queueMessage} * should be called. - * @method _converse.ChatRoom#onMessage - * @param { MessageAttributes } attrs - A promise which resolves to the message attributes. + * @method MUC#onMessage + * @param {MessageAttributes} attrs - A promise which resolves to the message attributes. */ async onMessage (attrs) { attrs = await attrs; - if (u.isErrorObject(attrs)) { + if (isErrorObject(attrs)) { attrs.stanza && log.error(attrs.stanza); return log.error(attrs.message); } else if (attrs.type === 'error' && !(await this.shouldShowErrorMessage(attrs))) { @@ -2309,6 +2357,9 @@ class MUC extends ChatBox { } } + /** + * @param {Element} pres + */ handleModifyError (pres) { const text = pres.querySelector('error text')?.textContent; if (text) { @@ -2335,7 +2386,7 @@ class MUC extends ChatBox { if (!x) { return; } - const disconnection_codes = Object.keys(_converse.muc.disconnect_messages); + const disconnection_codes = Object.keys(_converse.labels.muc.disconnect_messages); const codes = sizzle('status', x) .map(s => s.getAttribute('code')) .filter(c => disconnection_codes.includes(c)); @@ -2350,7 +2401,7 @@ class MUC extends ChatBox { const item = x.querySelector('item'); const reason = item ? item.querySelector('reason')?.textContent : undefined; const actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined; - const message = _converse.muc.disconnect_messages[codes[0]]; + const message = _converse.labels.muc.disconnect_messages[codes[0]]; const status = codes.includes('301') ? ROOMSTATUS.BANNED : ROOMSTATUS.DISCONNECTED; this.setDisconnectionState(message, reason, actor, status); } @@ -2384,19 +2435,19 @@ class MUC extends ChatBox { } const current_affiliation = occupant.get('affiliation'); - if (previous_affiliation === 'admin' && _converse.isInfoVisible(converse.AFFILIATION_CHANGES.EXADMIN)) { + if (previous_affiliation === 'admin' && isInfoVisible(converse.AFFILIATION_CHANGES.EXADMIN)) { this.createMessage({ 'type': 'info', 'message': __('%1$s is no longer an admin of this groupchat', occupant.get('nick')) }); - } else if (previous_affiliation === 'owner' && _converse.isInfoVisible(converse.AFFILIATION_CHANGES.EXOWNER)) { + } else if (previous_affiliation === 'owner' && isInfoVisible(converse.AFFILIATION_CHANGES.EXOWNER)) { this.createMessage({ 'type': 'info', 'message': __('%1$s is no longer an owner of this groupchat', occupant.get('nick')) }); } else if ( previous_affiliation === 'outcast' && - _converse.isInfoVisible(converse.AFFILIATION_CHANGES.EXOUTCAST) + isInfoVisible(converse.AFFILIATION_CHANGES.EXOUTCAST) ) { this.createMessage({ 'type': 'info', @@ -2407,7 +2458,7 @@ class MUC extends ChatBox { if ( current_affiliation === 'none' && previous_affiliation === 'member' && - _converse.isInfoVisible(converse.AFFILIATION_CHANGES.EXMEMBER) + isInfoVisible(converse.AFFILIATION_CHANGES.EXMEMBER) ) { this.createMessage({ 'type': 'info', @@ -2415,14 +2466,14 @@ class MUC extends ChatBox { }); } - if (current_affiliation === 'member' && _converse.isInfoVisible(converse.AFFILIATION_CHANGES.MEMBER)) { + if (current_affiliation === 'member' && isInfoVisible(converse.AFFILIATION_CHANGES.MEMBER)) { this.createMessage({ 'type': 'info', 'message': __('%1$s is now a member of this groupchat', occupant.get('nick')) }); } else if ( - (current_affiliation === 'admin' && _converse.isInfoVisible(converse.AFFILIATION_CHANGES.ADMIN)) || - (current_affiliation == 'owner' && _converse.isInfoVisible(converse.AFFILIATION_CHANGES.OWNER)) + (current_affiliation === 'admin' && isInfoVisible(converse.AFFILIATION_CHANGES.ADMIN)) || + (current_affiliation == 'owner' && isInfoVisible(converse.AFFILIATION_CHANGES.OWNER)) ) { // For example: AppleJack is now an (admin|owner) of this groupchat this.createMessage({ @@ -2438,17 +2489,17 @@ class MUC extends ChatBox { return; } const previous_role = occupant._previousAttributes.role; - if (previous_role === 'moderator' && _converse.isInfoVisible(converse.MUC_ROLE_CHANGES.DEOP)) { + if (previous_role === 'moderator' && isInfoVisible(converse.MUC_ROLE_CHANGES.DEOP)) { this.updateNotifications(occupant.get('nick'), converse.MUC_ROLE_CHANGES.DEOP); - } else if (previous_role === 'visitor' && _converse.isInfoVisible(converse.MUC_ROLE_CHANGES.VOICE)) { + } else if (previous_role === 'visitor' && isInfoVisible(converse.MUC_ROLE_CHANGES.VOICE)) { this.updateNotifications(occupant.get('nick'), converse.MUC_ROLE_CHANGES.VOICE); } - if (occupant.get('role') === 'visitor' && _converse.isInfoVisible(converse.MUC_ROLE_CHANGES.MUTE)) { + if (occupant.get('role') === 'visitor' && isInfoVisible(converse.MUC_ROLE_CHANGES.MUTE)) { this.updateNotifications(occupant.get('nick'), converse.MUC_ROLE_CHANGES.MUTE); } else if (occupant.get('role') === 'moderator') { if ( !['owner', 'admin'].includes(occupant.get('affiliation')) && - _converse.isInfoVisible(converse.MUC_ROLE_CHANGES.OP) + isInfoVisible(converse.MUC_ROLE_CHANGES.OP) ) { // Oly show this message if the user isn't already // an admin or owner, otherwise this isn't new information. @@ -2460,7 +2511,7 @@ class MUC extends ChatBox { /** * Create an info message based on a received MUC status code * @private - * @method _converse.ChatRoom#createInfoMessage + * @method MUC#createInfoMessage * @param { string } code - The MUC status code * @param { Element } stanza - The original stanza that contains the code * @param { Boolean } is_self - Whether this stanza refers to our own presence @@ -2468,20 +2519,22 @@ class MUC extends ChatBox { createInfoMessage (code, stanza, is_self) { const __ = _converse.__; const data = { 'type': 'info', 'is_ephemeral': true }; - if (!_converse.isInfoVisible(code)) { + const { info_messages, new_nickname_messages } = _converse.labels.muc; + + if (!isInfoVisible(code)) { return; } if (code === '110' || (code === '100' && !is_self)) { return; - } else if (code in _converse.muc.info_messages) { - data.message = _converse.muc.info_messages[code]; + } else if (code in info_messages) { + data.message = info_messages[code]; } else if (!is_self && ACTION_INFO_CODES.includes(code)) { const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop(); data.actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined; data.reason = item ? item.querySelector('reason')?.textContent : undefined; data.message = this.getActionInfoMessage(code, nick, data.actor); - } else if (is_self && code in _converse.muc.new_nickname_messages) { + } else if (is_self && code in new_nickname_messages) { // XXX: Side-effect of setting the nick. Should ideally be refactored out of this method let nick; if (code === '210') { @@ -2490,7 +2543,7 @@ class MUC extends ChatBox { nick = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop().getAttribute('nick'); } this.save('nick', nick); - data.message = __(_converse.muc.new_nickname_messages[code], nick); + data.message = __(new_nickname_messages[code], nick); } if (data.message) { @@ -2504,7 +2557,7 @@ class MUC extends ChatBox { /** * Create info messages based on a received presence or message stanza * @private - * @method _converse.ChatRoom#createInfoMessages + * @method MUC#createInfoMessages * @param { Element } stanza */ createInfoMessages (stanza) { @@ -2520,11 +2573,11 @@ class MUC extends ChatBox { /** * Set parameters regarding disconnection from this room. This helps to * communicate to the user why they were disconnected. - * @param { String } message - The disconnection message, as received from (or + * @param {string} message - The disconnection message, as received from (or * implied by) the server. - * @param { String } reason - The reason provided for the disconnection - * @param { String } actor - The person (if any) responsible for this disconnection - * @param { number } status - The status code (see `ROOMSTATUS`) + * @param {string} [reason] - The reason provided for the disconnection + * @param {string} [actor] - The person (if any) responsible for this disconnection + * @param {number} [status] - The status code (see `ROOMSTATUS`) */ setDisconnectionState (message, reason, actor, status=ROOMSTATUS.DISCONNECTED) { this.session.save({ @@ -2535,11 +2588,14 @@ class MUC extends ChatBox { }); } + /** + * @param {Element} presence + */ onNicknameClash (presence) { const __ = _converse.__; if (api.settings.get('muc_nickname_from_jid')) { const nick = presence.getAttribute('from').split('/')[1]; - if (nick === _converse.getDefaultMUCNickname()) { + if (nick === _converse.exports.getDefaultMUCNickname()) { this.join(nick + '-2'); } else { const del = nick.lastIndexOf('-'); @@ -2558,7 +2614,7 @@ class MUC extends ChatBox { /** * Parses a stanza with type "error" and sets the proper - * `connection_status` value for this {@link _converse.ChatRoom} as + * `connection_status` value for this {@link MUC} as * well as any additional output that can be shown to the user. * @private * @param { Element } stanza - The presence stanza @@ -2581,7 +2637,7 @@ class MUC extends ChatBox { this.setDisconnectionState(message, reason); } else if (error.querySelector('forbidden')) { this.setDisconnectionState( - _converse.muc.disconnect_messages[301], + _converse.labels.muc.disconnect_messages[301], reason, null, ROOMSTATUS.BANNED @@ -2625,7 +2681,7 @@ class MUC extends ChatBox { /** * Listens for incoming presence stanzas from the service that hosts this MUC * @private - * @method _converse.ChatRoom#onPresenceFromMUCHost + * @method MUC#onPresenceFromMUCHost * @param { Element } stanza - The presence stanza */ onPresenceFromMUCHost (stanza) { @@ -2644,7 +2700,7 @@ class MUC extends ChatBox { /** * Handles incoming presence stanzas coming from the MUC * @private - * @method _converse.ChatRoom#onPresence + * @method MUC#onPresence * @param { Element } stanza */ onPresence (stanza) { @@ -2677,8 +2733,8 @@ class MUC extends ChatBox { * auto-configured only if applicable and if the current * user is the groupchat's owner. * @private - * @method _converse.ChatRoom#onOwnPresence - * @param { Element } pres - The stanza + * @method MUC#onOwnPresence + * @param {Element} stanza - The stanza */ async onOwnPresence (stanza) { await this.occupants.fetched; @@ -2718,9 +2774,8 @@ class MUC extends ChatBox { /** * Returns a boolean to indicate whether the current user * was mentioned in a message. - * @private - * @method _converse.ChatRoom#isUserMentioned - * @param { String } - The text message + * @method MUC#isUserMentioned + * @param {MUCMessage} message - The text message */ isUserMentioned (message) { const nick = this.get('nick'); diff --git a/src/headless/plugins/muc/occupant.js b/src/headless/plugins/muc/occupant.js index 65feb417dd..7e02646f6e 100644 --- a/src/headless/plugins/muc/occupant.js +++ b/src/headless/plugins/muc/occupant.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; /** * Represents a participant in a MUC diff --git a/src/headless/plugins/muc/occupants.js b/src/headless/plugins/muc/occupants.js index c47c6e5b04..3b59226488 100644 --- a/src/headless/plugins/muc/occupants.js +++ b/src/headless/plugins/muc/occupants.js @@ -1,12 +1,14 @@ +/** + * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem + */ import ChatRoomOccupant from './occupant.js'; import _converse from '../../shared/_converse.js'; +import log from '../../log'; import api, { converse } from '../../shared/api/index.js'; -import { Collection } from '@converse/skeletor/src/collection.js'; -import { MUC_ROLE_WEIGHTS } from './constants.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Collection, Model } from '@converse/skeletor'; import { Strophe } from 'strophe.js'; import { getAffiliationList } from './affiliations/utils.js'; -import { getAutoFetchedAffiliationLists } from './utils.js'; +import { getAutoFetchedAffiliationLists, occupantsComparator } from './utils.js'; import { getUniqueId } from '../../utils/index.js'; const { u } = converse.env; @@ -19,18 +21,22 @@ const { u } = converse.env; * @memberOf _converse */ class ChatRoomOccupants extends Collection { - model = ChatRoomOccupant; - - comparator (occupant1, occupant2) { // eslint-disable-line class-methods-use-this - const role1 = occupant1.get('role') || 'none'; - const role2 = occupant2.get('role') || 'none'; - if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) { - const nick1 = occupant1.getDisplayName().toLowerCase(); - const nick2 = occupant2.getDisplayName().toLowerCase(); - return nick1 < nick2 ? -1 : nick1 > nick2 ? 1 : 0; - } else { - return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1; - } + + constructor (attrs, options) { + super( + attrs, + Object.assign({ comparator: occupantsComparator }, options) + ); + this.chatroom = null; + } + + get model() { + return ChatRoomOccupant; + } + + initialize() { + this.on('change:nick', () => this.sort()); + this.on('change:role', () => this.sort()); } create (attrs, options) { @@ -52,12 +58,31 @@ class ChatRoomOccupants extends Collection { } const muc_jid = this.chatroom.get('jid'); const aff_lists = await Promise.all(affiliations.map(a => getAffiliationList(a, muc_jid))); - const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []); + + + const new_members = aff_lists.reduce( + /** + * @param {MemberListItem[]} acc + * @param {MemberListItem[]|Error} val + * @returns {MemberListItem[]} + */ + (acc, val) => { + if (val instanceof Error) { + log.error(val); + return acc; + } + return [...val, ...acc]; + }, [] + ); + const known_affiliations = affiliations.filter( a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)]) ); - const new_jids = new_members.map(m => m.jid).filter(m => m !== undefined); - const new_nicks = new_members.map(m => (!m.jid && m.nick) || undefined).filter(m => m !== undefined); + const new_jids = /** @type {MemberListItem[]} */(new_members).map(m => m.jid).filter(m => m !== undefined); + + const new_nicks = /** @type {MemberListItem[]} */(new_members).map( + (m) => (!m.jid && m.nick) || undefined).filter(m => m !== undefined); + const removed_members = this.filter(m => { return ( known_affiliations.includes(m.get('affiliation')) && @@ -65,8 +90,10 @@ class ChatRoomOccupants extends Collection { !new_jids.includes(m.get('jid')) ); }); + + const bare_jid = _converse.session.get('bare_jid'); removed_members.forEach(occupant => { - if (occupant.get('jid') === _converse.bare_jid) { + if (occupant.get('jid') === bare_jid) { return; } else if (occupant.get('show') === 'offline') { occupant.destroy(); @@ -74,7 +101,7 @@ class ChatRoomOccupants extends Collection { occupant.save('affiliation', null); } }); - new_members.forEach(attrs => { + /** @type {MemberListItem[]} */(new_members).forEach(attrs => { const occupant = this.findOccupant(attrs); occupant ? occupant.save(attrs) : this.create(attrs); }); @@ -117,14 +144,15 @@ class ChatRoomOccupants extends Collection { } /** - * Get the {@link _converse.ChatRoomOccupant} instance which + * Get the {@link ChatRoomOccupant} instance which * represents the current user. * @method _converse.ChatRoomOccupants#getOwnOccupant - * @returns { _converse.ChatRoomOccupant } + * @returns {ChatRoomOccupant} */ getOwnOccupant () { + const bare_jid = _converse.session.get('bare_jid'); return this.findOccupant({ - 'jid': _converse.bare_jid, + 'jid': bare_jid, 'occupant_id': this.chatroom.get('occupant_id') }); } diff --git a/src/headless/plugins/muc/parsers.js b/src/headless/plugins/muc/parsers.js index c9a96789f5..f4c7ea987c 100644 --- a/src/headless/plugins/muc/parsers.js +++ b/src/headless/plugins/muc/parsers.js @@ -1,3 +1,8 @@ +/** + * @module:plugin-muc-parsers + * @typedef {import('../muc/muc.js').default} MUC + * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes + */ import dayjs from 'dayjs'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; @@ -72,10 +77,9 @@ function getJIDFromMUCUserData (stanza) { /** * @private - * @param { Element } stanza - The message stanza - * @param { Element } original_stanza - The original stanza, that contains the + * @param {Element} stanza - The message stanza * message stanza, if it was contained, otherwise it's the message stanza itself. - * @returns { Object } + * @returns {Object} */ function getModerationAttributes (stanza) { const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop(); @@ -121,9 +125,9 @@ function getOccupantID (stanza, chatbox) { /** * Determines whether the sender of this MUC message is the current user or * someone else. - * @param { MUCMessageAttributes } attrs - * @param { _converse.ChatRoom } chatbox - * @returns { 'me'|'them' } + * @param {MUCMessageAttributes} attrs + * @param {MUC} chatbox + * @returns {'me'|'them'} */ function getSender (attrs, chatbox) { let is_me; @@ -132,7 +136,8 @@ function getSender (attrs, chatbox) { if (own_occupant_id) { is_me = attrs.occupant_id === own_occupant_id; } else if (attrs.from_real_jid) { - is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === bare_jid; } else { is_me = attrs.nick === chatbox.get('nick') } @@ -141,12 +146,9 @@ function getSender (attrs, chatbox) { /** * Parses a passed in message stanza and returns an object of attributes. - * @param { Element } stanza - The message stanza - * @param { Element } original_stanza - The original stanza, that contains the - * message stanza, if it was contained, otherwise it's the message stanza itself. - * @param { _converse.ChatRoom } chatbox - * @param { _converse } _converse - * @returns { Promise } + * @param {Element} stanza - The message stanza + * @param {MUC} chatbox + * @returns {Promise} */ export async function parseMUCMessage (stanza, chatbox) { throwErrorIfInvalidForward(stanza); @@ -257,7 +259,7 @@ export async function parseMUCMessage (stanza, chatbox) { getOpenGraphMetadata(stanza), getRetractionAttributes(stanza, original_stanza), getModerationAttributes(stanza), - getEncryptionAttributes(stanza, _converse), + getEncryptionAttributes(stanza), ); await api.emojis.initialize(); @@ -303,20 +305,19 @@ export async function parseMUCMessage (stanza, chatbox) { /** * Given an IQ stanza with a member list, create an array of objects containing * known member data (e.g. jid, nick, role, affiliation). - * @private + * + * @typedef {Object} MemberListItem + * Either the JID or the nickname (or both) will be available. + * @property {string} affiliation + * @property {string} [role] + * @property {string} [jid] + * @property {string} [nick] + * * @method muc_utils#parseMemberListIQ * @returns { MemberListItem[] } */ export function parseMemberListIQ (iq) { return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map(item => { - /** - * @typedef {Object} MemberListItem - * Either the JID or the nickname (or both) will be available. - * @property {string} affiliation - * @property {string} [role] - * @property {string} [jid] - * @property {string} [nick] - */ const data = { 'affiliation': item.getAttribute('affiliation') }; @@ -343,21 +344,28 @@ export function parseMemberListIQ (iq) { /** * Parses a passed in MUC presence stanza and returns an object of attributes. * @method parseMUCPresence - * @param { Element } stanza - The presence stanza - * @param { _converse.ChatRoom } chatbox - * @returns { MUCPresenceAttributes } + * @param {Element} stanza - The presence stanza + * @param {MUC} chatbox + * @returns {MUCPresenceAttributes} */ export function parseMUCPresence (stanza, chatbox) { /** - * @typedef { Object } MUCPresenceAttributes + * Object representing a XEP-0371 Hat + * @typedef {Object} MUCHat + * @property {string} title + * @property {string} uri + * * The object which {@link parseMUCPresence} returns - * @property { ("offline|online") } show - * @property { Array } hats - An array of XEP-0317 hats - * @property { Array } states - * @property { String } from - The sender JID (${muc_jid}/${nick}) - * @property { String } nick - The nickname of the sender - * @property { String } occupant_id - The XEP-0421 occupant ID - * @property { String } type - The type of presence + * @typedef {Object} MUCPresenceAttributes + * @property {string} show + * @property {Array} hats - An array of XEP-0317 hats + * @property {Array} states + * @property {String} from - The sender JID (${muc_jid}/${nick}) + * @property {String} nick - The nickname of the sender + * @property {String} occupant_id - The XEP-0421 occupant ID + * @property {String} type - The type of presence + * @property {String} [jid] + * @property {boolean} [is_me] */ const from = stanza.getAttribute('from'); const type = stanza.getAttribute('type'); @@ -391,12 +399,6 @@ export function parseMUCPresence (stanza, chatbox) { } else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) { data.image_hash = child.querySelector('photo')?.textContent; } else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) { - /** - * @typedef { Object } MUCHat - * Object representing a XEP-0371 Hat - * @property { String } title - * @property { String } uri - */ data['hats'] = Array.from(child.children).map( c => c.matches('hat') && { diff --git a/src/headless/plugins/muc/utils.js b/src/headless/plugins/muc/utils.js index 4b332d0c6f..94f7a34de6 100644 --- a/src/headless/plugins/muc/utils.js +++ b/src/headless/plugins/muc/utils.js @@ -1,8 +1,13 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from '../../log.js'; -import { ROLES } from './constants.js'; +import { ROLES, MUC_ROLE_WEIGHTS } from './constants.js'; import { safeSave } from '../../utils/index.js'; +import { CHATROOMS_TYPE } from '../../shared/constants.js'; +import { getUnloadEvent } from '../../utils/session.js'; const { Strophe, sizzle, u } = converse.env; @@ -14,16 +19,27 @@ export function shouldCreateGroupchatMessage (attrs) { return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone); } - export function getAutoFetchedAffiliationLists () { const affs = api.settings.get('muc_fetch_members'); return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : []; } +export function occupantsComparator (occupant1, occupant2) { + const role1 = occupant1.get('role') || 'none'; + const role2 = occupant2.get('role') || 'none'; + if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) { + const nick1 = occupant1.getDisplayName().toLowerCase(); + const nick2 = occupant2.getDisplayName().toLowerCase(); + return nick1 < nick2 ? -1 : nick1 > nick2 ? 1 : 0; + } else { + return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1; + } +} + /** * Given an occupant model, see which roles may be assigned to that user. - * @param { Model } occupant - * @returns { Array<('moderator'|'participant'|'visitor')> } - An array of assignable roles + * @param {Model} occupant + * @returns {typeof ROLES} - An array of assignable roles */ export function getAssignableRoles (occupant) { let disabled = api.settings.get('modtools_disable_assign'); @@ -40,7 +56,7 @@ export function getAssignableRoles (occupant) { export function registerDirectInvitationHandler () { api.connection.get().addHandler( message => { - _converse.onDirectMUCInvitation(message); + _converse.exports.onDirectMUCInvitation(message); return true; }, 'jabber:x:conference', @@ -53,13 +69,13 @@ export function disconnectChatRooms () { * disconnected, so that they will be properly entered again * when fetched from session storage. */ - return _converse.chatboxes - .filter(m => m.get('type') === _converse.CHATROOMS_TYPE) + return _converse.state.chatboxes + .filter(m => m.get('type') === CHATROOMS_TYPE) .forEach(m => m.session.save({ 'connection_status': converse.ROOMSTATUS.DISCONNECTED })); } -export async function onWindowStateChanged (data) { - if (data.state === 'visible' && api.connection.connected()) { +export async function onWindowStateChanged () { + if (!document.hidden && api.connection.connected()) { const rooms = await api.rooms.get(); rooms.forEach(room => room.rejoinIfNecessary()); } @@ -89,7 +105,7 @@ export async function routeToRoom (event) { * "chatroom". */ export async function openChatRoom (jid, settings) { - settings.type = _converse.CHATROOMS_TYPE; + settings.type = CHATROOMS_TYPE; settings.id = jid; const chatbox = await api.rooms.get(jid, settings, true); chatbox.maybeShow(true); @@ -115,7 +131,7 @@ export async function onDirectMUCInvitation (message) { result = true; } else { // Invite request might come from someone not your roster list - const contact = _converse.roster.get(from)?.getDisplayName() ?? from; + const contact = _converse.state.roster.get(from)?.getDisplayName() ?? from; /** * *Hook* which is used to gather confirmation whether a direct MUC @@ -132,7 +148,7 @@ export async function onDirectMUCInvitation (message) { if (result) { const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') }); if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) { - _converse.chatboxes.get(room_jid).rejoin(); + _converse.state.chatboxes.get(room_jid).rejoin(); } } } @@ -140,16 +156,18 @@ export async function onDirectMUCInvitation (message) { export function getDefaultMUCNickname () { // XXX: if anything changes here, update the docs for the // locked_muc_nickname setting. - if (!_converse.xmppstatus) { + const { xmppstatus } = _converse.state; + if (!xmppstatus) { throw new Error( "Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired." ); } - const nick = _converse.xmppstatus.getNickname(); + const nick = xmppstatus.getNickname(); if (nick) { return nick; } else if (api.settings.get('muc_nickname_from_jid')) { - return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid)); + const bare_jid = _converse.session.get('bare_jid'); + return Strophe.unescapeNode(Strophe.getNodeFromJid(bare_jid)); } } @@ -177,7 +195,7 @@ export async function autoJoinRooms () { await Promise.all( api.settings.get('auto_join_rooms').map(muc => { if (typeof muc === 'string') { - if (_converse.chatboxes.where({ 'jid': muc }).length) { + if (_converse.state.chatboxes.where({ 'jid': muc }).length) { return Promise.resolve(); } return api.rooms.open(muc); @@ -209,13 +227,13 @@ export function onAddClientFeatures () { } export function onBeforeTearDown () { - _converse.chatboxes - .where({ 'type': _converse.CHATROOMS_TYPE }) + _converse.state.chatboxes + .where({ 'type': CHATROOMS_TYPE }) .forEach(muc => safeSave(muc.session, { 'connection_status': converse.ROOMSTATUS.DISCONNECTED })); } export function onStatusInitialized () { - window.addEventListener(_converse.unloadevent, () => { + window.addEventListener(getUnloadEvent(), () => { const using_websocket = api.connection.isType('websocket'); if ( using_websocket && @@ -233,9 +251,9 @@ export function onBeforeResourceBinding () { api.connection.get().addHandler( stanza => { const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); - if (!_converse.chatboxes.get(muc_jid)) { + if (!_converse.state.chatboxes.get(muc_jid)) { api.waitUntil('chatBoxesFetched').then(async () => { - const muc = _converse.chatboxes.get(muc_jid); + const muc = _converse.state.chatboxes.get(muc_jid); if (muc) { await muc.initialized; muc.message_handler.run(stanza); diff --git a/src/headless/plugins/ping/api.js b/src/headless/plugins/ping/api.js index 99dd8f9724..49d44d9598 100644 --- a/src/headless/plugins/ping/api.js +++ b/src/headless/plugins/ping/api.js @@ -9,11 +9,11 @@ export default { /** * Pings the entity represented by the passed in JID by sending an IQ stanza to it. * @method api.ping - * @param { String } [jid] - The JID of the service to ping + * @param {string} [jid] - The JID of the service to ping * If the ping is sent out to the user's bare JID and no response is received it will attempt to reconnect. - * @param { number } [timeout] - The amount of time in + * @param {number} [timeout] - The amount of time in * milliseconds to wait for a response. The default is 10000; - * @returns { Boolean | null } + * @returns {Promise} * Whether the pinged entity responded with a non-error IQ stanza. * If we already know we're not connected, no ping is sent out and `null` is returned. */ @@ -27,7 +27,8 @@ export default { // However, some servers don't advertise while still responding to pings // const feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING}); setLastStanzaDate(new Date()); - jid = jid || Strophe.getDomainFromJid(_converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + jid = jid || Strophe.getDomainFromJid(bare_jid); const iq = $iq({ 'type': 'get', 'to': jid, @@ -37,7 +38,7 @@ export default { const result = await api.sendIQ(iq, timeout || 10000, false); if (result === null) { log.warn(`Timeout while pinging ${jid}`); - if (jid === Strophe.getDomainFromJid(_converse.bare_jid)) { + if (jid === Strophe.getDomainFromJid(bare_jid)) { api.connection.reconnect(); } return false; diff --git a/src/headless/plugins/ping/index.js b/src/headless/plugins/ping/index.js index b625d6d0d1..26e5c7500d 100644 --- a/src/headless/plugins/ping/index.js +++ b/src/headless/plugins/ping/index.js @@ -27,6 +27,7 @@ converse.plugins.add('converse-ping', { api.listen.on('connected', registerHandlers); api.listen.on('reconnected', registerHandlers); api.listen.on('disconnected', unregisterIntervalHandler); - api.listen.on('windowStateChanged', onWindowStateChanged); + + document.addEventListener('visibilitychange', onWindowStateChanged); } }); diff --git a/src/headless/plugins/ping/tests/ping.js b/src/headless/plugins/ping/tests/ping.js index 4a6162a751..5bcd7b1ed4 100644 --- a/src/headless/plugins/ping/tests/ping.js +++ b/src/headless/plugins/ping/tests/ping.js @@ -30,7 +30,8 @@ describe("XMPP Ping", function () { ``); })); - it("is not sent out if we're not connected", mock.initConverse(async (_converse) => { + it("is not sent out if we're not connected", mock.initConverse( + [], {auto_login: false}, async (_converse) => { spyOn(_converse.api.connection.get(), 'send'); expect(await _converse.api.ping()).toBe(null); expect(_converse.api.connection.get().send.calls.count()).toBe(0); diff --git a/src/headless/plugins/ping/utils.js b/src/headless/plugins/ping/utils.js index d151033697..8f76839ecf 100644 --- a/src/headless/plugins/ping/utils.js +++ b/src/headless/plugins/ping/utils.js @@ -5,8 +5,8 @@ const { Strophe, $iq } = converse.env; let lastStanzaDate; -export function onWindowStateChanged (data) { - data.state === 'visible' && api.ping(null, 5000); +export function onWindowStateChanged () { + if (!document.hidden) api.ping(null, 5000); } export function setLastStanzaDate (date) { @@ -63,7 +63,7 @@ export function onEverySecond () { if (ping_interval > 0) { const now = new Date(); lastStanzaDate = lastStanzaDate ?? now; - if ((now - lastStanzaDate)/1000 > ping_interval) { + if ((now.valueOf() - lastStanzaDate.valueOf())/1000 > ping_interval) { api.ping(); } } diff --git a/src/headless/plugins/pubsub.js b/src/headless/plugins/pubsub.js index ed9e50e4c0..f23949398f 100644 --- a/src/headless/plugins/pubsub.js +++ b/src/headless/plugins/pubsub.js @@ -2,6 +2,7 @@ * @module converse-pubsub * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder */ import "./disco/index.js"; import _converse from '../shared/_converse.js'; @@ -44,8 +45,9 @@ converse.plugins.add('converse-pubsub', { * the publish options precondication cannot be met. */ async 'publish' (jid, node, item, options, strict_options=true) { + const bare_jid = _converse.session.get('bare_jid'); const stanza = $iq({ - 'from': _converse.bare_jid, + 'from': bare_jid, 'type': 'set', 'to': jid }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) @@ -53,7 +55,7 @@ converse.plugins.add('converse-pubsub', { .cnode(item.tree()).up().up(); if (options) { - jid = jid || _converse.bare_jid; + jid = jid || bare_jid; if (await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', jid)) { stanza.c('publish-options') .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'}) @@ -86,6 +88,5 @@ converse.plugins.add('converse-pubsub', { } } }); - /************************ END API ************************/ } }); diff --git a/src/headless/plugins/roster/api.js b/src/headless/plugins/roster/api.js index e3b58a08d1..623a2b1f5f 100644 --- a/src/headless/plugins/roster/api.js +++ b/src/headless/plugins/roster/api.js @@ -43,9 +43,10 @@ export default { */ async get (jids) { await api.waitUntil('rosterContactsFetched'); - const _getter = jid => _converse.roster.get(Strophe.getBareJidFromJid(jid)); + const { roster } = _converse.state; + const _getter = jid => roster.get(Strophe.getBareJidFromJid(jid)); if (jids === undefined) { - jids = _converse.roster.pluck('jid'); + jids = roster.pluck('jid'); } else if (typeof jids === 'string') { return _getter(jids); } @@ -68,7 +69,7 @@ export default { if (typeof jid !== 'string' || !jid.includes('@')) { throw new TypeError('contacts.add: invalid jid'); } - return _converse.roster.addAndSubscribe(jid, name); + return _converse.state.roster.addAndSubscribe(jid, name); } } } diff --git a/src/headless/plugins/roster/contact.js b/src/headless/plugins/roster/contact.js index 438b36e127..81838ae8b1 100644 --- a/src/headless/plugins/roster/contact.js +++ b/src/headless/plugins/roster/contact.js @@ -1,7 +1,7 @@ import '../../plugins/status/api.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; import { rejectPresenceSubscription } from './utils.js'; @@ -24,7 +24,7 @@ class RosterContact extends Model { } async initialize (attributes) { - super.initialize(attributes); + super.initialize(); this.initialized = getOpenPromise(); this.setPresence(); const { jid } = attributes; @@ -39,7 +39,7 @@ class RosterContact extends Model { * When a contact's presence status has changed. * The presence status is either `online`, `offline`, `dnd`, `away` or `xa`. * @event _converse#contactPresenceChanged - * @type { _converse.RosterContact } + * @type {RosterContact} * @example _converse.api.listen.on('contactPresenceChanged', contact => { ... }); */ this.listenTo(this.presence, 'change:show', () => api.trigger('contactPresenceChanged', this)); @@ -47,7 +47,7 @@ class RosterContact extends Model { /** * Synchronous event which provides a hook for further initializing a RosterContact * @event _converse#rosterContactInitialized - * @param { _converse.RosterContact } contact + * @param {RosterContact} contact */ await api.trigger('rosterContactInitialized', this, {'Synchronous': true}); this.initialized.resolve(); @@ -55,12 +55,12 @@ class RosterContact extends Model { setPresence () { const jid = this.get('jid'); - this.presence = _converse.presences.findWhere(jid) || _converse.presences.create({ jid }); + const { presences } = _converse.state; + this.presence = presences.findWhere(jid) || presences.create({ jid }); } openChat () { - const attrs = this.attributes; - api.chats.open(attrs.jid, attrs, true); + api.chats.open(this.get('jid'), this.attributes, true); } /** diff --git a/src/headless/plugins/roster/contacts.js b/src/headless/plugins/roster/contacts.js index 7b5b3128b4..7a7b7feb1b 100644 --- a/src/headless/plugins/roster/contacts.js +++ b/src/headless/plugins/roster/contacts.js @@ -2,8 +2,7 @@ import RosterContact from './contact.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from "../../log.js"; -import { Collection } from "@converse/skeletor/src/collection"; -import { Model } from "@converse/skeletor/src/model"; +import { Collection, Model } from "@converse/skeletor"; import { initStorage } from '../../utils/storage.js'; import { rejectPresenceSubscription } from './utils.js'; @@ -13,10 +12,12 @@ class RosterContacts extends Collection { constructor () { super(); this.model = RosterContact; + this.data = null; } initialize () { - const id = `roster.state-${_converse.bare_jid}-${this.get('jid')}`; + const bare_jid = _converse.session.get('bare_jid'); + const id = `roster.state-${bare_jid}-${this.get('jid')}`; this.state = new Model({ id, 'collapsed_groups': [] }); initStorage(this.state, id); this.state.fetch(); @@ -39,7 +40,7 @@ class RosterContacts extends Collection { // Register a handler for roster IQ "set" stanzas, which update // roster contacts. api.connection.get().addHandler(iq => { - _converse.roster.onRosterPush(iq); + _converse.state.roster.onRosterPush(iq); return true; }, Strophe.NS.ROSTER, 'iq', "set"); } @@ -54,9 +55,10 @@ class RosterContacts extends Collection { const connection = api.connection.get(); connection.addHandler( function (msg) { + const { roster } = _converse.state; window.setTimeout(function () { - _converse.connection.flush(); - _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg); + api.connection.get().flush(); + roster.subscribeToSuggestedItems(msg); }, t); t += msg.querySelectorAll('item').length * 250; return true; @@ -98,18 +100,19 @@ class RosterContacts extends Collection { */ api.trigger('cachedRoster', result); } else { - _converse.send_initial_presence = true; - return _converse.roster.fetchFromServer(); + api.connection.get().send_initial_presence = true; + return _converse.state.roster.fetchFromServer(); } } // eslint-disable-next-line class-methods-use-this subscribeToSuggestedItems (msg) { + const { xmppstatus } = _converse.state; Array.from(msg.querySelectorAll('item')).forEach((item) => { if (item.getAttribute('action') === 'add') { - _converse.roster.addAndSubscribe( + _converse.state.roster.addAndSubscribe( item.getAttribute('jid'), - _converse.xmppstatus.getNickname() || _converse.xmppstatus.getFullname() + xmppstatus.getNickname() || xmppstatus.getFullname() ); } }); @@ -133,7 +136,7 @@ class RosterContacts extends Collection { */ async addAndSubscribe (jid, name, groups, message, attributes) { const contact = await this.addContactToRoster(jid, name, groups, attributes); - if (contact instanceof _converse.RosterContact) { + if (contact instanceof _converse.exports.RosterContact) { contact.subscribe(message); } } @@ -192,13 +195,14 @@ class RosterContacts extends Collection { async subscribeBack (bare_jid, presence) { const contact = this.get(bare_jid); - if (contact instanceof _converse.RosterContact) { + const { RosterContact } = _converse.exports; + if (contact instanceof RosterContact) { contact.authorize().subscribe(); } else { // Can happen when a subscription is retried or roster was deleted const nickname = sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop()?.textContent || null; const contact = await this.addContactToRoster(bare_jid, nickname, [], { 'subscription': 'from' }); - if (contact instanceof _converse.RosterContact) { + if (contact instanceof RosterContact) { contact.authorize().subscribe(); } } @@ -213,7 +217,8 @@ class RosterContacts extends Collection { onRosterPush (iq) { const id = iq.getAttribute('id'); const from = iq.getAttribute('from'); - if (from && from !== _converse.bare_jid) { + const bare_jid = _converse.session.get('bare_jid'); + if (from && from !== bare_jid) { // https://tools.ietf.org/html/rfc6121#page-15 // // A receiving client MUST ignore the stanza unless it has no 'from' @@ -342,7 +347,7 @@ class RosterContacts extends Collection { /** * Triggered when someone has requested to subscribe to your presence (i.e. to be your contact). * @event _converse#contactRequest - * @type { _converse.RosterContact } + * @type {RosterContact} * @example _converse.api.listen.on('contactRequest', contact => { ... }); */ api.trigger('contactRequest', this.create(user_data)); @@ -378,9 +383,10 @@ class RosterContacts extends Collection { // eslint-disable-next-line class-methods-use-this handleOwnPresence (presence) { - const jid = presence.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - presence_type = presence.getAttribute('type'); + const jid = presence.getAttribute('from'); + const resource = Strophe.getResourceFromJid(jid); + const presence_type = presence.getAttribute('type'); + const { xmppstatus } = _converse.state; if ((api.connection.get().jid !== jid) && (presence_type !== 'unavailable') && @@ -390,12 +396,12 @@ class RosterContacts extends Collection { // synchronize_availability option set to update, // we'll update ours as well. const show = presence.querySelector('show')?.textContent || 'online'; - _converse.xmppstatus.save({ 'status': show }, { 'silent': true }); + xmppstatus.save({ 'status': show }, { 'silent': true }); const status_message = presence.querySelector('status')?.textContent; - if (status_message) _converse.xmppstatus.save({ status_message }); + if (status_message) xmppstatus.save({ status_message }); } - if (_converse.jid === jid && presence_type === 'unavailable') { + if (_converse.session.get('jid') === jid && presence_type === 'unavailable') { // XXX: We've received an "unavailable" presence from our // own resource. Apparently this happens due to a // Prosody bug, whereby we send an IQ stanza to remove diff --git a/src/headless/plugins/roster/filter.js b/src/headless/plugins/roster/filter.js index 18a85df1bc..08c93ac011 100644 --- a/src/headless/plugins/roster/filter.js +++ b/src/headless/plugins/roster/filter.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; class RosterFilter extends Model { initialize () { diff --git a/src/headless/plugins/roster/index.js b/src/headless/plugins/roster/index.js index 4dfc226124..2a29c45d67 100644 --- a/src/headless/plugins/roster/index.js +++ b/src/headless/plugins/roster/index.js @@ -36,16 +36,19 @@ converse.plugins.add('converse-roster', { Object.assign(_converse.api, roster_api); const { __ } = _converse; - _converse.HEADER_CURRENT_CONTACTS = __('My contacts'); - _converse.HEADER_PENDING_CONTACTS = __('Pending contacts'); - _converse.HEADER_REQUESTING_CONTACTS = __('Contact requests'); - _converse.HEADER_UNGROUPED = __('Ungrouped'); - _converse.HEADER_UNREAD = __('New messages'); - - _converse.Presence = Presence; - _converse.Presences = Presences; - _converse.RosterContact = RosterContact; - _converse.RosterContacts = RosterContacts; + const labels = { + HEADER_CURRENT_CONTACTS: __('My contacts'), + HEADER_PENDING_CONTACTS: __('Pending contacts'), + HEADER_REQUESTING_CONTACTS: __('Contact requests'), + HEADER_UNGROUPED: __('Ungrouped'), + HEADER_UNREAD: __('New messages'), + }; + Object.assign(_converse, labels); // XXX DEPRECATED + Object.assign(_converse.labels, labels); + + const exports = { Presence, Presences, RosterContact, RosterContacts }; + Object.assign(_converse, exports); // XXX DEPRECATED + Object.assign(_converse.exports, exports); api.listen.on('beforeTearDown', () => unregisterPresenceHandler()); api.listen.on('chatBoxesInitialized', onChatBoxesInitialized); diff --git a/src/headless/plugins/roster/presence.js b/src/headless/plugins/roster/presence.js index 1f5d58afe1..24929dfe63 100644 --- a/src/headless/plugins/roster/presence.js +++ b/src/headless/plugins/roster/presence.js @@ -1,5 +1,5 @@ import Resources from "./resources.js"; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { converse } from '../../shared/api/index.js'; import { initStorage } from '../../utils/storage.js'; diff --git a/src/headless/plugins/roster/presences.js b/src/headless/plugins/roster/presences.js index 4e464d0b6b..d714e72c4d 100644 --- a/src/headless/plugins/roster/presences.js +++ b/src/headless/plugins/roster/presences.js @@ -1,4 +1,4 @@ -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; import Presence from "./presence.js"; class Presences extends Collection { diff --git a/src/headless/plugins/roster/resource.js b/src/headless/plugins/roster/resource.js index 4924389092..bc578e1781 100644 --- a/src/headless/plugins/roster/resource.js +++ b/src/headless/plugins/roster/resource.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; class Resource extends Model { get idAttribute () { // eslint-disable-line class-methods-use-this diff --git a/src/headless/plugins/roster/resources.js b/src/headless/plugins/roster/resources.js index 070e6fa751..f1e7c2eb94 100644 --- a/src/headless/plugins/roster/resources.js +++ b/src/headless/plugins/roster/resources.js @@ -1,4 +1,4 @@ -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; import Resource from "./resource"; class Resources extends Collection { diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js index d2e60a3577..137c7f2bc6 100644 --- a/src/headless/plugins/roster/utils.js +++ b/src/headless/plugins/roster/utils.js @@ -2,9 +2,9 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from "../../log.js"; import { Strophe } from 'strophe.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { RosterFilter } from '../../plugins/roster/filter.js'; -import { STATUS_WEIGHTS } from "../../shared/constants"; +import { STATUS_WEIGHTS, PRIVATE_CHAT_TYPE } from "../../shared/constants"; import { initStorage } from '../../utils/storage.js'; import { shouldClearCache } from '../../utils/session.js'; @@ -13,16 +13,23 @@ const { $pres } = converse.env; function initRoster () { // Initialize the collections that represent the roster contacts and groups - const roster = _converse.roster = new _converse.RosterContacts(); - let id = `converse.contacts-${_converse.bare_jid}`; + const roster = new _converse.exports.RosterContacts(); + Object.assign(_converse, { roster }); // XXX Deprecated + Object.assign(_converse.state, { roster }); + + const bare_jid = _converse.session.get('bare_jid'); + let id = `converse.contacts-${bare_jid}`; initStorage(roster, id); - const filter = _converse.roster_filter = new RosterFilter(); - filter.id = `_converse.rosterfilter-${_converse.bare_jid}`; - initStorage(filter, filter.id); - filter.fetch(); + const roster_filter = new RosterFilter(); + Object.assign(_converse, { roster_filter }); // XXX Deprecated + Object.assign(_converse.state, { roster_filter }); + + roster_filter.id = `_converse.rosterfilter-${bare_jid}`; + initStorage(roster_filter, roster_filter.id); + roster_filter.fetch(); - id = `converse-roster-model-${_converse.bare_jid}`; + id = `converse-roster-model-${bare_jid}`; roster.data = new Model(); roster.data.id = id; initStorage(roster.data, id); @@ -42,50 +49,52 @@ function initRoster () { /** * Fetch all the roster groups, and then the roster contacts. * Emit an event after fetching is done in each case. - * @private - * @param { Bool } ignore_cache - If set to to true, the local cache + * @param {boolean} ignore_cache - If set to to true, the local cache * will be ignored it's guaranteed that the XMPP server * will be queried for the roster. */ async function populateRoster (ignore_cache=false) { + const connection = api.connection.get(); if (ignore_cache) { - _converse.send_initial_presence = true; + connection.send_initial_presence = true; } try { - await _converse.roster.fetchRosterContacts(); + await _converse.state.roster.fetchRosterContacts(); api.trigger('rosterContactsFetched'); } catch (reason) { log.error(reason); } finally { - _converse.send_initial_presence && api.user.presence.send(); + connection.send_initial_presence && api.user.presence.send(); } } function updateUnreadCounter (chatbox) { - const contact = _converse.roster?.get(chatbox.get('jid')); + const contact = _converse.state.roster?.get(chatbox.get('jid')); contact?.save({'num_unread': chatbox.get('num_unread')}); } +let presence_ref; + function registerPresenceHandler () { unregisterPresenceHandler(); const connection = api.connection.get(); - _converse.presence_ref = connection.addHandler(presence => { - _converse.roster.presenceHandler(presence); + presence_ref = connection.addHandler(presence => { + _converse.state.roster.presenceHandler(presence); return true; }, null, 'presence', null); } export function unregisterPresenceHandler () { - if (_converse.presence_ref !== undefined) { + if (presence_ref) { const connection = api.connection.get(); - connection.deleteHandler(_converse.presence_ref); - delete _converse.presence_ref; + connection.deleteHandler(presence_ref); + presence_ref = null; } } async function clearPresences () { - await _converse.presences?.clearStore(); + await _converse.state.presences?.clearStore(); } @@ -95,14 +104,12 @@ async function clearPresences () { export async function onClearSession () { await clearPresences(); if (shouldClearCache()) { - if (_converse.rostergroups) { - await _converse.rostergroups.clearStore(); - delete _converse.rostergroups; - } - if (_converse.roster) { - _converse.roster.data?.destroy(); - await _converse.roster.clearStore(); - delete _converse.roster; + const { roster } = _converse.state; + if (roster) { + roster.data?.destroy(); + await roster.clearStore(); + delete _converse.state.roster; + Object.assign(_converse, { roster: undefined }); // XXX DEPRECATED } } } @@ -125,7 +132,7 @@ export function onPresencesInitialized (reconnecting) { } else { initRoster(); } - _converse.roster.onConnected(); + _converse.state.roster.onConnected(); registerPresenceHandler(); populateRoster(!api.connection.get().restored); } @@ -142,12 +149,17 @@ export async function onStatusInitialized (reconnecting) { // and we'll receive new presence updates !api.connection.get().hasResumed() && (await clearPresences()); } else { - _converse.presences = new _converse.Presences(); - const id = `converse.presences-${_converse.bare_jid}`; - initStorage(_converse.presences, id, 'session'); + const presences = new _converse.exports.Presences(); + Object.assign(_converse, { presences }); + Object.assign(_converse.state, { presences }); + + const bare_jid = _converse.session.get('bare_jid'); + const id = `converse.presences-${bare_jid}`; + + initStorage(presences, id, 'session'); // We might be continuing an existing session, so we fetch // cached presence data. - _converse.presences.fetch(); + presences.fetch(); } /** * Triggered once the _converse.Presences collection has been @@ -155,7 +167,7 @@ export async function onStatusInitialized (reconnecting) { * Returns a boolean indicating whether this event has fired due to * Converse having reconnected. * @event _converse#presencesInitialized - * @type { bool } + * @type {boolean} * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... }); */ api.trigger('presencesInitialized', reconnecting); @@ -166,10 +178,11 @@ export async function onStatusInitialized (reconnecting) { * Roster specific event handler for the chatBoxesInitialized event */ export function onChatBoxesInitialized () { - _converse.chatboxes.on('change:num_unread', updateUnreadCounter); + const { chatboxes } = _converse.state; + chatboxes.on('change:num_unread', updateUnreadCounter); - _converse.chatboxes.on('add', chatbox => { - if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) { + chatboxes.on('add', chatbox => { + if (chatbox.get('type') === PRIVATE_CHAT_TYPE) { chatbox.setRosterContact(chatbox.get('jid')); } }); @@ -180,10 +193,10 @@ export function onChatBoxesInitialized () { * Roster specific handler for the rosterContactsFetched promise */ export function onRosterContactsFetched () { - _converse.roster.on('add', contact => { + _converse.state.roster.on('add', contact => { // When a new contact is added, check if we already have a // chatbox open for it, and if so attach it to the chatbox. - const chatbox = _converse.chatboxes.findWhere({ 'jid': contact.get('jid') }); + const chatbox = _converse.state.chatboxes.findWhere({ 'jid': contact.get('jid') }); chatbox?.setRosterContact(contact.get('jid')); }); } @@ -214,11 +227,19 @@ export function contactsComparator (contact1, contact2) { export function groupsComparator (a, b) { const HEADER_WEIGHTS = {}; - HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0; - HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1; - HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS] = 2; - HEADER_WEIGHTS[_converse.HEADER_UNGROUPED] = 3; - HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS] = 4; + const { + HEADER_UNREAD, + HEADER_REQUESTING_CONTACTS, + HEADER_CURRENT_CONTACTS, + HEADER_UNGROUPED, + HEADER_PENDING_CONTACTS, + } = _converse.labels; + + HEADER_WEIGHTS[HEADER_UNREAD] = 0; + HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 1; + HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 2; + HEADER_WEIGHTS[HEADER_UNGROUPED] = 3; + HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 4; const WEIGHTS = HEADER_WEIGHTS; const special_groups = Object.keys(HEADER_WEIGHTS); @@ -229,22 +250,22 @@ export function groupsComparator (a, b) { } else if (a_is_special && b_is_special) { return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0); } else if (!a_is_special && b_is_special) { - const a_header = _converse.HEADER_CURRENT_CONTACTS; + const a_header = HEADER_CURRENT_CONTACTS; return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0); } else if (a_is_special && !b_is_special) { - const b_header = _converse.HEADER_CURRENT_CONTACTS; + const b_header = HEADER_CURRENT_CONTACTS; return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0); } } export function getGroupsAutoCompleteList () { - const { roster } = _converse; + const { roster } = _converse.state; const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []); return [...new Set(groups.filter(i => i))]; } export function getJIDsAutoCompleteList () { - return [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]; + return [...new Set(_converse.state.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]; } @@ -253,7 +274,7 @@ export function getJIDsAutoCompleteList () { */ export async function getNamesAutoCompleteList (query) { const options = { - 'mode': 'cors', + 'mode': /** @type {RequestMode} */('cors'), 'headers': { 'Accept': 'text/json' } diff --git a/src/headless/plugins/smacks/tests/smacks.js b/src/headless/plugins/smacks/tests/smacks.js index cd4ac87dbc..0011de163d 100644 --- a/src/headless/plugins/smacks/tests/smacks.js +++ b/src/headless/plugins/smacks/tests/smacks.js @@ -213,6 +213,8 @@ describe("XEP-0198 Stream Management", function () { }, async function (_converse) { + const { api } = _converse; + const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit"; sessionStorage.setItem( key, @@ -251,15 +253,26 @@ describe("XEP-0198 Stream Management", function () { }) ); - _converse.no_connection_on_bind = true; // XXX Don't trigger CONNECTED in MockConnection - await _converse.api.user.login('romeo@montague.lit', 'secret'); + const proto = Object.getPrototypeOf(api.connection.get()) + const _changeConnectStatus = proto._changeConnectStatus; + let count = 0; + spyOn(proto, '_changeConnectStatus').and.callFake((status) => { + if (status === Strophe.Status.CONNECTED && count === 0) { + // Don't trigger CONNECTED + count++; + return; + } + _changeConnectStatus.call(api.connection.get(), status); + }); - const sent_stanzas = _converse.api.connection.get().sent_stanzas; + await api.user.login('romeo@montague.lit', 'secret'); + + const sent_stanzas = api.connection.get().sent_stanzas; const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); expect(Strophe.serialize(stanza)).toEqual(''); const result = u.toStanza(``); - _converse.api.connection.get()._dataRecv(mock.createRequest(result)); + api.connection.get()._dataRecv(mock.createRequest(result)); expect(_converse.session.get('smacks_enabled')).toBe(true); const nick = 'romeo'; @@ -278,9 +291,9 @@ describe("XEP-0198 Stream Management", function () { type: 'groupchat' }).c('body').t('First message').tree(); - _converse.api.connection.get()._dataRecv(mock.createRequest(msg)); + api.connection.get()._dataRecv(mock.createRequest(msg)); - await _converse.api.waitUntil('chatBoxesFetched'); + await api.waitUntil('chatBoxesFetched'); const muc = _converse.chatboxes.get(muc_jid); await mock.getRoomFeatures(_converse, muc_jid); await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); @@ -288,6 +301,5 @@ describe("XEP-0198 Stream Management", function () { await muc.messages.fetched; await u.waitUntil(() => muc.messages.length); expect(muc.messages.at(0).get('message')).toBe('First message') - delete _converse.no_connection_on_bind; })); }); diff --git a/src/headless/plugins/status/api.js b/src/headless/plugins/status/api.js index 4e8355d2f0..3b6b02bacb 100644 --- a/src/headless/plugins/status/api.js +++ b/src/headless/plugins/status/api.js @@ -18,7 +18,7 @@ export default { */ async get () { await api.waitUntil('statusInitialized'); - return _converse.xmppstatus.get('status'); + return _converse.state.xmppstatus.get('status'); }, /** @@ -43,7 +43,7 @@ export default { data.status_message = message; } await api.waitUntil('statusInitialized'); - _converse.xmppstatus.save(data); + _converse.state.xmppstatus.save(data); }, /** @@ -61,7 +61,7 @@ export default { */ async get () { await api.waitUntil('statusInitialized'); - return _converse.xmppstatus.get('status_message'); + return _converse.state.xmppstatus.get('status_message'); }, /** * @async @@ -71,7 +71,7 @@ export default { */ async set (status) { await api.waitUntil('statusInitialized'); - _converse.xmppstatus.save({ status_message: status }); + _converse.state.xmppstatus.save({ status_message: status }); } } } diff --git a/src/headless/plugins/status/index.js b/src/headless/plugins/status/index.js index b67e297eb2..6b9b0e8acc 100644 --- a/src/headless/plugins/status/index.js +++ b/src/headless/plugins/status/index.js @@ -13,6 +13,7 @@ import { onEverySecond, onUserActivity, registerIntervalHandler, + tearDown, sendCSI } from './utils.js'; @@ -35,28 +36,23 @@ converse.plugins.add('converse-status', { }); api.promises.add(['statusInitialized']); - _converse.XMPPStatus = XMPPStatus; - _converse.onUserActivity = onUserActivity; - _converse.onEverySecond = onEverySecond; - _converse.sendCSI = sendCSI; - _converse.registerIntervalHandler = registerIntervalHandler; - + const exports = { XMPPStatus, onUserActivity, onEverySecond, sendCSI, registerIntervalHandler }; + Object.assign(_converse, exports); // Deprecated + Object.assign(_converse.exports, exports); Object.assign(_converse.api.user, status_api); if (api.settings.get("idle_presence_timeout") > 0) { api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE)); } - api.listen.on('presencesInitialized', (reconnecting) => { - if (!reconnecting) { - _converse.registerIntervalHandler(); - } - }); + api.listen.on('presencesInitialized', (reconnecting) => (!reconnecting && registerIntervalHandler())); + api.listen.on('beforeTearDown', tearDown); api.listen.on('clearSession', () => { - if (shouldClearCache() && _converse.xmppstatus) { - _converse.xmppstatus.destroy(); - delete _converse.xmppstatus; + if (shouldClearCache() && _converse.state.xmppstatus) { + _converse.state.xmppstatus.destroy(); + delete _converse.state.xmppstatus; + Object.assign(_converse, { xmppstatus: undefined }); // XXX DEPRECATED api.promises.add(['statusInitialized']); } }); diff --git a/src/headless/plugins/status/status.js b/src/headless/plugins/status/status.js index 1e95e26d8f..c63fbd9861 100644 --- a/src/headless/plugins/status/status.js +++ b/src/headless/plugins/status/status.js @@ -1,12 +1,13 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; +import { isIdle, getIdleSeconds } from './utils.js'; const { Strophe, $pres } = converse.env; export default class XMPPStatus extends Model { - defaults () { // eslint-disable-line class-methods-use-this + defaults () { return { "status": api.settings.get("default_state") } } @@ -22,14 +23,14 @@ export default class XMPPStatus extends Model { } getDisplayName () { - return this.getFullname() || this.getNickname() || _converse.bare_jid; + return this.getFullname() || this.getNickname() || _converse.session.get('bare_jid'); } - getNickname () { // eslint-disable-line class-methods-use-this + getNickname () { return api.settings.get('nickname'); } - getFullname () { // eslint-disable-line class-methods-use-this + getFullname () { return ''; // Gets overridden in converse-vcard } @@ -46,7 +47,7 @@ export default class XMPPStatus extends Model { if (type === 'subscribe') { presence = $pres({ to, type }); - const { xmppstatus } = _converse; + const { xmppstatus } = _converse.state; const nick = xmppstatus.getNickname(); if (nick) presence.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up(); @@ -73,10 +74,9 @@ export default class XMPPStatus extends Model { const priority = api.settings.get("priority"); presence.c('priority').t(Number.isNaN(Number(priority)) ? 0 : priority).up(); - const { idle, idle_seconds } = _converse; - if (idle) { + if (isIdle()) { const idle_since = new Date(); - idle_since.setSeconds(idle_since.getSeconds() - idle_seconds); + idle_since.setSeconds(idle_since.getSeconds() - getIdleSeconds()); presence.c('idle', { xmlns: Strophe.NS.IDLE, since: idle_since.toISOString() }); } diff --git a/src/headless/plugins/status/utils.js b/src/headless/plugins/status/utils.js index bc5e20b641..3210ae3d64 100644 --- a/src/headless/plugins/status/utils.js +++ b/src/headless/plugins/status/utils.js @@ -1,6 +1,8 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import { initStorage } from '../../utils/storage.js'; +import { getUnloadEvent } from '../../utils/session.js'; +import { ACTIVE, INACTIVE } from '../../shared/constants.js'; const { Strophe, $build } = converse.env; @@ -17,14 +19,15 @@ function onStatusInitialized (reconnecting) { export function initStatus (reconnecting) { // If there's no xmppstatus obj, then we were never connected to // begin with, so we set reconnecting to false. - reconnecting = _converse.xmppstatus === undefined ? false : reconnecting; + reconnecting = _converse.state.xmppstatus === undefined ? false : reconnecting; if (reconnecting) { onStatusInitialized(reconnecting); } else { - const id = `converse.xmppstatus-${_converse.bare_jid}`; - _converse.xmppstatus = new _converse.XMPPStatus({ id }); - initStorage(_converse.xmppstatus, id, 'session'); - _converse.xmppstatus.fetch({ + const id = `converse.xmppstatus-${_converse.session.get('bare_jid')}`; + _converse.state.xmppstatus = new _converse.exports.XMPPStatus({ id }); + Object.assign(_converse, { xmppstatus: _converse.state.xmppstatus }); + initStorage(_converse.state.xmppstatus, id, 'session'); + _converse.state.xmppstatus.fetch({ 'success': () => onStatusInitialized(reconnecting), 'error': () => onStatusInitialized(reconnecting), 'silent': true @@ -32,28 +35,43 @@ export function initStatus (reconnecting) { } } +let idle_seconds = 0; +let idle = false; +let auto_changed_status = false; +let inactive = false; + +export function isIdle () { + return idle; +} + +export function getIdleSeconds () { + return idle_seconds; +} + +/** + * Resets counters and flags relating to CSI and auto_away/auto_xa + */ export function onUserActivity () { - /* Resets counters and flags relating to CSI and auto_away/auto_xa */ - if (_converse.idle_seconds > 0) { - _converse.idle_seconds = 0; + if (idle_seconds > 0) { + idle_seconds = 0; } if (!api.connection.get()?.authenticated) { // We can't send out any stanzas when there's no authenticated connection. // This can happen when the connection reconnects. return; } - if (_converse.inactive) { - _converse.sendCSI(_converse.ACTIVE); - } - if (_converse.idle) { - _converse.idle = false; + if (inactive) sendCSI(ACTIVE); + + if (idle) { + idle = false; api.user.presence.send(); } - if (_converse.auto_changed_status === true) { - _converse.auto_changed_status = false; + + if (auto_changed_status === true) { + auto_changed_status = false; // XXX: we should really remember the original state here, and // then set it back to that... - _converse.xmppstatus.set('status', api.settings.get("default_state")); + _converse.state.xmppstatus.set('status', api.settings.get("default_state")); } } @@ -66,29 +84,30 @@ export function onEverySecond () { // This can happen when the connection reconnects. return; } - const stat = _converse.xmppstatus.get('status'); - _converse.idle_seconds++; + const { xmppstatus } = _converse.state; + const stat = xmppstatus.get('status'); + idle_seconds++; if (api.settings.get("csi_waiting_time") > 0 && - _converse.idle_seconds > api.settings.get("csi_waiting_time") && - !_converse.inactive) { - _converse.sendCSI(_converse.INACTIVE); + idle_seconds > api.settings.get("csi_waiting_time") && + !inactive) { + sendCSI(INACTIVE); } if (api.settings.get("idle_presence_timeout") > 0 && - _converse.idle_seconds > api.settings.get("idle_presence_timeout") && - !_converse.idle) { - _converse.idle = true; + idle_seconds > api.settings.get("idle_presence_timeout") && + !idle) { + idle = true; api.user.presence.send(); } if (api.settings.get("auto_away") > 0 && - _converse.idle_seconds > api.settings.get("auto_away") && + idle_seconds > api.settings.get("auto_away") && stat !== 'away' && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - _converse.xmppstatus.set('status', 'away'); + auto_changed_status = true; + xmppstatus.set('status', 'away'); } else if (api.settings.get("auto_xa") > 0 && - _converse.idle_seconds > api.settings.get("auto_xa") && + idle_seconds > api.settings.get("auto_xa") && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - _converse.xmppstatus.set('status', 'xa'); + auto_changed_status = true; + xmppstatus.set('status', 'xa'); } } @@ -99,9 +118,11 @@ export function onEverySecond () { */ export function sendCSI (stat) { api.send($build(stat, {xmlns: Strophe.NS.CSI})); - _converse.inactive = (stat === _converse.INACTIVE) ? true : false; + inactive = (stat === INACTIVE) ? true : false; } +let everySecondTrigger; + /** * Set an interval of one second and register a handler for it. * Required for the auto_away, auto_xa and csi_waiting_time features. @@ -116,20 +137,33 @@ export function registerIntervalHandler () { // Waiting time of less then one second means features aren't used. return; } - _converse.idle_seconds = 0; - _converse.auto_changed_status = false; // Was the user's status changed by Converse? - - const { unloadevent } = _converse; - window.addEventListener('click', _converse.onUserActivity); - window.addEventListener('focus', _converse.onUserActivity); - window.addEventListener('keypress', _converse.onUserActivity); - window.addEventListener('mousemove', _converse.onUserActivity); - window.addEventListener(unloadevent, _converse.onUserActivity, {'once': true, 'passive': true}); - _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000); + idle_seconds = 0; + auto_changed_status = false; // Was the user's status changed by Converse? + + const { onUserActivity, onEverySecond } = _converse.exports; + window.addEventListener('click', onUserActivity); + window.addEventListener('focus', onUserActivity); + window.addEventListener('keypress', onUserActivity); + window.addEventListener('mousemove', onUserActivity); + window.addEventListener(getUnloadEvent(), onUserActivity, {'once': true, 'passive': true}); + everySecondTrigger = window.setInterval(onEverySecond, 1000); +} + +export function tearDown () { + const { onUserActivity } = _converse.exports; + window.removeEventListener('click', onUserActivity); + window.removeEventListener('focus', onUserActivity); + window.removeEventListener('keypress', onUserActivity); + window.removeEventListener('mousemove', onUserActivity); + window.removeEventListener(getUnloadEvent(), onUserActivity); + if (everySecondTrigger) { + window.clearInterval(everySecondTrigger); + everySecondTrigger = null; + } } export function addStatusToMUCJoinPresence (_, stanza) { - const { xmppstatus } = _converse; + const { xmppstatus } = _converse.state; const status = xmppstatus.get('status'); if (['away', 'chat', 'dnd', 'xa'].includes(status)) { diff --git a/src/headless/plugins/vcard/api.js b/src/headless/plugins/vcard/api.js index 8f43eaaa16..b2541dbc88 100644 --- a/src/headless/plugins/vcard/api.js +++ b/src/headless/plugins/vcard/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import log from "../../log.js"; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; @@ -23,8 +26,8 @@ export default { * for the passed in JID. * * @method _converse.api.vcard.set - * @param { string } jid The JID for which the VCard should be set - * @param { object } data A map of VCard keys and values + * @param {string} jid The JID for which the VCard should be set + * @param {object} data A map of VCard keys and values * @example * let jid = _converse.bare_jid; * _converse.api.vcard.set( jid, { @@ -68,7 +71,7 @@ export default { * @param {Model|string} model Either a `Model` instance, or a string JID. * If a `Model` instance is passed in, then it must have either a `jid` * attribute or a `muc_jid` attribute. - * @param { boolean } [force] A boolean indicating whether the vcard should be + * @param {boolean} [force] A boolean indicating whether the vcard should be * fetched from the server even if it's been fetched before. * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for * a `Model` instance which represents an entity with a JID (such as a roster contact, @@ -107,8 +110,8 @@ export default { * returned VCard data. * * @method _converse.api.vcard.update - * @param { Model } model A `Model` instance - * @param { boolean } [force] A boolean indicating whether the vcard should be + * @param {Model} model A `Model` instance + * @param {boolean} [force] A boolean indicating whether the vcard should be * fetched again even if it's been fetched before. * @returns {promise} A promise which resolves once the update has completed. * @example @@ -120,7 +123,7 @@ export default { */ async update (model, force) { const data = await this.get(model, force); - model = typeof model === 'string' ? _converse.vcards.get(model) : model; + model = typeof model === 'string' ? _converse.exports.vcards.get(model) : model; if (!model) { log.error(`Could not find a VCard model for ${model}`); return; diff --git a/src/headless/plugins/vcard/index.js b/src/headless/plugins/vcard/index.js index 192f07542d..bea06c01fe 100644 --- a/src/headless/plugins/vcard/index.js +++ b/src/headless/plugins/vcard/index.js @@ -72,8 +72,9 @@ converse.plugins.add('converse-vcard', { initialize () { api.promises.add('VCardsInitialized'); - _converse.VCard = VCard; - _converse.VCards = VCards; + const exports = { VCard, VCards }; + Object.assign(_converse, exports); // XXX DEPRECATED + Object.assign(_converse.exports, exports); api.listen.on('chatRoomInitialized', (m) => { setVCardOnModel(m) diff --git a/src/headless/plugins/vcard/utils.js b/src/headless/plugins/vcard/utils.js index 8b5e9a6d92..da53ebda1d 100644 --- a/src/headless/plugins/vcard/utils.js +++ b/src/headless/plugins/vcard/utils.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../muc/occupant.js').default} ChatRoomOccupant + * @typedef {import('../chat/model-with-contact.js').default} ModelWithContact + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from "../../log.js"; @@ -7,7 +11,10 @@ import { shouldClearCache } from '../../utils/session.js'; const { Strophe, $iq, u } = converse.env; -async function onVCardData (jid, iq) { +/** + * @param {Element} iq + */ +async function onVCardData (iq) { const vcard = iq.querySelector('vCard'); let result = {}; if (vcard !== null) { @@ -21,7 +28,8 @@ async function onVCardData (jid, iq) { 'role': vcard.querySelector('ROLE')?.textContent, 'email': vcard.querySelector('EMAIL USERID')?.textContent, 'vcard_updated': (new Date()).toISOString(), - 'vcard_error': undefined + 'vcard_error': undefined, + image_hash: undefined, }; } if (result.image) { @@ -44,20 +52,26 @@ export function createStanza (type, jid, vcard_el) { } +/** + * @param {ChatRoomOccupant} occupant + */ export function onOccupantAvatarChanged (occupant) { const hash = occupant.get('image_hash'); const vcards = []; if (occupant.get('jid')) { - vcards.push(_converse.vcards.get(occupant.get('jid'))); + vcards.push(_converse.state.vcards.get(occupant.get('jid'))); } - vcards.push(_converse.vcards.get(occupant.get('from'))); + vcards.push(_converse.state.vcards.get(occupant.get('from'))); vcards.forEach(v => (hash && v?.get('image_hash') !== hash) && api.vcard.update(v, true)); } +/** + * @param {ModelWithContact} model + */ export async function setVCardOnModel (model) { let jid; - if (model instanceof _converse.Message) { + if (model instanceof _converse.exports.Message) { if (['error', 'info'].includes(model.get('type'))) { return; } @@ -72,22 +86,24 @@ export async function setVCardOnModel (model) { } await api.waitUntil('VCardsInitialized'); - model.vcard = _converse.vcards.get(jid) || _converse.vcards.create({ jid }); + const { vcards } = _converse.state; + model.vcard = vcards.get(jid) || vcards.create({ jid }); model.vcard.on('change', () => model.trigger('vcard:change')); model.trigger('vcard:add'); } function getVCardForOccupant (occupant) { + const { vcards, xmppstatus } = _converse.state; const muc = occupant?.collection?.chatroom; const nick = occupant.get('nick'); if (nick && muc?.get('nick') === nick) { - return _converse.xmppstatus.vcard; + return xmppstatus.vcard; } else { const jid = occupant.get('jid') || occupant.get('from'); if (jid) { - return _converse.vcards.get(jid) || _converse.vcards.create({ jid }); + return vcards.get(jid) || vcards.create({ jid }); } else { log.warn(`Could not get VCard for occupant because no JID found!`); return; @@ -106,15 +122,16 @@ export async function setVCardOnOccupant (occupant) { function getVCardForMUCMessage (message) { + const { vcards, xmppstatus } = _converse.state; const muc = message?.collection?.chatbox; const nick = Strophe.getResourceFromJid(message.get('from')); if (nick && muc?.get('nick') === nick) { - return _converse.xmppstatus.vcard; + return xmppstatus.vcard; } else { const jid = message.occupant?.get('jid') || message.get('from'); if (jid) { - return _converse.vcards.get(jid) || _converse.vcards.create({ jid }); + return vcards.get(jid) || vcards.create({ jid }); } else { log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get('msgid')}`); return; @@ -137,24 +154,24 @@ export async function setVCardOnMUCMessage (message) { export async function initVCardCollection () { - _converse.vcards = new _converse.VCards(); - const id = `${_converse.bare_jid}-converse.vcards`; - initStorage(_converse.vcards, id); + const vcards = new _converse.exports.VCards(); + _converse.state.vcards = vcards; + Object.assign(_converse, { vcards }); // XXX DEPRECATED + + const bare_jid = _converse.session.get('bare_jid'); + const id = `${bare_jid}-converse.vcards`; + initStorage(vcards, id); await new Promise(resolve => { - _converse.vcards.fetch({ + vcards.fetch({ 'success': resolve, 'error': resolve }, {'silent': true}); }); - const vcards = _converse.vcards; - if (_converse.session) { - const jid = _converse.session.get('bare_jid'); - const status = _converse.xmppstatus; - status.vcard = vcards.get(jid) || vcards.create({'jid': jid}); - if (status.vcard) { - status.vcard.on('change', () => status.trigger('vcard:change')); - status.trigger('vcard:add'); - } + const { xmppstatus } = _converse.state; + xmppstatus.vcard = vcards.get(bare_jid) || vcards.create({'jid': bare_jid}); + if (xmppstatus.vcard) { + xmppstatus.vcard.on('change', () => xmppstatus.trigger('vcard:change')); + xmppstatus.trigger('vcard:add'); } /** * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache. @@ -167,15 +184,17 @@ export async function initVCardCollection () { export function clearVCardsSession () { if (shouldClearCache()) { api.promises.add('VCardsInitialized'); - if (_converse.vcards) { - _converse.vcards.clearStore(); - delete _converse.vcards; + if (_converse.state.vcards) { + _converse.state.vcards.clearStore(); + Object.assign(_converse, { vcards: undefined }); // XXX DEPRECATED + delete _converse.state.vcards; } } } export async function getVCard (jid) { - const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid; + const bare_jid = _converse.session.get('bare_jid'); + const to = Strophe.getBareJidFromJid(jid) === bare_jid ? null : jid; let iq; try { iq = await api.sendIQ(createStanza("get", to)) @@ -186,5 +205,5 @@ export async function getVCard (jid) { 'vcard_error': (new Date()).toISOString() } } - return onVCardData(jid, iq); + return onVCardData(iq); } diff --git a/src/headless/plugins/vcard/vcard.js b/src/headless/plugins/vcard/vcard.js index 2e122654a0..7fd13c6140 100644 --- a/src/headless/plugins/vcard/vcard.js +++ b/src/headless/plugins/vcard/vcard.js @@ -1,5 +1,5 @@ import _converse from '../../shared/_converse.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; /** * Represents a VCard diff --git a/src/headless/plugins/vcard/vcards.js b/src/headless/plugins/vcard/vcards.js index 728f0a618b..fb077dff61 100644 --- a/src/headless/plugins/vcard/vcards.js +++ b/src/headless/plugins/vcard/vcards.js @@ -1,5 +1,5 @@ import VCard from "./vcard"; -import { Collection } from "@converse/skeletor/src/collection"; +import { Collection } from "@converse/skeletor"; import { api } from "../../index.js"; class VCards extends Collection { diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js index e0cc8e58e6..a8e1bff889 100644 --- a/src/headless/shared/_converse.js +++ b/src/headless/shared/_converse.js @@ -1,49 +1,39 @@ -import i18n from './i18n.js'; +/** + * @module:shared.converse + * @typedef {import('@converse/skeletor/src/storage.js').Storage} Storage + */ import log from '../log.js'; +import i18n from './i18n.js'; import pluggable from 'pluggable.js/src/pluggable.js'; -import { Events } from '@converse/skeletor/src/events.js'; +import { EventEmitter, Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; +import { isTestEnv } from '../utils/session.js'; import { ACTIVE, ANONYMOUS, - CHATROOMS_TYPE, CLOSED, COMPOSING, - CONTROLBOX_TYPE, DEFAULT_IMAGE, DEFAULT_IMAGE_TYPE, EXTERNAL, FAILURE, GONE, - HEADLINES_TYPE, INACTIVE, LOGIN, LOGOUT, OPENED, PAUSED, PREBIND, - PRIVATE_CHAT_TYPE, SUCCESS, VERSION_NAME } from './constants'; -/** - * A private, closured object containing the private api (via {@link _converse.api}) - * as well as private methods and internal data-structures. - * @global - * @namespace _converse - */ -const _converse = { - VERSION_NAME, - - templates: {}, - promises: { - 'initialized': getOpenPromise() - }, +const DEPRECATED_ATTRS = { + chatboxes: null, + bookmarks: null, - // TODO: remove constants in next major release ANONYMOUS, CLOSED, EXTERNAL, @@ -51,39 +41,119 @@ const _converse = { LOGOUT, OPENED, PREBIND, - SUCCESS, FAILURE, - - DEFAULT_IMAGE_TYPE, - DEFAULT_IMAGE, - INACTIVE, ACTIVE, COMPOSING, PAUSED, GONE, +} + + +/** + * A private, closured namespace containing the private api (via {@link _converse.api}) + * as well as private methods and internal data-structures. + * @global + * @namespace _converse + */ +class ConversePrivateGlobal extends EventEmitter(Object) { + + constructor () { + super(); + const proxy = new Proxy(this, { + get: (target, key) => { + if (!isTestEnv() && typeof key === 'string') { + if (Object.keys(DEPRECATED_ATTRS).includes(key)) { + log.warn(`Accessing ${key} on _converse is DEPRECATED`); + } + } + return Reflect.get(target, key) + } + }); + proxy.initialize(); + return proxy; + } + + initialize () { + this.VERSION_NAME = VERSION_NAME; + + this.strict_plugin_dependencies = false; + + this.pluggable = null; + + this.templates = {}; + + this.storage = /** @type {Record} */{}; + + this.promises = { + 'initialized': getOpenPromise(), + }; + + this.DEFAULT_IMAGE_TYPE = DEFAULT_IMAGE_TYPE; + this.DEFAULT_IMAGE = DEFAULT_IMAGE; + + // Set as module attr so that we can override in tests. + // TODO: replace with config settings + this.TIMEOUTS = { + PAUSED: 10000, + INACTIVE: 90000 + }; + + Object.assign(this, DEPRECATED_ATTRS); + + this.api = /** @type {module:shared-api.APIEndpoint} */ null; + + /** + * Namespace for storing translated strings. + */ + this.labels = + /** + * @typedef {Record} UserMessage + * @typedef {Record} UserMessage + * @type {UserMessages} */{}; + + /** + * Namespace for storing code that might be useful to 3rd party + * plugins. We want to make it possible for 3rd party plugins to have + * access to code (e.g. classes) from converse.js without having to add + * converse.js as a dependency. + */ + this.exports = /** @type {Record} */{}; + + /** + * Namespace for storing the state, as represented by instances of + * Models and Collections. + */ + this.state = /** @type {Record} */{}; + + this.initSession(); + } - PRIVATE_CHAT_TYPE, - CHATROOMS_TYPE, - HEADLINES_TYPE, - CONTROLBOX_TYPE, + initSession () { + this.session?.destroy(); + this.session = new Model(); - // Set as module attr so that we can override in tests. - // TODO: replace with config settings - TIMEOUTS: { - PAUSED: 10000, - INACTIVE: 90000 - }, + // XXX DEPRECATED + Object.assign( + this, { + jid: undefined, + bare_jid: undefined, + domain: undefined, + resource: undefined + } + ); + } /** * Translate the given string based on the current locale. * @method __ - * @private * @memberOf _converse - * @param { ...String } args + * @param {...String} args */ - '__': (...args) => i18n.__(...args), + __ (...args) { + return i18n.__(...args); + } /** * A no-op method which is used to signal to gettext that the passed in string @@ -97,15 +167,15 @@ const _converse = { * and we don't yet have the variables at scan time. * * @method ___ - * @private * @memberOf _converse - * @param { String } str + * @param {String} str */ - '___': str => str + ___ (str) { + return str; + } } -// Make _converse an event emitter -Object.assign(_converse, Events); +const _converse = new ConversePrivateGlobal(); // Make _converse pluggable pluggable.enable(_converse, '_converse', 'pluggable'); diff --git a/src/headless/shared/actions.js b/src/headless/shared/actions.js index 36af0e425f..fdb901fac9 100644 --- a/src/headless/shared/actions.js +++ b/src/headless/shared/actions.js @@ -28,7 +28,7 @@ export function rejectMessage (stanza, text) { * @param { String } to_jid * @param { String } id - The id of the message being marked * @param { String } type - The marker type - * @param { String } msg_type + * @param { String } [msg_type] */ export function sendMarker (to_jid, id, type, msg_type) { const stanza = $msg({ diff --git a/src/headless/shared/api/events.js b/src/headless/shared/api/events.js index 86f81368a2..d78863b189 100644 --- a/src/headless/shared/api/events.js +++ b/src/headless/shared/api/events.js @@ -10,21 +10,21 @@ export default { * * Some events also double as promises and can be waited on via {@link _converse.api.waitUntil}. * - * @method _converse.api.trigger - * @param { string } name - The event name - * @param {...any} [argument] - Argument to be passed to the event handler - * @param { object } [options] - * @param { boolean } [options.synchronous] - Whether the event is synchronous or not. + * @typedef {object} Options + * @property {boolean} [Options.synchronous] - Whether the event is synchronous or not. * When a synchronous event is fired, a promise will be returned * by {@link _converse.api.trigger} which resolves once all the * event handlers' promises have been resolved. + * + * @method _converse.api.trigger + * @param { string } name - The event name */ async trigger (name) { if (!_converse._events) { return; } const args = Array.from(arguments); - const options = args.pop(); + const options = /** @type {Options} */(args.pop()); if (options && options.synchronous) { const events = _converse._events[name] || []; const event_args = args.splice(1); @@ -118,7 +118,7 @@ export default { } else { options = options || {}; } - api.connection.get().addHandler( + _converse.api.connection.get().addHandler( handler, options.ns, name, diff --git a/src/headless/shared/api/index.js b/src/headless/shared/api/index.js index af83dba846..d404038865 100644 --- a/src/headless/shared/api/index.js +++ b/src/headless/shared/api/index.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../_converse.js').default} _converse + * @typedef {module:shared-api.APIEndpoint} APIEndpoint + */ import connection_api from '../connection/api.js'; import events_api from './events.js'; import promise_api from './promise.js'; @@ -17,7 +21,10 @@ import { settings_api } from '../settings/api.js'; * * @memberOf _converse * @namespace api - * @property {Object} disco + * @typedef {Record} module:shared-api.APIEndpoint + * @typedef {Record} APINamespace + * @typedef {Record} API + * @type {API} */ const api = { connection: connection_api, @@ -28,6 +35,8 @@ const api = { ...promise_api, disco: null, + elements: null, + contacts: null, }; export default api; diff --git a/src/headless/shared/api/presence.js b/src/headless/shared/api/presence.js index a0ff977529..39c35127c8 100644 --- a/src/headless/shared/api/presence.js +++ b/src/headless/shared/api/presence.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder + */ import _converse from '../_converse.js'; import api from '../../shared/api/index.js'; @@ -21,7 +24,7 @@ export default { if (child_nodes && !Array.isArray(child_nodes)) { child_nodes = [child_nodes]; } - const model = _converse.xmppstatus + const model = _converse.state.xmppstatus const presence = await model.constructPresence(type, to, status); child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up()); api.send(presence); diff --git a/src/headless/shared/api/promise.js b/src/headless/shared/api/promise.js index e44f6c7318..6ee600f81f 100644 --- a/src/headless/shared/api/promise.js +++ b/src/headless/shared/api/promise.js @@ -43,8 +43,8 @@ export default { * via {@link _converse.api.listen}). * * @method _converse.api.promises.add - * @param {string|array} [name|names] The name or an array of names for the promise(s) to be added - * @param { boolean } [replace=true] Whether this promise should be replaced with a new one when the user logs out. + * @param {string|array} [promises] The name or an array of names for the promise(s) to be added + * @param {boolean} [replace=true] Whether this promise should be replaced with a new one when the user logs out. * @example _converse.api.promises.add('foo-completed'); */ add (promises, replace=true) { @@ -67,7 +67,7 @@ export default { */ waitUntil (condition) { if (isFunction(condition)) { - return waitUntil(condition); + return waitUntil(/** @type {Function} */(condition)); } else { const promise = _converse.promises[condition]; if (promise === undefined) { diff --git a/src/headless/shared/api/public.js b/src/headless/shared/api/public.js index 08ffc4b97e..54d2bdad17 100644 --- a/src/headless/shared/api/public.js +++ b/src/headless/shared/api/public.js @@ -1,3 +1,6 @@ +/** + * @typedef {module:shared-api-public.ConversePrivateGlobal} ConversePrivateGlobal + */ import ConnectionFeedback from './../connection/feedback.js'; import URI from 'urijs'; import _converse from '../_converse.js'; @@ -7,9 +10,8 @@ import log from '../../log.js'; import sizzle from 'sizzle'; import u, { setLogLevelFromRoute } from '../../utils/index.js'; import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js'; -import { setUnloadEvent, isTestEnv } from '../../utils/session.js'; -import { Collection } from "@converse/skeletor/src/collection"; -import { Model } from '@converse/skeletor/src/model.js'; +import { isTestEnv } from '../../utils/session.js'; +import { Collection, Model } from "@converse/skeletor"; import { Strophe, $build, $iq, $msg, $pres, stx } from 'strophe.js'; import { TimeoutError } from '../errors.js'; import { filesize } from 'filesize'; @@ -26,6 +28,8 @@ import { } from '../../utils/init.js'; /** + * @typedef {Window & {converse: ConversePrivateGlobal} } window + * * ### The Public API * * This namespace contains public API methods which are are @@ -38,7 +42,7 @@ import { * @global * @namespace converse */ -export const converse = Object.assign(window.converse || {}, { +export const converse = Object.assign(/** @type {ConversePrivateGlobal} */(window).converse || {}, { CHAT_STATES, @@ -68,7 +72,6 @@ export const converse = Object.assign(window.converse || {}, { const { api } = _converse; await cleanup(_converse); - setUnloadEvent(); initAppSettings(settings); _converse.strict_plugin_dependencies = settings.strict_plugin_dependencies; // Needed by pluggable.js log.setLogLevel(api.settings.get("loglevel")); @@ -84,16 +87,9 @@ export const converse = Object.assign(window.converse || {}, { setLogLevelFromRoute(); addEventListener('hashchange', setLogLevelFromRoute); - _converse.connfeedback = new ConnectionFeedback(); - - /* When reloading the page: - * For new sessions, we need to send out a presence stanza to notify - * the server/network that we're online. - * When re-attaching to an existing session we don't need to again send out a presence stanza, - * because it's as if "we never left" (see onConnectStatusChanged). - * https://github.com/conversejs/converse.js/issues/521 - */ - _converse.send_initial_presence = true; + const connfeedback = new ConnectionFeedback(); + Object.assign(_converse, { connfeedback }); // XXX: DEPRECATED + Object.assign(_converse.state, { connfeedback }); await initSessionStorage(_converse); await initClientConfig(_converse); diff --git a/src/headless/shared/api/send.js b/src/headless/shared/api/send.js index aefd51524e..e016eb27f4 100644 --- a/src/headless/shared/api/send.js +++ b/src/headless/shared/api/send.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder + */ import _converse from '../_converse.js'; import log from '../../log.js'; import { Strophe, toStanza } from 'strophe.js'; @@ -7,8 +10,8 @@ export default { /** * Allows you to send XML stanzas. * @method _converse.api.send - * @param { Element | Stanza } stanza - * @return { void } + * @param {Element|Strophe.Builder} stanza + * @return {void} * @example * const msg = converse.env.$msg({ * 'from': 'juliet@example.com/balcony', @@ -41,12 +44,12 @@ export default { /** * Send an IQ stanza * @method _converse.api.sendIQ - * @param { Element } stanza - * @param { number } [timeout] - The default timeout value is taken from + * @param {Element|Strophe.Builder} stanza + * @param {number} [timeout] - The default timeout value is taken from * the `stanza_timeout` configuration setting. - * @param { Boolean } [reject=true] - Whether an error IQ should cause the promise + * @param {boolean} [reject=true] - Whether an error IQ should cause the promise * to be rejected. If `false`, the promise will resolve instead of being rejected. - * @returns { Promise } A promise which resolves (or potentially rejected) once we + * @returns {Promise} A promise which resolves (or potentially rejected) once we * receive a `result` or `error` stanza or once a timeout is reached. * If the IQ stanza being sent is of type `result` or `error`, there's * nothing to wait for, so an already resolved promise is returned. diff --git a/src/headless/shared/api/user.js b/src/headless/shared/api/user.js index 6aff1afc42..c187c192ba 100644 --- a/src/headless/shared/api/user.js +++ b/src/headless/shared/api/user.js @@ -1,3 +1,6 @@ +/** + * @module:shared.api.user + */ import _converse from '../_converse.js'; import presence_api from './presence.js'; import connection_api from '../connection/api.js'; @@ -5,9 +8,9 @@ import { replacePromise } from '../../utils/session.js'; import { attemptNonPreboundSession, setUserJID } from '../../utils/init.js'; import { getOpenPromise } from '@converse/openpromise'; import { user_settings_api } from '../settings/api.js'; -import { LOGOUT, PREBIND } from '../constants.js'; +import { LOGOUT } from '../constants.js'; -export default { +const api = { /** * This grouping collects API functions related to the current logged in user. * @@ -57,15 +60,31 @@ export default { jid = await setUserJID(jid); } - // See whether there is a BOSH session to re-attach to - const bosh_plugin = _converse.pluggable.plugins['converse-bosh']; - if (bosh_plugin?.enabled()) { - if (await _converse.restoreBOSHSession()) { - return; - } else if (api.settings.get("authentication") === PREBIND && (!automatic || api.settings.get("auto_login"))) { - return _converse.startNewPreboundBOSHSession(); - } - } + /** + * *Hook* which allows 3rd party code to attempt logging in before + * the core code attempts it. + * + * Note: If the hook handler has logged the user in, it should set the + * `success` flag on the payload to `true`. + * + * @typedef {Object} LoginHookPayload + * @property {string} jid + * @property {string} password + * @property {boolean} [automatic] - An internally used flag that indicates whether + * this method was called automatically once the connection has been initialized. + * @property {boolean} [success] - A flag which indicates whether + * login has succeeded. If a hook handler receives a payload with + * this flag, it should NOT attempt to log in. + * If a handler has successfully logged in, it should return the + * payload with this flag set to true. + * + * @event _converse#login + * @param {typeof api.user} context + * @param {LoginHookPayload} payload + */ + const { success } = await _converse.api.hook('login', this, { jid, password, automatic }); + if (success) return; + password = password || api.settings.get("password"); const credentials = (jid && password) ? { jid, password } : null; attemptNonPreboundSession(credentials, automatic); @@ -88,7 +107,6 @@ export default { const complete = () => { // Recreate all the promises Object.keys(_converse.promises).forEach(replacePromise); - delete _converse.jid // Remove the session JID, otherwise the user would just be logged // in again upon reload. See #2759 @@ -115,3 +133,5 @@ export default { } } } + +export default api; diff --git a/src/headless/shared/chat/utils.js b/src/headless/shared/chat/utils.js index a79e8ca8d7..e0e6104b63 100644 --- a/src/headless/shared/chat/utils.js +++ b/src/headless/shared/chat/utils.js @@ -1,8 +1,19 @@ +/** + * @module:headless-shared-chat-utils + * @typedef {import('../../plugins/muc/muc.js').default} MUC + * @typedef {import('../../plugins/chat/model.js').default} ChatBox + * @typedef {import('../../plugins/chat/message.js').default} Message + * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata + * @typedef {module:headless-shared-chat-utils.MediaURLData} MediaURLData + */ import debounce from 'lodash-es/debounce.js'; import api, { converse } from '../../shared/api/index.js'; const { u } = converse.env; +/** + * @param {ChatBox|MUC} model + */ export function pruneHistory (model) { const max_history = api.settings.get('prune_messages_above'); if (max_history && typeof max_history === 'number') { @@ -17,7 +28,7 @@ export function pruneHistory (model) { * once older messages have been removed to keep the * number of messages below the value set in `prune_messages_above`. * @event _converse#historyPruned - * @type { _converse.ChatBox | _converse.ChatRoom } + * @type { ChatBox | MUC } * @example _converse.api.listen.on('historyPruned', this => { ... }); */ api.trigger('historyPruned', model); @@ -29,20 +40,20 @@ export function pruneHistory (model) { /** * Given an array of {@link MediaURLMetadata} objects and text, return an * array of {@link MediaURL} objects. - * @param { Array } arr - * @param { String } text - * @returns{ Array } + * @param {Array} arr + * @param {String} text + * @returns{Array} */ export function getMediaURLs (arr, text, offset=0) { /** - * @typedef { Object } MediaURLData + * @typedef {Object} MediaURLData * An object representing a URL found in a chat message - * @property { Boolean } is_audio - * @property { Boolean } is_image - * @property { Boolean } is_video - * @property { String } end - * @property { String } start - * @property { String } url + * @property {Boolean} is_audio + * @property {Boolean} is_image + * @property {Boolean} is_video + * @property {String} end + * @property {String} start + * @property {String} url */ return arr.map(o => { const start = o.start - offset; @@ -63,11 +74,11 @@ export function getMediaURLs (arr, text, offset=0) { * Determines whether the given attributes of an incoming message * represent a XEP-0308 correction and, if so, handles it appropriately. * @private - * @method _converse.ChatBox#handleCorrection - * @param { _converse.ChatBox | _converse.ChatRoom } - * @param { object } attrs - Attributes representing a received + * @method ChatBox#handleCorrection + * @param {ChatBox|MUC} model + * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMessage} - * @returns { _converse.Message|undefined } Returns the corrected + * @returns {Promise} Returns the corrected * message or `undefined` if not applicable. */ export async function handleCorrection (model, attrs) { @@ -108,4 +119,6 @@ export async function handleCorrection (model, attrs) { } -export const debouncedPruneHistory = debounce(pruneHistory, 500); +export const debouncedPruneHistory = debounce(pruneHistory, 500, { + maxWait: 2000 +}); diff --git a/src/headless/shared/connection/api.js b/src/headless/shared/connection/api.js index 04126adb44..0fb88ebda1 100644 --- a/src/headless/shared/connection/api.js +++ b/src/headless/shared/connection/api.js @@ -23,7 +23,7 @@ export default { * @method api.connection.init * @memberOf api.connection * @param {string} [jid] - * @return {Connection | MockConnection} + * @return {Connection|MockConnection} */ init (jid) { if (jid && connection?.jid && isSameDomain(connection.jid, jid)) return connection; diff --git a/src/headless/shared/connection/feedback.js b/src/headless/shared/connection/feedback.js index 95400063b2..250dd599fd 100644 --- a/src/headless/shared/connection/feedback.js +++ b/src/headless/shared/connection/feedback.js @@ -1,5 +1,5 @@ import _converse from '../_converse'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { Strophe } from 'strophe.js'; @@ -15,7 +15,7 @@ class Feedback extends Model { initialize () { super.initialize(); const { api } = _converse; - this.on('change', () => api.trigger('connfeedback', _converse.connfeedback)); + this.on('change', () => api.trigger('connfeedback', _converse.state.connfeedback)); } } diff --git a/src/headless/shared/connection/index.js b/src/headless/shared/connection/index.js index db42749c7a..806175616c 100644 --- a/src/headless/shared/connection/index.js +++ b/src/headless/shared/connection/index.js @@ -23,6 +23,11 @@ export class Connection extends Strophe.Connection { constructor (service, options) { super(service, options); + // For new sessions, we need to send out a presence stanza to notify + // the server/network that we're online. + // When re-attaching to an existing session we don't need to again send out a presence stanza, + // because it's as if "we never left" (see onConnectStatusChanged). + this.send_initial_presence = true; this.debouncedReconnect = debounce(this.reconnect, 3000); } @@ -68,11 +73,12 @@ export class Connection extends Strophe.Connection { * connection of their own XMPP server instead of a proxy provided by the * host of Converse.js. * @method Connnection.discoverConnectionMethods + * @param {string} domain */ async discoverConnectionMethods (domain) { // Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods. const options = { - 'mode': 'cors', + 'mode': /** @type {RequestMode} */('cors'), 'headers': { 'Accept': 'application/xrd+xml, text/xml' } @@ -97,9 +103,9 @@ export class Connection extends Strophe.Connection { * Establish a new XMPP session by logging in with the supplied JID and * password. * @method Connnection.connect - * @param { String } jid - * @param { String } password - * @param { Funtion } callback + * @param {String} jid + * @param {String} password + * @param {Function} callback */ async connect (jid, password, callback) { if (api.settings.get("discover_connection_methods")) { @@ -112,6 +118,14 @@ export class Connection extends Strophe.Connection { super.connect(jid, password, callback || this.onConnectStatusChanged, BOSH_WAIT); } + /** + * @param {string} reason + */ + disconnect(reason) { + super.disconnect(reason); + this.send_initial_presence = true; + } + /** * Switch to a different transport if a service URL is available for it. * @@ -123,8 +137,9 @@ export class Connection extends Strophe.Connection { * for the old transport are removed. */ async switchTransport () { + const bare_jid = _converse.session.get('bare_jid'); if (api.connection.isType('websocket') && api.settings.get('bosh_service_url')) { - await setUserJID(_converse.bare_jid); + await setUserJID(bare_jid); this._proto._doDisconnect(); this._proto = new Strophe.Bosh(this); this.service = api.settings.get('bosh_service_url'); @@ -135,7 +150,7 @@ export class Connection extends Strophe.Connection { // (now failed) session. await setUserJID(api.settings.get("jid")); } else { - await setUserJID(_converse.bare_jid); + await setUserJID(bare_jid); } this._proto._doDisconnect(); this._proto = new Strophe.Websocket(this); @@ -148,7 +163,7 @@ export class Connection extends Strophe.Connection { this.reconnecting = true; await tearDown(); - const conn_status = _converse.connfeedback.get('connection_status'); + const conn_status = _converse.state.connfeedback.get('connection_status'); if (conn_status === Strophe.Status.CONNFAIL) { this.switchTransport(); } else if (conn_status === Strophe.Status.AUTHFAIL && api.settings.get("authentication") === ANONYMOUS) { @@ -168,14 +183,15 @@ export class Connection extends Strophe.Connection { if (api.settings.get("authentication") === ANONYMOUS) { await clearSession(); } - return api.user.login(_converse.jid); + const jid = _converse.session.get('jid'); + return api.user.login(jid); } /** * Called as soon as a new connection has been established, either * by logging in or by attaching to an existing BOSH session. * @method Connection.onConnected - * @param { Boolean } reconnecting - Whether Converse.js reconnected from an earlier dropped session. + * @param {Boolean} [reconnecting] - Whether Converse.js reconnected from an earlier dropped session. */ async onConnected (reconnecting) { delete this.reconnecting; @@ -184,8 +200,9 @@ export class Connection extends Strophe.Connection { // Save the current JID in persistent storage so that we can attempt to // recreate the session from SCRAM keys - if (_converse.config.get('trusted')) { - localStorage.setItem('conversejs-session-jid', _converse.bare_jid); + if (_converse.state.config.get('trusted')) { + const bare_jid = _converse.session.get('bare_jid'); + localStorage.setItem('conversejs-session-jid', bare_jid); } /** @@ -218,10 +235,10 @@ export class Connection extends Strophe.Connection { * Used to keep track of why we got disconnected, so that we can * decide on what the next appropriate action is (in onDisconnected) * @method Connection.setDisconnectionCause - * @param { Number } cause - The status number as received from Strophe. - * @param { String } [reason] - An optional user-facing message as to why + * @param {Number|'logout'} [cause] - The status number as received from Strophe. + * @param {String} [reason] - An optional user-facing message as to why * there was a disconnection. - * @param { Boolean } [override] - An optional flag to replace any previous + * @param {Boolean} [override] - An optional flag to replace any previous * disconnection cause and reason. */ setDisconnectionCause (cause, reason, override) { @@ -234,9 +251,13 @@ export class Connection extends Strophe.Connection { } } + /** + * @param {Number} [status] - The status number as received from Strophe. + * @param {String} [message] - An optional user-facing message + */ setConnectionStatus (status, message) { this.status = status; - _converse.connfeedback.set({'connection_status': status, message }); + _converse.state.connfeedback.set({'connection_status': status, message }); } async finishDisconnection () { @@ -306,8 +327,8 @@ export class Connection extends Strophe.Connection { * Callback method called by Strophe as the Connection goes * through various states while establishing or tearing down a * connection. - * @param { Number } status - * @param { String } message + * @param {Number} status + * @param {String} message */ onConnectStatusChanged (status, message) { const { __ } = _converse; @@ -324,8 +345,6 @@ export class Connection extends Strophe.Connection { this.setConnectionStatus(status); this.worker_attach_promise?.resolve(true); - // By default we always want to send out an initial presence stanza. - _converse.send_initial_presence = true; this.setDisconnectionCause(); if (this.reconnecting) { log.debug(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); @@ -335,7 +354,7 @@ export class Connection extends Strophe.Connection { if (this.restored) { // No need to send an initial presence stanza when // we're restoring an existing session. - _converse.send_initial_presence = false; + this.send_initial_presence = false; } this.onConnected(); } @@ -375,6 +394,9 @@ export class Connection extends Strophe.Connection { } } + /** + * @param {string} type + */ isType (type) { if (type.toLowerCase() === 'websocket') { return this._proto instanceof Strophe.Websocket; @@ -385,7 +407,7 @@ export class Connection extends Strophe.Connection { hasResumed () { if (api.settings.get("connection_options")?.worker || this.isType('bosh')) { - return _converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED; + return _converse.state.connfeedback.get('connection_status') === Strophe.Status.ATTACHED; } else { // Not binding means that the session was resumed. return !this.do_bind; @@ -425,10 +447,12 @@ export class MockConnection extends Connection { ''+ ''+ ''+ - '').firstChild; + '').firstElementChild; + // eslint-disable-next-line @typescript-eslint/no-empty-function this._proto._processRequest = () => {}; this._proto._disconnect = () => this._onDisconnectTimeout(); + // eslint-disable-next-line @typescript-eslint/no-empty-function this._proto._onDisconnectTimeout = () => {}; this._proto._connect = () => { this.connected = true; @@ -460,8 +484,6 @@ export class MockConnection extends Connection { async bind () { await api.trigger('beforeResourceBinding', {'synchronous': true}); this.authenticated = true; - if (!_converse.no_connection_on_bind) { - this._changeConnectStatus(Strophe.Status.CONNECTED); - } + this._changeConnectStatus(Strophe.Status.CONNECTED); } } diff --git a/src/headless/shared/errors.js b/src/headless/shared/errors.js index 3174fa3104..59ebe4c165 100644 --- a/src/headless/shared/errors.js +++ b/src/headless/shared/errors.js @@ -2,4 +2,13 @@ * Custom error for indicating timeouts * @namespace converse.env */ -export class TimeoutError extends Error {} +export class TimeoutError extends Error { + + /** + * @param {string} message + */ + constructor (message) { + super(message); + this.retry_event_id = null; + } +} diff --git a/src/headless/shared/i18n.js b/src/headless/shared/i18n.js index dbe327ff2c..e944459aa3 100644 --- a/src/headless/shared/i18n.js +++ b/src/headless/shared/i18n.js @@ -4,6 +4,7 @@ import { sprintf } from 'sprintf-js'; * @namespace i18n */ export default { + // eslint-disable-next-line @typescript-eslint/no-empty-function initialize () {}, /** @@ -18,7 +19,6 @@ export default { * @method __ * @private * @memberOf i18n - * @param { String } str */ __ (...args) { return sprintf(...args); diff --git a/src/headless/shared/parsers.js b/src/headless/shared/parsers.js index ca3bce80d4..00cb34dc8c 100644 --- a/src/headless/shared/parsers.js +++ b/src/headless/shared/parsers.js @@ -1,3 +1,8 @@ +/** + * @module:headless-shared-parsers + * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata + * @typedef {module:headless-shared-parsers.Reference} Reference + */ import URI from 'urijs'; import _converse from './_converse.js'; import api from './api/index.js'; @@ -18,8 +23,12 @@ import { const { NS } = Strophe; export class StanzaParseError extends Error { + /** + * @param {string} message + * @param {Element} stanza + */ constructor (message, stanza) { - super(message, stanza); + super(message); this.name = 'StanzaParseError'; this.stanza = stanza; } @@ -28,9 +37,10 @@ export class StanzaParseError extends Error { /** * Extract the XEP-0359 stanza IDs from the passed in stanza * and return a map containing them. - * @private - * @param { Element } stanza - The message stanza - * @returns { Object } + * @param {Element} stanza - The message stanza + * @param {Element} original_stanza - The encapsulating stanza which contains + * the message stanza. + * @returns {Object} */ export function getStanzaIDs (stanza, original_stanza) { const attrs = {}; @@ -45,7 +55,8 @@ export function getStanzaIDs (stanza, original_stanza) { // Store the archive id const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(); if (result) { - const by_jid = original_stanza.getAttribute('from') || _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + const by_jid = original_stanza.getAttribute('from') || bare_jid; attrs[`stanza_id ${by_jid}`] = result.getAttribute('id'); } @@ -57,6 +68,9 @@ export function getStanzaIDs (stanza, original_stanza) { return attrs; } +/** + * @param {Element} stanza + */ export function getEncryptionAttributes (stanza) { const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop(); const namespace = eme_tag?.getAttribute('namespace'); @@ -151,6 +165,10 @@ export function getOpenGraphMetadata (stanza) { } +/** + * @param {string} text + * @param {number} offset + */ export function getMediaURLsMetadata (text, offset=0) { const objs = []; if (!text) { @@ -241,7 +259,7 @@ export function getErrorAttributes (stanza) { /** * Given a message stanza, find and return any XEP-0372 references - * @param { Element } stana - The message stanza + * @param {Element} stanza - The message stanza * @returns { Reference } */ export function getReferences (stanza) { @@ -272,6 +290,9 @@ export function getReferences (stanza) { }).filter(r => r); } +/** + * @param {Element} stanza + */ export function getReceiptId (stanza) { const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop(); return receipt?.getAttribute('id'); @@ -333,10 +354,9 @@ export function throwErrorIfInvalidForward (stanza) { /** * Determines whether the passed in stanza is a XEP-0333 Chat Marker - * @private * @method getChatMarker - * @param { Element } stanza - The message stanza - * @returns { Boolean } + * @param {Element} stanza - The message stanza + * @returns {Element} */ export function getChatMarker (stanza) { // If we receive more than one marker (which shouldn't happen), we take @@ -349,10 +369,16 @@ export function getChatMarker (stanza) { ).pop(); } +/** + * @param {Element} stanza + */ export function isHeadline (stanza) { return stanza.getAttribute('type') === 'headline'; } +/** + * @param {Element} stanza + */ export function isServerMessage (stanza) { if (sizzle(`mentions[xmlns="${Strophe.NS.MENTIONS}"]`, stanza).pop()) { return false; @@ -370,10 +396,9 @@ export function isServerMessage (stanza) { /** * Determines whether the passed in stanza is a XEP-0313 MAM stanza - * @private * @method isArchived - * @param { Element } stanza - The message stanza - * @returns { Boolean } + * @param {Element} original_stanza - The message stanza + * @returns {boolean} */ export function isArchived (original_stanza) { return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(); @@ -383,8 +408,8 @@ export function isArchived (original_stanza) { /** * Returns an object containing all attribute names and values for a particular element. * @method getAttributes - * @param { Element } stanza - * @returns { Object } + * @param {Element} stanza + * @returns {object} */ export function getAttributes (stanza) { return stanza.getAttributeNames().reduce((acc, name) => { diff --git a/src/headless/shared/rsm.js b/src/headless/shared/rsm.js index 01e3cd2a5d..467331ccaa 100644 --- a/src/headless/shared/rsm.js +++ b/src/headless/shared/rsm.js @@ -6,7 +6,6 @@ * Some code taken from the Strophe RSM plugin, licensed under the MIT License * Copyright 2006-2017 Strophe (https://github.com/strophe/strophejs) */ -import _converse from './_converse.js'; import { converse } from './api/index.js'; import pick from 'lodash-es/pick'; @@ -16,12 +15,12 @@ Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); /** - * @typedef { Object } RSMQueryParameters + * @typedef {Object} RSMQueryParameters * [XEP-0059 RSM](https://xmpp.org/extensions/xep-0059.html) Attributes that can be used to filter query results - * @property { String } [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging. - * @property { String } [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging. - * @property { number } [index=0] - The index of the results page to return. - * @property { number } [max] - The maximum number of items to return. + * @property {String} [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging. + * @property {String} [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging. + * @property {number} [index=0] - The index of the results page to return. + * @property {number} [max] - The maximum number of items to return. */ const RSM_QUERY_PARAMETERS = ['after', 'before', 'index', 'max']; @@ -103,6 +102,3 @@ export class RSM { return new RSM(options); } } - -_converse.RSM_ATTRIBUTES = RSM_ATTRIBUTES; -_converse.RSM = RSM; diff --git a/src/headless/shared/settings/api.js b/src/headless/shared/settings/api.js index b04af3c35d..af8e0eac57 100644 --- a/src/headless/shared/settings/api.js +++ b/src/headless/shared/settings/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import log from '../../log.js'; import { clearUserSettings, diff --git a/src/headless/shared/settings/constants.js b/src/headless/shared/settings/constants.js index e8738888f2..5933592cbc 100644 --- a/src/headless/shared/settings/constants.js +++ b/src/headless/shared/settings/constants.js @@ -38,17 +38,18 @@ export const DEFAULT_SETTINGS = { assets_path: '/dist', authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external". auto_login: false, // Currently only used in connection with anonymous login - reuse_scram_keys: false, auto_reconnect: true, blacklisted_plugins: [], clear_cache_on_logout: false, connection_options: {}, credentials_url: null, // URL from where login credentials can be fetched + disable_effects: false, // Disabled UI transition effects. Mainly used for tests. discover_connection_methods: true, geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g, geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', i18n: undefined, jid: undefined, + reuse_scram_keys: false, keepalive: true, loglevel: 'info', locales: [ diff --git a/src/headless/shared/settings/utils.js b/src/headless/shared/settings/utils.js index eeff76606d..1a58e06fd1 100644 --- a/src/headless/shared/settings/utils.js +++ b/src/headless/shared/settings/utils.js @@ -1,17 +1,20 @@ +import EventEmitter from '@converse/skeletor/src/eventemitter.js'; import _converse from '../_converse.js'; import isEqual from "lodash-es/isEqual.js"; import log from '../../log.js'; import pick from 'lodash-es/pick'; -import { merge } from '../../utils/object.js'; import { DEFAULT_SETTINGS } from './constants.js'; -import { Events } from '@converse/skeletor/src/events.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { initStorage } from '../../utils/storage.js'; +import { merge } from '../../utils/object.js'; + let app_settings; let init_settings = {}; // Container for settings passed in via converse.initialize let user_settings; // User settings, populated via api.users.settings +class AppSettings extends EventEmitter(Object) {} + export function getAppSettings () { return app_settings; } @@ -19,8 +22,7 @@ export function getAppSettings () { export function initAppSettings (settings) { init_settings = settings; - app_settings = {}; - Object.assign(app_settings, Events); + app_settings = new AppSettings(); // Allow only whitelisted settings to be overwritten via converse.initialize const allowed_settings = pick(settings, Object.keys(DEFAULT_SETTINGS)); @@ -93,21 +95,22 @@ export function updateAppSettings (key, val) { } /** - * @async + * @returns {Promise|void} A promise when the user settings object + * is created anew and it's contents fetched from storage. */ function initUserSettings () { - if (!_converse.bare_jid) { + const bare_jid = _converse.session.get('bare_jid'); + if (!bare_jid) { const msg = "No JID to fetch user settings for"; log.error(msg); throw Error(msg); } - if (!user_settings?.fetched) { - const id = `converse.user-settings.${_converse.bare_jid}`; + const id = `converse.user-settings.${bare_jid}`; + if (user_settings?.get('id') !== id) { user_settings = new Model({id}); initStorage(user_settings, id); - user_settings.fetched = user_settings.fetch({'promise': true}); + return user_settings.fetch({'promise': true}); } - return user_settings.fetched; } export async function getUserSettings () { @@ -121,6 +124,10 @@ export async function updateUserSettings (data, options) { } export async function clearUserSettings () { - await initUserSettings(); - return user_settings.clear(); + const bare_jid = _converse.session.get('bare_jid'); + if (bare_jid) { + await initUserSettings(); + return user_settings.clear(); + } + user_settings = undefined; } diff --git a/src/headless/tests/converse.js b/src/headless/tests/converse.js index e63ed5a09f..e8510714e1 100644 --- a/src/headless/tests/converse.js +++ b/src/headless/tests/converse.js @@ -9,25 +9,23 @@ describe("Converse", function() { it("are sent out when the client becomes or stops being idle", mock.initConverse(['discoInitialized'], {}, (_converse) => { - spyOn(_converse, 'sendCSI').and.callThrough(); - let sent_stanza; - spyOn(_converse.api.connection.get(), 'send').and.callFake(function (stanza) { + let i = 0; + const domain = _converse.session.get('domain'); + _converse.disco_entities.get(domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI + + let sent_stanza = null; + spyOn(_converse.api.connection.get(), 'send').and.callFake((stanza) => { sent_stanza = stanza; }); - let i = 0; - _converse.idle_seconds = 0; // Usually initialized by registerIntervalHandler - _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI _converse.api.settings.set('csi_waiting_time', 3); while (i <= _converse.api.settings.get("csi_waiting_time")) { - expect(_converse.sendCSI).not.toHaveBeenCalled(); - _converse.onEverySecond(); + expect(sent_stanza).toBe(null); + _converse.exports.onEverySecond(); i++; } - expect(_converse.sendCSI).toHaveBeenCalledWith('inactive'); expect(Strophe.serialize(sent_stanza)).toBe(''); _converse.onUserActivity(); - expect(_converse.sendCSI).toHaveBeenCalledWith('active'); expect(Strophe.serialize(sent_stanza)).toBe(''); })); }); @@ -40,8 +38,6 @@ describe("Converse", function() { const { api } = _converse; let i = 0; // Usually initialized by registerIntervalHandler - _converse.idle_seconds = 0; - _converse.auto_changed_status = false; _converse.api.settings.set('auto_away', 3); _converse.api.settings.set('auto_xa', 6); @@ -49,7 +45,6 @@ describe("Converse", function() { while (i <= _converse.api.settings.get("auto_away")) { _converse.onEverySecond(); i++; } - expect(_converse.auto_changed_status).toBe(true); while (i <= api.settings.get('auto_xa')) { expect(await _converse.api.user.status.get()).toBe('away'); @@ -57,11 +52,9 @@ describe("Converse", function() { i++; } expect(await _converse.api.user.status.get()).toBe('xa'); - expect(_converse.auto_changed_status).toBe(true); _converse.onUserActivity(); expect(await _converse.api.user.status.get()).toBe('online'); - expect(_converse.auto_changed_status).toBe(false); // Check that it also works for the chat feature await _converse.api.user.status.set('chat') @@ -70,18 +63,15 @@ describe("Converse", function() { _converse.onEverySecond(); i++; } - expect(_converse.auto_changed_status).toBe(true); while (i <= api.settings.get('auto_xa')) { expect(await _converse.api.user.status.get()).toBe('away'); _converse.onEverySecond(); i++; } expect(await _converse.api.user.status.get()).toBe('xa'); - expect(_converse.auto_changed_status).toBe(true); _converse.onUserActivity(); expect(await _converse.api.user.status.get()).toBe('online'); - expect(_converse.auto_changed_status).toBe(false); // Check that it doesn't work for 'dnd' await _converse.api.user.status.set('dnd'); @@ -91,18 +81,15 @@ describe("Converse", function() { i++; } expect(await _converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); while (i <= api.settings.get('auto_xa')) { expect(await _converse.api.user.status.get()).toBe('dnd'); _converse.onEverySecond(); i++; } expect(await _converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); _converse.onUserActivity(); expect(await _converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); })); }); diff --git a/src/headless/utils/arraybuffer.js b/src/headless/utils/arraybuffer.js index edb5be6b05..5dc9db643a 100644 --- a/src/headless/utils/arraybuffer.js +++ b/src/headless/utils/arraybuffer.js @@ -15,7 +15,7 @@ export function arrayBufferToString (ab) { } export function stringToArrayBuffer (string) { - const bytes = new TextEncoder("utf-8").encode(string); + const bytes = new TextEncoder().encode(string); return bytes.buffer; } diff --git a/src/headless/utils/index.js b/src/headless/utils/index.js index 2850a7f7dd..3baa31298e 100644 --- a/src/headless/utils/index.js +++ b/src/headless/utils/index.js @@ -4,10 +4,10 @@ * @description This is the core utilities module. */ import log, { LEVELS } from '../log.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { toStanza } from 'strophe.js'; import { getOpenPromise } from '@converse/openpromise'; -import { saveWindowState, shouldClearCache } from './session.js'; +import { shouldClearCache } from './session.js'; import { merge, isError, isFunction } from './object.js'; import { createStore, getDefaultStore } from './storage.js'; import { waitUntil } from './promise.js'; @@ -51,6 +51,7 @@ import { /** * The utils object * @namespace u + * @type {Record} */ const u = {}; @@ -226,7 +227,6 @@ export default Object.assign({ queryChildren, replaceCurrentWord, safeSave, - saveWindowState, shouldClearCache, shouldCreateMessage, shouldRenderMediaFromURL, diff --git a/src/headless/utils/init.js b/src/headless/utils/init.js index e2c334e420..319115cc30 100644 --- a/src/headless/utils/init.js +++ b/src/headless/utils/init.js @@ -1,3 +1,6 @@ +/** + * @typedef {module:shared.converse.ConversePrivateGlobal} ConversePrivateGlobal + */ import Storage from '@converse/skeletor/src/storage.js'; import _converse from '../shared/_converse'; import api from '../shared/api/index.js'; @@ -7,14 +10,20 @@ import log from '../log.js'; import syncDriver from 'localforage-webextensionstorage-driver/sync'; import { ANONYMOUS, CORE_PLUGINS, EXTERNAL, LOGIN } from '../shared/constants.js'; import { Connection } from '../shared/connection/index.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { Strophe } from 'strophe.js'; import { createStore, initStorage } from './storage.js'; import { getConnectionServiceURL } from '../shared/connection/utils'; import { isValidJID } from './jid.js'; -import { saveWindowState, isTestEnv } from './session.js'; +import { getUnloadEvent, isTestEnv } from './session.js'; +/** + * Initializes the plugins for the Converse instance. + * @param {ConversePrivateGlobal} _converse + * @fires _converse#pluginsInitialized - Triggered once all plugins have been initialized. + * @memberOf _converse + */ export function initPlugins (_converse) { // If initialize gets called a second time (e.g. during tests), then we // need to re-apply all plugins (for a new converse instance), and we @@ -58,6 +67,9 @@ export function initPlugins (_converse) { } +/** + * @param {ConversePrivateGlobal} _converse + */ export async function initClientConfig (_converse) { /* The client config refers to configuration of the client which is * independent of any particular user. @@ -65,9 +77,13 @@ export async function initClientConfig (_converse) { * user sessions. */ const id = 'converse.client-config'; - _converse.config = new Model({ id, 'trusted': true }); - _converse.config.browserStorage = createStore(id, "session"); - await new Promise(r => _converse.config.fetch({'success': r, 'error': r})); + const config = new Model({ id, 'trusted': true }); + config.browserStorage = createStore(id, "session"); + + Object.assign(_converse, { config }); // XXX DEPRECATED + Object.assign(_converse.state, { config }); + + await new Promise(r => config.fetch({'success': r, 'error': r})); /** * Triggered once the XMPP-client configuration has been initialized. * The client configuration is independent of any particular and its values @@ -81,18 +97,24 @@ export async function initClientConfig (_converse) { } +/** + * @param {ConversePrivateGlobal} _converse + */ export async function initSessionStorage (_converse) { await Storage.sessionStorageInitialized; - _converse.storage = { - 'session': Storage.localForage.createInstance({ - 'name': isTestEnv() ? 'converse-test-session' : 'converse-session', - 'description': 'sessionStorage instance', - 'driver': ['sessionStorageWrapper'] - }) - }; + _converse.storage['session'] = Storage.localForage.createInstance({ + 'name': isTestEnv() ? 'converse-test-session' : 'converse-session', + 'description': 'sessionStorage instance', + 'driver': ['sessionStorageWrapper'] + }); } +/** + * Initializes persistent storage + * @param {ConversePrivateGlobal} _converse + * @param {string} store_name - The name of the store. + */ function initPersistentStorage (_converse, store_name) { if (_converse.api.settings.get('persistent_store') === 'sessionStorage') { return; @@ -126,20 +148,24 @@ function initPersistentStorage (_converse, store_name) { } +/** + * @param {ConversePrivateGlobal} _converse + * @param {string} jid + */ function saveJIDtoSession (_converse, jid) { jid = _converse.session.get('jid') || jid; if (_converse.api.settings.get("authentication") !== ANONYMOUS && !Strophe.getResourceFromJid(jid)) { jid = jid.toLowerCase() + Connection.generateResource(); } - _converse.jid = jid; - _converse.bare_jid = Strophe.getBareJidFromJid(jid); - _converse.resource = Strophe.getResourceFromJid(jid); - _converse.domain = Strophe.getDomainFromJid(jid); - _converse.session.save({ - 'jid': jid, - 'bare_jid': _converse.bare_jid, - 'resource': _converse.resource, - 'domain': _converse.domain, + + const bare_jid = Strophe.getBareJidFromJid(jid); + const resource = Strophe.getResourceFromJid(jid); + const domain = Strophe.getDomainFromJid(jid); + + // TODO: Storing directly on _converse is deprecated + Object.assign(_converse, { jid, bare_jid, resource, domain }); + + _converse.session.save({ jid, bare_jid, resource, domain, // We use the `active` flag to determine whether we should use the values from sessionStorage. // When "cloning" a tab (e.g. via middle-click), the `active` flag will be set and we'll create // a new empty user session, otherwise it'll be false and we can re-use the user session. @@ -161,7 +187,7 @@ function saveJIDtoSession (_converse, jid) { * connection is set up. * * @emits _converse#setUserJID - * @params { String } jid + * @param {string} jid */ export async function setUserJID (jid) { await initSession(_converse, jid); @@ -175,6 +201,10 @@ export async function setUserJID (jid) { } +/** + * @param {ConversePrivateGlobal} _converse + * @param {string} jid + */ export async function initSession (_converse, jid) { const is_shared_session = _converse.api.settings.get('connection_options').worker; @@ -183,7 +213,7 @@ export async function initSession (_converse, jid) { if (_converse.session?.get('id') !== id) { initPersistentStorage(_converse, bare_jid); - _converse.session = new Model({ id }); + _converse.session.set({ id }); initStorage(_converse.session, id, is_shared_session ? "persistent" : "session"); await new Promise(r => _converse.session.fetch({'success': r, 'error': r})); @@ -196,7 +226,7 @@ export async function initSession (_converse, jid) { saveJIDtoSession(_converse, jid); // Set `active` flag to false when the tab gets reloaded - window.addEventListener(_converse.unloadevent, () => _converse.session?.save('active', false)); + window.addEventListener(getUnloadEvent(), () => _converse.session?.save('active', false)); /** * Triggered once the user's session has been initialized. The session is a @@ -211,13 +241,12 @@ export async function initSession (_converse, jid) { } +/** + * @param {ConversePrivateGlobal} _converse + */ export function registerGlobalEventHandlers (_converse) { - document.addEventListener("visibilitychange", saveWindowState); - saveWindowState({'type': document.hidden ? "blur" : "focus"}); // Set initial state /** - * Called once Converse has registered its global event handlers - * (for events such as window resize or unload). - * Plugins can listen to this event as cue to register their own + * Plugins can listen to this event as cue to register their * global event handlers. * @event _converse#registeredGlobalEventHandlers * @example _converse.api.listen.on('registeredGlobalEventHandlers', () => { ... }); @@ -226,15 +255,19 @@ export function registerGlobalEventHandlers (_converse) { } +/** + * @param {ConversePrivateGlobal} _converse + */ function unregisterGlobalEventHandlers (_converse) { - const { api } = _converse; - document.removeEventListener("visibilitychange", saveWindowState); - api.trigger('unregisteredGlobalEventHandlers'); + _converse.api.trigger('unregisteredGlobalEventHandlers'); } -// Make sure everything is reset in case this is a subsequent call to -// converse.initialize (happens during tests). +/** + * Make sure everything is reset in case this is a subsequent call to + * converse.initialize (happens during tests). + * @param {ConversePrivateGlobal} _converse + */ export async function cleanup (_converse) { const { api } = _converse; await api.trigger('cleanup', {'synchronous': true}); @@ -248,6 +281,14 @@ export async function cleanup (_converse) { } +/** + * Fetches login credentials from the server. + * @param {number} [wait=0] + * The time to wait and debounce subsequent calls to this function before making the request. + * @returns {Promise<{jid: string, password: string}>} + * A promise that resolves with the provided login credentials (JID and password). + * @throws {Error} If the request fails or returns an error status. + */ function fetchLoginCredentials (wait=0) { return new Promise( debounce(async (resolve, reject) => { @@ -333,6 +374,7 @@ export async function attemptNonPreboundSession (credentials, automatic) { const { api } = _converse; if (api.settings.get("authentication") === LOGIN) { + const jid = _converse.session.get('jid'); // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and // ``authentication`` is set to ``login``, then Converse will try to log the user in, // since we don't have a way to distinguish between wether we're @@ -345,7 +387,7 @@ export async function attemptNonPreboundSession (credentials, automatic) { // We give credentials_url preference, because // connection.pass might be an expired token. return connect(await getLoginCredentialsFromURL()); - } else if (_converse.jid && (api.settings.get("password") || api.connection.get().pass)) { + } else if (jid && (api.settings.get("password") || api.connection.get().pass)) { return connect(); } @@ -398,9 +440,10 @@ export async function savedLoginInfo (jid) { */ async function connect (credentials) { const { api } = _converse; + const jid = _converse.session.get('jid'); const connection = api.connection.get(); if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) { - if (!_converse.jid) { + if (!jid) { throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + "Either when calling converse.initialize, or when calling " + @@ -409,7 +452,7 @@ async function connect (credentials) { if (!connection.reconnecting) { connection.reset(); } - connection.connect(_converse.jid.toLowerCase()); + connection.connect(jid.toLowerCase()); } else if (api.settings.get("authentication") === LOGIN) { const password = credentials?.password ?? (connection?.pass || api.settings.get("password")); if (!password) { @@ -427,24 +470,25 @@ async function connect (credentials) { } let callback; - // Save the SCRAM data if we're not already logged in with SCRAM if ( - _converse.config.get('trusted') && - _converse.jid && + _converse.state.config.get('trusted') && + jid && api.settings.get("reuse_scram_keys") && !password?.ck ) { // Store scram keys in scram storage - const login_info = await savedLoginInfo(_converse.jid); - - callback = (status) => { - const { scram_keys } = connection; - if (scram_keys) login_info.save({ scram_keys }); - connection.onConnectStatusChanged(status); - }; + const login_info = await savedLoginInfo(jid); + + callback = + /** @param {string} status */ + (status) => { + const { scram_keys } = connection; + if (scram_keys) login_info.save({ scram_keys }); + connection.onConnectStatusChanged(status); + }; } - connection.connect(_converse.jid, password, callback); + connection.connect(jid, password, callback); } } diff --git a/src/headless/utils/jid.js b/src/headless/utils/jid.js index 0c4afe28c4..8989fe5e4a 100644 --- a/src/headless/utils/jid.js +++ b/src/headless/utils/jid.js @@ -25,6 +25,9 @@ export function isSameDomain (jid1, jid2) { return Strophe.getDomainFromJid(jid1).toLowerCase() === Strophe.getDomainFromJid(jid2).toLowerCase(); } +/** + * @param {string} jid + */ export function getJIDFromURI (jid) { return jid.startsWith('xmpp:') && jid.endsWith('?join') ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '') diff --git a/src/headless/utils/session.js b/src/headless/utils/session.js index 827d07a1dd..5a0edb7bf6 100644 --- a/src/headless/utils/session.js +++ b/src/headless/utils/session.js @@ -18,49 +18,17 @@ export function isTestEnv () { return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind'; } -export function saveWindowState (ev) { - // XXX: eventually we should be able to just use - // document.visibilityState (when we drop support for older - // browsers). - let state; - const event_map = { - 'focus': "visible", - 'focusin': "visible", - 'pageshow': "visible", - 'blur': "hidden", - 'focusout': "hidden", - 'pagehide': "hidden" - }; - ev = ev || document.createEvent('Events'); - if (ev.type in event_map) { - state = event_map[ev.type]; - } else { - state = document.hidden ? "hidden" : "visible"; - } - _converse.windowState = state; - /** - * Triggered when window state has changed. - * Used to determine when a user left the page and when came back. - * @event _converse#windowStateChanged - * @type { object } - * @property{ string } state - Either "hidden" or "visible" - * @example _converse.api.listen.on('windowStateChanged', obj => { ... }); - */ - _converse.api.trigger('windowStateChanged', {state}); -} - -export function setUnloadEvent () { +export function getUnloadEvent () { if ('onpagehide' in window) { // Pagehide gets thrown in more cases than unload. Specifically it // gets thrown when the page is cached and not just // closed/destroyed. It's the only viable event on mobile Safari. // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ - _converse.unloadevent = 'pagehide'; + return 'pagehide'; } else if ('onbeforeunload' in window) { - _converse.unloadevent = 'beforeunload'; - } else if ('onunload' in window) { - _converse.unloadevent = 'unload'; + return 'beforeunload'; } + return 'unload'; } export function replacePromise (name) { @@ -79,7 +47,7 @@ export function replacePromise (name) { export function shouldClearCache () { const { api } = _converse; - return !_converse.config.get('trusted') || + return !_converse.state.config.get('trusted') || api.settings.get('clear_cache_on_logout') || isTestEnv(); } @@ -88,21 +56,14 @@ export function shouldClearCache () { export async function tearDown () { const { api } = _converse; await api.trigger('beforeTearDown', {'synchronous': true}); - window.removeEventListener('click', _converse.onUserActivity); - window.removeEventListener('focus', _converse.onUserActivity); - window.removeEventListener('keypress', _converse.onUserActivity); - window.removeEventListener('mousemove', _converse.onUserActivity); - window.removeEventListener(_converse.unloadevent, _converse.onUserActivity); - window.clearInterval(_converse.everySecondTrigger); api.trigger('afterTearDown'); return _converse; } export function clearSession () { - _converse.session?.destroy(); - delete _converse.session; shouldClearCache() && _converse.api.user.settings.clear(); + _converse.initSession(); /** * Synchronouse event triggered once the user session has been cleared, * for example when the user has logged out or when Converse has diff --git a/src/headless/utils/storage.js b/src/headless/utils/storage.js index 69b2f27554..0a0de09b6c 100644 --- a/src/headless/utils/storage.js +++ b/src/headless/utils/storage.js @@ -1,9 +1,10 @@ import Storage from '@converse/skeletor/src/storage.js'; import _converse from '../shared/_converse.js'; import { settings_api } from '../shared/settings/api.js'; +import { getUnloadEvent } from './session.js'; export function getDefaultStore () { - if (_converse.config.get('trusted')) { + if (_converse.state.config.get('trusted')) { const is_non_persistent = settings_api.get('persistent_store') === 'sessionStorage'; return is_non_persistent ? 'session': 'persistent'; } else { @@ -29,8 +30,9 @@ export function initStorage (model, id, type) { model.browserStorage = createStore(id, store); if (storeUsesIndexedDB(store)) { const flush = () => model.browserStorage.flush(); - window.addEventListener(_converse.unloadevent, flush); - model.on('destroy', () => window.removeEventListener(_converse.unloadevent, flush)); + const unloadevent = getUnloadEvent(); + window.addEventListener(unloadevent, flush); + model.on('destroy', () => window.removeEventListener(unloadevent, flush)); model.listenTo(_converse, 'beforeLogout', flush); } } diff --git a/src/headless/utils/url.js b/src/headless/utils/url.js index d532454bb9..17e3f4720c 100644 --- a/src/headless/utils/url.js +++ b/src/headless/utils/url.js @@ -1,14 +1,31 @@ +/** + * @typedef {module:headless-shared-chat-utils.MediaURLData} MediaURLData + */ import URI from 'urijs'; import log from '../log.js'; import api from '../shared/api/index.js'; +/** + * Will return false if URL is malformed or contains disallowed characters + * @param {string} text + * @returns {boolean} + */ +export function isValidURL (text) { + try { + return !!(new URL(text)); + } catch (error) { + log.error(error); + return false; + } +} + /** * Given a url, check whether the protocol being used is allowed for rendering * the media in the chat (as opposed to just rendering a URL hyperlink). * @param {string} url * @returns {boolean} */ -export function isAllowedProtocolForMedia(url) { +export function isAllowedProtocolForMedia (url) { const uri = getURI(url); const { protocol } = window.location; if (['chrome-extension:','file:'].includes(protocol)) { @@ -20,6 +37,9 @@ export function isAllowedProtocolForMedia(url) { ); } +/** + * @param {string|URI} url + */ export function getURI (url) { try { return url instanceof URI ? url : new URI(url); @@ -90,9 +110,9 @@ export function isDomainAllowed (url, setting) { } /** - * Accepts a {@link MediaURL} object and then checks whether its domain is + * Accepts a {@link MediaURLData} object and then checks whether its domain is * allowed for rendering in the chat. - * @param {MediaURL} o + * @param {MediaURLData} o * @returns {boolean} */ export function isMediaURLDomainAllowed (o) { diff --git a/src/index.js b/src/index.js index aa38ce41a3..3e7fae1492 100644 --- a/src/index.js +++ b/src/index.js @@ -38,8 +38,7 @@ import "./plugins/dragresize/index.js"; // Allows chat boxes to be resized b import "./plugins/fullscreen/index.js"; /* END: Removable components */ - -_converse.CustomElement = CustomElement; +_converse.exports.CustomElement = CustomElement; const initialize = converse.initialize; diff --git a/src/plugins/adhoc-views/adhoc-commands.js b/src/plugins/adhoc-views/adhoc-commands.js index 1bf44ddb79..cf3abb84d2 100644 --- a/src/plugins/adhoc-views/adhoc-commands.js +++ b/src/plugins/adhoc-views/adhoc-commands.js @@ -32,15 +32,24 @@ export default class AdHocCommands extends CustomElement { return tplAdhoc(this) } + /** + * @param {SubmitEvent} ev + */ async fetchCommands (ev) { ev.preventDefault(); - delete this.alert_type; - delete this.alert; + + if (!(ev.target instanceof HTMLFormElement)) { + this.alert_type = 'danger'; + this.alert = 'Form could not be submitted'; + return; + } this.fetching = true; + delete this.alert_type; + delete this.alert; const form_data = new FormData(ev.target); - const jid = form_data.get('jid').trim(); + const jid = /** @type {string} */(form_data.get('jid')).trim(); let supported; try { supported = await api.disco.supports(Strophe.NS.ADHOC, jid); @@ -109,8 +118,8 @@ export default class AdHocCommands extends CustomElement { async runCommand (form, action) { const form_data = new FormData(form); - const jid = form_data.get('command_jid').trim(); - const node = form_data.get('command_node').trim(); + const jid = /** @type {string} */(form_data.get('command_jid')).trim(); + const node = /** @type {string} */(form_data.get('command_node')).trim(); const cmd = this.commands.filter(c => c.node === node)[0]; delete cmd.alert; @@ -159,8 +168,8 @@ export default class AdHocCommands extends CustomElement { this.requestUpdate(); const form_data = new FormData(ev.target.form); - const jid = form_data.get('command_jid').trim(); - const node = form_data.get('command_node').trim(); + const jid = /** @type {string} */(form_data.get('command_jid')).trim(); + const node = /** @type {string} */(form_data.get('command_node')).trim(); const cmd = this.commands.filter(c => c.node === node)[0]; delete cmd.alert; diff --git a/src/plugins/bookmark-views/components/bookmark-form.js b/src/plugins/bookmark-views/components/bookmark-form.js index e79e4dbc28..5b63d0fa15 100644 --- a/src/plugins/bookmark-views/components/bookmark-form.js +++ b/src/plugins/bookmark-views/components/bookmark-form.js @@ -5,6 +5,11 @@ import { _converse, api } from "@converse/headless"; class MUCBookmarkForm extends CustomElement { + constructor () { + super(); + this.jid = null; + } + static get properties () { return { 'jid': { type: String } diff --git a/src/plugins/bookmark-views/components/bookmarks-list.js b/src/plugins/bookmark-views/components/bookmarks-list.js index 804312140f..bf5aa64a1e 100644 --- a/src/plugins/bookmark-views/components/bookmarks-list.js +++ b/src/plugins/bookmark-views/components/bookmarks-list.js @@ -2,7 +2,7 @@ import debounce from "lodash-es/debounce"; import tplBookmarksList from './templates/list.js'; import tplSpinner from "templates/spinner.js"; import { CustomElement } from 'shared/components/element.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { _converse, api } from '@converse/headless'; import { initStorage } from '@converse/headless/utils/storage.js'; diff --git a/src/plugins/bookmark-views/mixins.js b/src/plugins/bookmark-views/mixins.js index 12a00bb257..5e39bb3675 100644 --- a/src/plugins/bookmark-views/mixins.js +++ b/src/plugins/bookmark-views/mixins.js @@ -8,8 +8,9 @@ export const bookmarkableChatRoomView = { * @private */ setBookmarkState () { - if (_converse.bookmarks !== undefined) { - const models = _converse.bookmarks.where({ 'jid': this.model.get('jid') }); + const { bookmarks } = _converse.state; + if (bookmarks) { + const models = bookmarks.where({ 'jid': this.model.get('jid') }); if (!models.length) { this.model.save('bookmarked', false); } else { diff --git a/src/plugins/bookmark-views/modals/bookmark-form.js b/src/plugins/bookmark-views/modals/bookmark-form.js index 65df95c341..15319b17fc 100644 --- a/src/plugins/bookmark-views/modals/bookmark-form.js +++ b/src/plugins/bookmark-views/modals/bookmark-form.js @@ -6,6 +6,11 @@ import { api } from "@converse/headless"; export default class BookmarkFormModal extends BaseModal { + constructor (options) { + super(options); + this.jid = null; + } + renderModal () { return html` diff --git a/src/plugins/bookmark-views/utils.js b/src/plugins/bookmark-views/utils.js index 08091c9d19..97b887e9cf 100644 --- a/src/plugins/bookmark-views/utils.js +++ b/src/plugins/bookmark-views/utils.js @@ -1,10 +1,11 @@ import { __ } from 'i18n'; import { _converse, api, converse } from '@converse/headless'; import { checkBookmarksSupport } from '@converse/headless/plugins/bookmarks/utils'; +import { CHATROOMS_TYPE } from '@converse/headless/shared/constants'; export function getHeadingButtons (view, buttons) { - if (api.settings.get('allow_bookmarks') && view.model.get('type') === _converse.CHATROOMS_TYPE) { + if (api.settings.get('allow_bookmarks') && view.model.get('type') === CHATROOMS_TYPE) { const data = { 'i18n_title': __('Bookmark this groupchat'), 'i18n_text': __('Bookmark'), diff --git a/src/plugins/chatboxviews/index.js b/src/plugins/chatboxviews/index.js index edd2ec0ad7..4bd2f82d25 100644 --- a/src/plugins/chatboxviews/index.js +++ b/src/plugins/chatboxviews/index.js @@ -24,7 +24,9 @@ converse.plugins.add('converse-chatboxviews', { // configuration settings. api.settings.extend({ 'animate': true }); - _converse.chatboxviews = new ChatBoxViews(); + const chatboxviews = new ChatBoxViews(); + Object.assign(_converse, { chatboxviews }); // XXX DEPRECATED + Object.assign(_converse.state, { chatboxviews }); /************************ BEGIN Event Handlers ************************/ api.listen.on('chatBoxesInitialized', () => { diff --git a/src/plugins/chatboxviews/templates/chats.js b/src/plugins/chatboxviews/templates/chats.js index a61bd12649..ee94fa5353 100644 --- a/src/plugins/chatboxviews/templates/chats.js +++ b/src/plugins/chatboxviews/templates/chats.js @@ -1,17 +1,17 @@ import { html } from 'lit'; import { repeat } from 'lit/directives/repeat.js'; import { _converse, api } from '@converse/headless'; +import { CONTROLBOX_TYPE, CHATROOMS_TYPE, HEADLINES_TYPE } from '@converse/headless/shared/constants'; function shouldShowChat (c) { - const { CONTROLBOX_TYPE } = _converse; const is_minimized = (api.settings.get('view_mode') === 'overlayed' && c.get('minimized')); return c.get('type') === CONTROLBOX_TYPE || !(c.get('hidden') || is_minimized); } export default () => { - const { chatboxes, CONTROLBOX_TYPE, CHATROOMS_TYPE, HEADLINES_TYPE } = _converse; + const { chatboxes } = _converse; const view_mode = api.settings.get('view_mode'); const connection = api.connection.get(); const logged_out = !connection?.connected || !connection?.authenticated || connection?.disconnecting; diff --git a/src/plugins/chatview/bottom-panel.js b/src/plugins/chatview/bottom-panel.js index 0fd0e5cfb7..34ef857139 100644 --- a/src/plugins/chatview/bottom-panel.js +++ b/src/plugins/chatview/bottom-panel.js @@ -1,3 +1,8 @@ +/** + * @typedef {import('shared/chat/emoji-picker.js').default} EmojiPicker + * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown + * @typedef {import('./message-form.js').default} MessageForm + */ import './message-form.js'; import tplBottomPanel from './templates/bottom-panel.js'; import { CustomElement } from 'shared/components/element.js'; @@ -38,7 +43,8 @@ export default class ChatBottomPanel extends CustomElement { sendButtonClicked (ev) { if (ev.delegateTarget?.dataset.action === 'sendMessage') { - this.querySelector('converse-message-form')?.onFormSubmitted(ev); + const form = /** @type {MessageForm} */(this.querySelector('converse-message-form')); + form?.onFormSubmitted(ev); } } @@ -55,16 +61,6 @@ export default class ChatBottomPanel extends CustomElement { _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev); } - onDrop (evt) { - if (evt.dataTransfer.files.length == 0) { - // There are no files to be dropped, so this isn’t a file - // transfer operation. - return; - } - evt.preventDefault(); - this.model.sendFiles(evt.dataTransfer.files); - } - onDragOver (ev) { // eslint-disable-line class-methods-use-this ev.preventDefault(); } @@ -76,14 +72,14 @@ export default class ChatBottomPanel extends CustomElement { async autocompleteInPicker (input, value) { await api.emojis.initialize(); - const emoji_picker = this.querySelector('converse-emoji-picker'); + const emoji_picker = /** @type {EmojiPicker} */(this.querySelector('converse-emoji-picker')); if (emoji_picker) { emoji_picker.model.set({ 'ac_position': input.selectionStart, 'autocompleting': value, 'query': value }); - const emoji_dropdown = this.querySelector('converse-emoji-dropdown'); + const emoji_dropdown = /** @type {EmojiDropdown} */(this.querySelector('converse-emoji-dropdown')); emoji_dropdown?.showMenu(); } } diff --git a/src/plugins/chatview/chat.js b/src/plugins/chatview/chat.js index 2b1ae0e529..b929fe5edb 100644 --- a/src/plugins/chatview/chat.js +++ b/src/plugins/chatview/chat.js @@ -8,7 +8,7 @@ import { _converse, api } from '@converse/headless'; /** * The view of an open/ongoing chat conversation. * @class - * @namespace _converse.ChatBoxView + * @namespace _converse.ChatView * @memberOf _converse */ export default class ChatView extends BaseChatView { @@ -17,10 +17,11 @@ export default class ChatView extends BaseChatView { async initialize () { _converse.chatboxviews.add(this.jid, this); this.model = _converse.chatboxes.get(this.jid); - this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged); this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown()); this.listenTo(this.model, 'change:show_help_messages', () => this.requestUpdate()); + document.addEventListener('visibilitychange', () => this.onWindowStateChanged()); + await this.model.messages.fetched; !this.model.get('hidden') && this.afterShown() /** diff --git a/src/plugins/chatview/heading.js b/src/plugins/chatview/heading.js index ccf57b911b..518c80dddb 100644 --- a/src/plugins/chatview/heading.js +++ b/src/plugins/chatview/heading.js @@ -1,3 +1,17 @@ +/** + * @typedef { Object } HeadingButtonAttributes + * An object representing a chat heading button + * @property { Boolean } standalone + * True if shown on its own, false if it must be in the dropdown menu. + * @property { Function } handler + * A handler function to be called when the button is clicked. + * @property { String } a_class - HTML classes to show on the button + * @property { String } i18n_text - The user-visiible name of the button + * @property { String } i18n_title - The tooltip text for this button + * @property { String } icon_class - What kind of CSS class to use for the icon + * @property { String } name - The internal name of the button + */ + import 'shared/modals/user-details.js'; import tplChatboxHead from './templates/chat-head.js'; import { CustomElement } from 'shared/components/element.js'; @@ -9,6 +23,11 @@ import './styles/chat-head.scss'; export default class ChatHeading extends CustomElement { + constructor () { + super(); + this.jid = null; + } + static get properties () { return { 'jid': { type: String }, @@ -54,19 +73,7 @@ export default class ChatHeading extends CustomElement { */ getHeadingButtons () { const buttons = [ - /** - * @typedef { Object } HeadingButtonAttributes - * An object representing a chat heading button - * @property { Boolean } standalone - * True if shown on its own, false if it must be in the dropdown menu. - * @property { Function } handler - * A handler function to be called when the button is clicked. - * @property { String } a_class - HTML classes to show on the button - * @property { String } i18n_text - The user-visiible name of the button - * @property { String } i18n_title - The tooltip text for this button - * @property { String } icon_class - What kind of CSS class to use for the icon - * @property { String } name - The internal name of the button - */ + /** @type {HeadingButtonAttributes} */ { 'a_class': 'show-user-details-modal', 'handler': ev => this.showUserDetailsModal(ev), diff --git a/src/plugins/chatview/message-form.js b/src/plugins/chatview/message-form.js index 2616c96bc0..b10e88387f 100644 --- a/src/plugins/chatview/message-form.js +++ b/src/plugins/chatview/message-form.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown + */ import tplMessageForm from './templates/message-form.js'; import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; @@ -16,7 +19,7 @@ export default class MessageForm extends CustomElement { this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting); this.listenTo(this.model, 'change:composing_spoiler', () => this.requestUpdate()); - this.handleEmojiSelection = ({ detail }) => { + this.handleEmojiSelection = (/** @type { CustomEvent } */{ detail }) => { if (this.model.get('jid') === detail.jid) { this.insertIntoTextArea(detail.value, detail.autocompleting, false, detail.ac_position); } @@ -34,13 +37,12 @@ export default class MessageForm extends CustomElement { return tplMessageForm( Object.assign(this.model.toJSON(), { 'onDrop': ev => this.onDrop(ev), - 'hint_value': this.querySelector('.spoiler-hint')?.value, - 'message_value': this.querySelector('.chat-textarea')?.value, + 'hint_value': /** @type {HTMLInputElement} */(this.querySelector('.spoiler-hint'))?.value, + 'message_value': /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'))?.value, 'onChange': ev => this.model.set({'draft': ev.target.value}), 'onKeyDown': ev => this.onKeyDown(ev), 'onKeyUp': ev => this.onKeyUp(ev), - 'onPaste': ev => this.onPaste(ev), - 'viewUnreadMessages': ev => this.viewUnreadMessages(ev) + 'onPaste': ev => this.onPaste(ev) }) ); } @@ -56,7 +58,7 @@ export default class MessageForm extends CustomElement { * replaced with the new value. */ insertIntoTextArea (value, replace = false, correcting = false, position) { - const textarea = this.querySelector('.chat-textarea'); + const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea')); if (correcting) { u.addClass('correcting', textarea); } else { @@ -120,6 +122,16 @@ export default class MessageForm extends CustomElement { this.model.set({'draft': ev.clipboardData.getData('text/plain')}); } + onDrop (evt) { + if (evt.dataTransfer.files.length == 0) { + // There are no files to be dropped, so this isn’t a file + // transfer operation. + return; + } + evt.preventDefault(); + this.model.sendFiles(evt.dataTransfer.files); + } + onKeyUp (ev) { this.model.set({'draft': ev.target.value}); } @@ -141,11 +153,11 @@ export default class MessageForm extends CustomElement { // Forward slash is used to run commands. Nothing to do here. return; } else if (ev.keyCode === converse.keycodes.ESCAPE) { - return this.onEscapePressed(ev, this); + return this.onEscapePressed(ev); } else if (ev.keyCode === converse.keycodes.ENTER) { return this.onFormSubmitted(ev); } else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) { - const textarea = this.querySelector('.chat-textarea'); + const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea')); if (!textarea.value || u.hasClass('correcting', textarea)) { return this.model.editEarlierMessage(); } @@ -178,7 +190,7 @@ export default class MessageForm extends CustomElement { async onFormSubmitted (ev) { ev?.preventDefault?.(); - const textarea = this.querySelector('.chat-textarea'); + const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea')); const message_text = textarea.value.trim(); if ( (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) || @@ -195,12 +207,12 @@ export default class MessageForm extends CustomElement { let spoiler_hint, hint_el = {}; if (this.model.get('composing_spoiler')) { - hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint'); + hint_el = /** @type {HTMLInputElement} */(this.querySelector('form.sendXMPPMessage input.spoiler-hint')); spoiler_hint = hint_el.value; } u.addClass('disabled', textarea); textarea.setAttribute('disabled', 'disabled'); - this.querySelector('converse-emoji-dropdown')?.hideMenu(); + /** @type {EmojiDropdown} */(this.querySelector('converse-emoji-dropdown'))?.hideMenu(); const is_command = await parseMessageForCommands(this.model, message_text); const message = is_command ? null : await this.model.sendMessage({'body': message_text, spoiler_hint}); diff --git a/src/plugins/chatview/styles/chat-bottom-panel.scss b/src/plugins/chatview/styles/chat-bottom-panel.scss index 9aeba39923..2b675eaa34 100644 --- a/src/plugins/chatview/styles/chat-bottom-panel.scss +++ b/src/plugins/chatview/styles/chat-bottom-panel.scss @@ -40,8 +40,9 @@ } .chat-textarea, input { + outline: 1px solid var(--chat-head-color); &:active, &:focus{ - outline-color: var(--chat-head-color); + outline-width: 2px; } &.correcting { background-color: var(--chat-correcting-color); diff --git a/src/plugins/chatview/templates/chat-head.js b/src/plugins/chatview/templates/chat-head.js index 778d21bbba..a429aea055 100644 --- a/src/plugins/chatview/templates/chat-head.js +++ b/src/plugins/chatview/templates/chat-head.js @@ -3,6 +3,7 @@ import { _converse } from '@converse/headless'; import { getStandaloneButtons, getDropdownButtons } from 'shared/chat/utils.js'; import { html } from "lit"; import { until } from 'lit/directives/until.js'; +import { HEADLINES_TYPE } from '@converse/headless/shared/constants.js'; export default (o) => { @@ -19,9 +20,9 @@ export default (o) => {
${ (!_converse.api.settings.get("singleton")) ? html`` : '' } - ${ (o.type !== _converse.HEADLINES_TYPE) ? html`${ avatar }` : '' } + ${ (o.type !== HEADLINES_TYPE) ? html`${ avatar }` : '' }
- ${ (o.type !== _converse.HEADLINES_TYPE) ? html`${ display_name }` : display_name } + ${ (o.type !== HEADLINES_TYPE) ? html`${ display_name }` : display_name }
diff --git a/src/plugins/chatview/templates/chat.js b/src/plugins/chatview/templates/chat.js index 3d0eed8099..ece6fffec6 100644 --- a/src/plugins/chatview/templates/chat.js +++ b/src/plugins/chatview/templates/chat.js @@ -1,5 +1,6 @@ import { html } from "lit"; import { _converse } from '@converse/headless'; +import { CHATROOMS_TYPE } from "@converse/headless/shared/constants"; export default (o) => html`
@@ -18,7 +19,7 @@ export default (o) => html` .messages=${o.help_messages} ?hidden=${!o.show_help_messages} type="info" - chat_type="${_converse.CHATROOMS_TYPE}" + chat_type="${CHATROOMS_TYPE}" >
` : '' }
diff --git a/src/plugins/chatview/tests/messages.js b/src/plugins/chatview/tests/messages.js index b7799ad725..936c83fa23 100644 --- a/src/plugins/chatview/tests/messages.js +++ b/src/plugins/chatview/tests/messages.js @@ -17,7 +17,7 @@ describe("A Chat Message", function () { await u.waitUntil(() => view.querySelector('converse-chat-message .chat-msg__text')?.textContent === 'This message will be read'); expect(view.model.get('num_unread')).toBe(0); - _converse.windowState = 'hidden'; + spyOn(view.model, 'isHidden').and.returnValue(true); await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be new')); await u.waitUntil(() => view.model.messages.length); diff --git a/src/plugins/chatview/tests/receipts.js b/src/plugins/chatview/tests/receipts.js index c3f41f1747..5ab02f1165 100644 --- a/src/plugins/chatview/tests/receipts.js +++ b/src/plugins/chatview/tests/receipts.js @@ -130,7 +130,7 @@ describe("A delivery receipt", function () { await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 1); // Also handle receipts with type 'chat'. See #1353 - spyOn(_converse, 'handleMessageStanza').and.callThrough(); + spyOn(_converse.exports, 'handleMessageStanza').and.callThrough(); textarea.value = 'Another message'; message_form.onKeyDown({ target: textarea, @@ -149,6 +149,6 @@ describe("A delivery receipt", function () { }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); api.connection.get()._dataRecv(mock.createRequest(msg)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2); - expect(_converse.handleMessageStanza.calls.count()).toBe(1); + expect(_converse.exports.handleMessageStanza.calls.count()).toBe(1); })); }); diff --git a/src/plugins/chatview/tests/styling.js b/src/plugins/chatview/tests/styling.js index a37fd0f75a..dfd6c114ae 100644 --- a/src/plugins/chatview/tests/styling.js +++ b/src/plugins/chatview/tests/styling.js @@ -173,8 +173,23 @@ describe("An incoming chat Message", function () { await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); expect(msg_el.innerText).toBe(msg_text); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === - 'Go to ~https://conversejs.org~now _please_'); + + // Chrome < 119 thinks this is not a valid URL while Chrome 119 does. + let valid_url = false; + try { + valid_url = !!(new URL('https://conversejs.org~now')); + } catch (e) { + valid_url = false; + } + + if (valid_url) { + await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === + 'Go to ~https://conversejs.org~now '+ + '_please_', 1000); + } else { + await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === + 'Go to ~https://conversejs.org~now _please_'); + } msg_text = `Go to _https://converse_js.org_ _please_`; msg = mock.createChatMessage(_converse, contact_jid, msg_text) diff --git a/src/plugins/chatview/tests/unreads.js b/src/plugins/chatview/tests/unreads.js index eae11c62eb..2a8f723bf6 100644 --- a/src/plugins/chatview/tests/unreads.js +++ b/src/plugins/chatview/tests/unreads.js @@ -56,7 +56,7 @@ describe("A ChatBox's Unread Message Count", function () { const sent_stanzas = []; spyOn(api.connection.get(), 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); - _converse.windowState = 'hidden'; + spyOn(chatbox, 'isHidden').and.returnValue(true); const msg = msgFactory(); _converse.handleMessageStanza(msg); await u.waitUntil(() => chatbox.messages.length); @@ -79,7 +79,6 @@ describe("A ChatBox's Unread Message Count", function () { spyOn(api.connection.get(), 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); chatbox.ui.set('scrolled', true); - _converse.windowState = 'hidden'; const msg = msgFactory(); _converse.handleMessageStanza(msg); await u.waitUntil(() => chatbox.messages.length); @@ -101,7 +100,7 @@ describe("A ChatBox's Unread Message Count", function () { const sent_stanzas = []; spyOn(api.connection.get(), 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); - _converse.windowState = 'hidden'; + spyOn(chatbox, 'isHidden').and.returnValue(true); const msg = msgFactory(); _converse.handleMessageStanza(msg); await u.waitUntil(() => chatbox.messages.length); @@ -110,7 +109,8 @@ describe("A ChatBox's Unread Message Count", function () { expect(chatbox.get('first_unread_id')).toBe(msgid); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1); expect(sent_stanzas[0].querySelector('received')).toBeDefined(); - u.saveWindowState({'type': 'focus'}); + chatbox.isHidden.and.returnValue(false); + document.dispatchEvent(new Event('visibilitychange')); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2); expect(sent_stanzas[1].querySelector('displayed')).toBeDefined(); expect(chatbox.get('num_unread')).toBe(0); @@ -145,7 +145,6 @@ describe("A ChatBox's Unread Message Count", function () { spyOn(api.connection.get(), 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); chatbox.ui.set('scrolled', true); - _converse.windowState = 'hidden'; const msg = msgFactory(); _converse.handleMessageStanza(msg); await u.waitUntil(() => chatbox.messages.length); @@ -154,7 +153,7 @@ describe("A ChatBox's Unread Message Count", function () { expect(chatbox.get('first_unread_id')).toBe(msgid); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1); expect(sent_stanzas[0].querySelector('received')).toBeDefined(); - u.saveWindowState({'type': 'focus'}); + document.dispatchEvent(new Event('visibilitychange')); await u.waitUntil(() => chatbox.get('num_unread') === 1); expect(chatbox.get('first_unread_id')).toBe(msgid); expect(sent_stanzas[0].querySelector('received')).toBeDefined(); diff --git a/src/plugins/controlbox/api.js b/src/plugins/controlbox/api.js index 7a23a836d2..0d7a002ee9 100644 --- a/src/plugins/controlbox/api.js +++ b/src/plugins/controlbox/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('./controlbox.js').default} ControlBox + */ import { _converse, api, converse } from "@converse/headless"; const { u } = converse.env; @@ -18,8 +21,10 @@ export default { */ async open () { await api.waitUntil('chatBoxesFetched'); - const model = await api.chatboxes.get('controlbox') || - api.chatboxes.create('controlbox', {}, _converse.Controlbox); + let model = await api.chatboxes.get('controlbox'); + if (!model) { + model = await api.chatboxes.create('controlbox', {}, _converse.ControlBox); + } u.safeSave(model, {'closed': false}); return model; }, @@ -27,7 +32,7 @@ export default { /** * Returns the controlbox view. * @method _converse.api.controlbox.get - * @returns { View } View representing the controlbox + * @returns {ControlBox} View representing the controlbox * @example const view = _converse.api.controlbox.get(); */ get () { diff --git a/src/plugins/controlbox/controlbox.js b/src/plugins/controlbox/controlbox.js index c1e4bca05e..662c3aa2e2 100644 --- a/src/plugins/controlbox/controlbox.js +++ b/src/plugins/controlbox/controlbox.js @@ -34,8 +34,8 @@ class ControlBox extends CustomElement { } setModel () { - this.model = _converse.chatboxes.get('controlbox'); - this.listenTo(_converse.connfeedback, 'change:connection_status', () => this.requestUpdate()); + this.model = _converse.state.chatboxes.get('controlbox'); + this.listenTo(_converse.state.connfeedback, 'change:connection_status', () => this.requestUpdate()); this.listenTo(this.model, 'change:active-form', () => this.requestUpdate()); this.listenTo(this.model, 'change:connected', () => this.requestUpdate()); this.listenTo(this.model, 'change:closed', () => !this.model.get('closed') && this.afterShown()); diff --git a/src/plugins/controlbox/index.js b/src/plugins/controlbox/index.js index f395eee3fc..5c00923c11 100644 --- a/src/plugins/controlbox/index.js +++ b/src/plugins/controlbox/index.js @@ -12,6 +12,7 @@ import ControlBoxView from './controlbox.js'; import controlbox_api from './api.js'; import { _converse, api, converse, log } from '@converse/headless'; import { addControlBox, clearSession, disconnect, onChatBoxesFetched } from './utils.js'; +import { CONTROLBOX_TYPE } from "@converse/headless/shared/constants.js"; import './styles/_controlbox.scss'; import './styles/controlbox-head.scss'; @@ -50,10 +51,7 @@ converse.plugins.add('converse-controlbox', { _converse.ControlBox = ControlBox; _converse.ControlBoxToggle = ControlBoxToggle; - api.chatboxes.registry.add( - _converse.CONTROLBOX_TYPE, - ControlBox - ); + api.chatboxes.registry.add(CONTROLBOX_TYPE, ControlBox); api.listen.on('chatBoxesFetched', onChatBoxesFetched); api.listen.on('clearSession', clearSession); diff --git a/src/plugins/controlbox/loginform.js b/src/plugins/controlbox/loginform.js index f405c3f783..2913d3e92f 100644 --- a/src/plugins/controlbox/loginform.js +++ b/src/plugins/controlbox/loginform.js @@ -5,13 +5,13 @@ import { CustomElement } from 'shared/components/element.js'; import { _converse, api, converse } from '@converse/headless'; import { updateSettingsWithFormData, validateJID } from './utils.js'; -const { Strophe, u } = converse.env; +const { Strophe } = converse.env; class LoginForm extends CustomElement { initialize () { - this.listenTo(_converse.connfeedback, 'change', () => this.requestUpdate()); + this.listenTo(_converse.state.connfeedback, 'change', () => this.requestUpdate()); this.handler = () => this.requestUpdate() } diff --git a/src/plugins/controlbox/model.js b/src/plugins/controlbox/model.js index 01a90e66c3..e2d86f3390 100644 --- a/src/plugins/controlbox/model.js +++ b/src/plugins/controlbox/model.js @@ -1,5 +1,6 @@ import { _converse, api, converse } from '@converse/headless'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; +import { CONTROLBOX_TYPE } from '@converse/headless/shared/constants'; const { dayjs } = converse.env; @@ -21,13 +22,13 @@ class ControlBox extends Model { 'closed': !api.settings.get('show_controlbox_by_default'), 'num_unread': 0, 'time_opened': dayjs(0).valueOf(), - 'type': _converse.CONTROLBOX_TYPE, + 'type': CONTROLBOX_TYPE, 'url': '' }; } validate (attrs) { - if (attrs.type === _converse.CONTROLBOX_TYPE) { + if (attrs.type === CONTROLBOX_TYPE) { if (api.settings.get('view_mode') === 'embedded' && api.settings.get('singleton')) { return 'Controlbox not relevant in embedded view mode'; } diff --git a/src/plugins/controlbox/navback.js b/src/plugins/controlbox/navback.js index 73a487c07d..64f7154586 100644 --- a/src/plugins/controlbox/navback.js +++ b/src/plugins/controlbox/navback.js @@ -5,6 +5,11 @@ import { api } from "@converse/headless"; class ControlBoxNavback extends CustomElement { + constructor () { + super(); + this.jid = null; + } + static get properties () { return { 'jid': { type: String } diff --git a/src/plugins/controlbox/templates/controlbox.js b/src/plugins/controlbox/templates/controlbox.js index e9ed52159e..18e925e3fa 100644 --- a/src/plugins/controlbox/templates/controlbox.js +++ b/src/plugins/controlbox/templates/controlbox.js @@ -6,7 +6,7 @@ const { Strophe } = converse.env; function whenNotConnected (o) { - const connection_status = _converse.connfeedback.get('connection_status'); + const connection_status = _converse.state.connfeedback.get('connection_status'); if ([Strophe.Status.RECONNECTING, Strophe.Status.CONNECTING].includes(connection_status)) { return tplSpinner(); } diff --git a/src/plugins/controlbox/templates/loginform.js b/src/plugins/controlbox/templates/loginform.js index b91f57a45e..bd1d5f2db2 100644 --- a/src/plugins/controlbox/templates/loginform.js +++ b/src/plugins/controlbox/templates/loginform.js @@ -143,13 +143,14 @@ const form_fields = (el) => { }; export default (el) => { - const connection_status = _converse.connfeedback.get('connection_status'); + const { connfeedback } = _converse.state; + const connection_status = connfeedback.get('connection_status'); let feedback_class, pretty_status; if (REPORTABLE_STATUSES.includes(connection_status)) { pretty_status = PRETTY_CONNECTION_STATUS[connection_status]; feedback_class = CONNECTION_STATUS_CSS_CLASS[connection_status]; } - const conn_feedback_message = _converse.connfeedback.get('message'); + const conn_feedback_message = connfeedback.get('message'); return html`
diff --git a/src/plugins/muc-views/templates/muc-sidebar.js b/src/plugins/muc-views/templates/muc-sidebar.js index defdf361d6..1788254dcd 100644 --- a/src/plugins/muc-views/templates/muc-sidebar.js +++ b/src/plugins/muc-views/templates/muc-sidebar.js @@ -1,21 +1,51 @@ +import 'shared/components/contacts-filter.js'; import tplOccupant from "./occupant.js"; +import tplOccupantsFilter from './occupants-filter.js'; import { __ } from 'i18n'; import { html } from "lit"; import { repeat } from 'lit/directives/repeat.js'; +function isOccupantFiltered (el, occ) { + const type = el.filter.get('filter_type'); + const q = (type === 'state') ? + el.filter.get('chat_state').toLowerCase() : + el.filter.get('filter_text').toLowerCase(); -export default (o) => { + if (!q) return false; + + if (type === 'state') { + const show = occ.get('show'); + return q === 'online' ? ["offline", "unavailable"].includes(show) : !show.includes(q); + } else if (type === 'contacts') { + return !occ.getDisplayName().toLowerCase().includes(q); + } +} + +function shouldShowOccupant (el, occ, o) { + return isOccupantFiltered(el, occ) ? '' : tplOccupant(occ, o); +} + +export default (el, o) => { const i18n_participants = o.occupants.length === 1 ? __('Participant') : __('Participants'); return html`
${o.occupants.length} ${i18n_participants} - + el.closeSidebar(ev)}>
-
    ${ repeat(o.occupants, (occ) => occ.get('jid'), (occ) => tplOccupant(occ, o)) }
+
    + el.requestUpdate()} + .promise=${el.model.initialized} + .contacts=${el.model.occupants} + .template=${tplOccupantsFilter} + .filter=${el.filter}> + + ${ repeat(o.occupants, (occ) => occ.get('jid'), (occ) => shouldShowOccupant(el, occ, o)) } +
`; } diff --git a/src/plugins/muc-views/templates/occupants-filter.js b/src/plugins/muc-views/templates/occupants-filter.js new file mode 100644 index 0000000000..0d92889d8f --- /dev/null +++ b/src/plugins/muc-views/templates/occupants-filter.js @@ -0,0 +1,66 @@ +import { html } from "lit"; +import { __ } from 'i18n'; +import { api } from '@converse/headless'; + +/** + * @param {import('shared/components/contacts-filter').ContactsFilter} el + */ +export default (el) => { + const i18n_placeholder = __('Filter'); + const title_contact_filter = __('Filter by name'); + const title_status_filter = __('Filter by status'); + const label_any = __('Any'); + const label_online = __('Online'); + const label_chatty = __('Chatty'); + const label_busy = __('Busy'); + const label_away = __('Away'); + const label_xa = __('Extended Away'); + const label_offline = __('Offline'); + + const chat_state = el.filter.get('chat_state'); + const filter_text = el.filter.get('filter_text'); + const filter_type = el.filter.get('filter_type'); + + const is_overlay_mode = api.settings.get('view_mode') === 'overlayed'; + + return html` + el.submitFilter(ev)}> +
+
+ el.changeTypeFilter(ev)} + class="fa fa-user clickable ${ (filter_type === 'contacts') ? 'selected' : '' }" + data-type="contacts" + title="${title_contact_filter}"> + el.changeTypeFilter(ev)} + class="fa fa-circle clickable ${ (filter_type === 'state') ? 'selected' : '' }" + data-type="state" + title="${title_status_filter}"> +
+
+ el.liveFilter(ev)} + class="contacts-filter form-control ${ (filter_type === 'state') ? 'hidden' : '' }" + placeholder="${i18n_placeholder}"/> + el.clearFilter(ev)}> + +
+ +
+ ` +}; diff --git a/src/plugins/muc-views/tests/modtools.js b/src/plugins/muc-views/tests/modtools.js index 3162ac18ed..d31500f414 100644 --- a/src/plugins/muc-views/tests/modtools.js +++ b/src/plugins/muc-views/tests/modtools.js @@ -462,16 +462,16 @@ describe("The groupchat moderator tool", function () { await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner', 'admin']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner', 'admin', 'outcast']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['member', 'none']); expect(_converse.getAssignableRoles(occupant)).toEqual(['moderator', 'participant', 'visitor']); diff --git a/src/plugins/muc-views/tests/muc.js b/src/plugins/muc-views/tests/muc.js index a470aa5bc6..db84570f82 100644 --- a/src/plugins/muc-views/tests/muc.js +++ b/src/plugins/muc-views/tests/muc.js @@ -1305,7 +1305,7 @@ describe("Groupchats", function () { })); it("allows the user to invite their roster contacts to enter the groupchat", - mock.initConverse(['chatBoxesFetched'], {'view_mode': 'fullscreen'}, async function (_converse) { + mock.initConverse(['chatBoxesFetched'], {'view_mode': 'overlayed'}, async function (_converse) { // We need roster contacts, so that we have someone to invite await mock.waitForRoster(_converse, 'current'); @@ -2394,7 +2394,7 @@ describe("Groupchats", function () { }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) _converse.api.connection.get()._dataRecv(mock.createRequest(result)); await u.waitUntil(() => view.querySelectorAll('.occupant').length, 500); - await u.waitUntil(() => view.querySelectorAll('.badge').length > 1); + await u.waitUntil(() => view.querySelectorAll('.badge').length > 2); expect(view.model.occupants.length).toBe(2); expect(view.querySelectorAll('.occupant').length).toBe(2); })); diff --git a/src/plugins/muc-views/tests/nickname.js b/src/plugins/muc-views/tests/nickname.js index e0b0185058..1740790971 100644 --- a/src/plugins/muc-views/tests/nickname.js +++ b/src/plugins/muc-views/tests/nickname.js @@ -122,8 +122,8 @@ describe("A MUC", function () { const view = _converse.chatboxviews.get('lounge@montague.lit'); await u.waitUntil(() => view.querySelectorAll('li .occupant-nick').length, 500); let occupants = view.querySelector('.occupant-list'); - expect(occupants.childElementCount).toBe(1); - expect(occupants.firstElementChild.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick"); + expect(occupants.querySelectorAll('.occupant-nick').length).toBe(1); + expect(occupants.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick"); const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent); expect(csntext.trim()).toEqual("oldnick has entered the groupchat"); @@ -153,7 +153,7 @@ describe("A MUC", function () { expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); occupants = view.querySelector('.occupant-list'); - expect(occupants.childElementCount).toBe(1); + expect(occupants.querySelectorAll('.occupant-nick').length).toBe(1); presence = $pres().attrs({ from:'lounge@montague.lit/newnick', diff --git a/src/plugins/muc-views/tests/occupants-filter.js b/src/plugins/muc-views/tests/occupants-filter.js new file mode 100644 index 0000000000..8a4c54cff7 --- /dev/null +++ b/src/plugins/muc-views/tests/occupants-filter.js @@ -0,0 +1,75 @@ +/* global mock, converse */ + +const { $pres, u } = converse.env; + +describe("The MUC occupants filter", function () { + + it("can be used to filter which occupants are shown", + mock.initConverse( + [], {}, + async function (_converse) { + + const muc_jid = 'lounge@montague.lit' + const members = [{ + 'nick': 'juliet', + 'jid': 'juliet@capulet.lit', + 'affiliation': 'member' + }, { + 'nick': 'tybalt', + 'jid': 'tybalt@capulet.lit', + 'affiliation': 'member' + }]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.model.occupants.length === 3); + + let filter_el = view.querySelector('converse-contacts-filter'); + expect(u.isVisible(filter_el.firstElementChild)).toBe(false); + + for (let i=0; i occupants.querySelectorAll('li').length > 3); + expect(occupants.querySelectorAll('li').length).toBe(3+mock.chatroom_names.length); + expect(view.model.occupants.length).toBe(3+mock.chatroom_names.length); + + mock.chatroom_names.forEach(name => { + const model = view.model.occupants.findWhere({'nick': name}); + const index = view.model.occupants.indexOf(model); + expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); + + filter_el = view.querySelector('converse-contacts-filter'); + expect(u.isVisible(filter_el.firstElementChild)).toBe(true); + + const filter = view.querySelector('.contacts-filter'); + filter.value = "j"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 1); + + filter_el.querySelector('.fa-times').click(); + await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 3+mock.chatroom_names.length); + + filter_el.querySelector('.fa-circle').click(); + const state_select = view.querySelector('.state-type'); + state_select.value = "dnd"; + u.triggerEvent(state_select, 'change'); + expect(state_select.value).toBe('dnd'); + expect(state_select.options[state_select.selectedIndex].textContent).toBe('Busy'); + await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 0); + })); +}); diff --git a/src/plugins/muc-views/tests/occupants.js b/src/plugins/muc-views/tests/occupants.js index 6771995ecd..9d794e93e1 100644 --- a/src/plugins/muc-views/tests/occupants.js +++ b/src/plugins/muc-views/tests/occupants.js @@ -17,7 +17,6 @@ describe("The occupants sidebar", function () { const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => view.model.occupants.length === 2); - const occupants = view.querySelector('.occupant-list'); for (let i=0; i occupants.querySelectorAll('li').length > 2, 500); expect(occupants.querySelectorAll('li').length).toBe(2+mock.chatroom_names.length); expect(view.model.occupants.length).toBe(2+mock.chatroom_names.length); diff --git a/src/plugins/muc-views/utils.js b/src/plugins/muc-views/utils.js index f51086dee1..9521d0325a 100644 --- a/src/plugins/muc-views/utils.js +++ b/src/plugins/muc-views/utils.js @@ -3,6 +3,7 @@ import './modals/moderator-tools.js'; import tplSpinner from 'templates/spinner.js'; import { __ } from 'i18n'; import { _converse, api, converse, log } from "@converse/headless"; +import { CHATROOMS_TYPE } from '@converse/headless/shared/constants.js'; import { html } from "lit"; import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js'; @@ -241,7 +242,7 @@ export function showOccupantModal (ev, occupant) { export function parseMessageForMUCCommands (data, handled) { const model = data.model; if (handled || - model.get('type') !== _converse.CHATROOMS_TYPE || ( + model.get('type') !== CHATROOMS_TYPE || ( api.settings.get('muc_disable_slash_commands') && !Array.isArray(api.settings.get('muc_disable_slash_commands')) )) { diff --git a/src/plugins/notifications/tests/notification.js b/src/plugins/notifications/tests/notification.js index f04fc45ba6..8500318faf 100644 --- a/src/plugins/notifications/tests/notification.js +++ b/src/plugins/notifications/tests/notification.js @@ -204,7 +204,9 @@ describe("Notifications", function () { spyOn(converse.env, 'Favico').and.returnValue(favico); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const previous_state = _converse.windowState; + const view = await mock.openChatBoxFor(_converse, sender_jid) + spyOn(view.model, 'isHidden').and.returnValue(true); + const msg = $msg({ from: sender_jid, to: _converse.api.connection.get().jid, @@ -212,7 +214,6 @@ describe("Notifications", function () { id: u.getUniqueId() }).c('body').t('This message will increment the message counter').up() .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.windowState = 'hidden'; spyOn(_converse.api, "trigger").and.callThrough(); @@ -234,17 +235,16 @@ describe("Notifications", function () { await u.waitUntil(() => favico.badge.calls.count() === 1); expect(favico.badge.calls.mostRecent().args.pop()).toBe(2); - const view = _converse.chatboxviews.get(sender_jid); expect(view.model.get('num_unread')).toBe(2); // Check that it's cleared when the window is focused - _converse.windowState = 'hidden'; - u.saveWindowState({'type': 'focus'}); + view.model.isHidden.and.returnValue(false); + document.dispatchEvent(new Event('visibilitychange')); + await u.waitUntil(() => favico.badge.calls.count() === 2); expect(favico.badge.calls.mostRecent().args.pop()).toBe(0); expect(view.model.get('num_unread')).toBe(0); - _converse.windowSate = previous_state; })); it("is not incremented when the message is received and the window is focused", @@ -256,7 +256,7 @@ describe("Notifications", function () { const favico = jasmine.createSpyObj('favico', ['badge']); spyOn(converse.env, 'Favico').and.returnValue(favico); - u.saveWindowState({'type': 'focus'}); + document.dispatchEvent(new Event('visibilitychange')); const message = 'This message will not increment the message counter'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', msg = $msg({ @@ -297,16 +297,17 @@ describe("Notifications", function () { .tree(); // leave converse-chat page - _converse.windowState = 'hidden'; + spyOn(_converse.exports.ChatBox.prototype, 'isHidden').and.returnValue(true); + await _converse.handleMessageStanza(msgFactory()); let view = _converse.chatboxviews.get(sender_jid); await u.waitUntil(() => favico.badge.calls.count() === 1, 1000); expect(favico.badge.calls.mostRecent().args.pop()).toBe(1); expect(view.model.get('num_unread')).toBe(1); + view.model.isHidden.and.returnValue(false); // come back to converse-chat page - u.saveWindowState({'type': 'focus'}); - + document.dispatchEvent(new Event('visibilitychange')); await u.waitUntil(() => u.isVisible(view)); expect(view.model.get('num_unread')).toBe(0); @@ -316,7 +317,7 @@ describe("Notifications", function () { // close chatbox and leave converse-chat page again view.close(); - _converse.windowState = 'hidden'; + view.model.isHidden.and.returnValue(true); // check that msg_counter is incremented from zero again await _converse.handleMessageStanza(msgFactory()); diff --git a/src/plugins/notifications/utils.js b/src/plugins/notifications/utils.js index 3ed15c0bd8..25660137e1 100644 --- a/src/plugins/notifications/utils.js +++ b/src/plugins/notifications/utils.js @@ -1,3 +1,8 @@ +/** + * @typedef {module:headless-plugins-muc-muc.MUCMessageAttributes} MUCMessageAttributes + * @typedef {module:headless-plugins-muc-muc.MUCMessageData} MUCMessageData + * @typedef {module:headless-plugins-chat-utils.MessageData} MessageData + */ import Favico from 'favico.js-slevomat'; import { __ } from 'i18n'; import { _converse, api, converse, log } from '@converse/headless'; @@ -24,9 +29,13 @@ export function areDesktopNotificationsEnabled () { ); } +/** + * @typedef {Navigator & {clearAppBadge: Function, setAppBadge: Function} } navigator + */ + export function clearFavicon () { favicon = null; - navigator.clearAppBadge?.() + /** @type navigator */(navigator).clearAppBadge?.() .catch(e => log.error("Could not clear unread count in app badge " + e)); } @@ -36,7 +45,7 @@ export function updateUnreadFavicon () { const chats = _converse.chatboxes.models; const num_unread = chats.reduce((acc, chat) => acc + (chat.get('num_unread') || 0), 0); favicon.badge(num_unread); - navigator.setAppBadge?.(num_unread) + /** @type navigator */(navigator).setAppBadge?.(num_unread) .catch(e => log.error("Could set unread count in app badge - " + e)); } } @@ -50,7 +59,6 @@ function isReferenced (references, muc_jid, nick) { /** * Is this a group message for which we should notify the user? - * @private * @param { MUCMessageAttributes } attrs */ export async function shouldNotifyOfGroupMessage (attrs) { @@ -116,7 +124,7 @@ async function shouldNotifyOfInfoMessage (attrs) { * @private * @async * @method shouldNotifyOfMessage - * @param { MessageData|MUCMessageData } data + * @param {MessageData|MUCMessageData} data */ function shouldNotifyOfMessage (data) { const { attrs } = data; @@ -287,7 +295,7 @@ export async function handleMessageNotification (data) { * Triggered when a notification (sound or HTML5 notification) for a new * message has will be made. * @event _converse#messageNotification - * @type { MessageData|MUCMessageData} + * @type {MessageData|MUCMessageData} * @example _converse.api.listen.on('messageNotification', data => { ... }); */ api.trigger('messageNotification', data); @@ -296,7 +304,7 @@ export async function handleMessageNotification (data) { } export function handleFeedback (data) { - if (areDesktopNotificationsEnabled(true)) { + if (areDesktopNotificationsEnabled()) { showFeedbackNotification(data); } } @@ -321,7 +329,7 @@ function showContactRequestNotification (contact) { } export function handleContactRequestNotification (contact) { - if (areDesktopNotificationsEnabled(true)) { + if (areDesktopNotificationsEnabled()) { showContactRequestNotification(contact); } } diff --git a/src/plugins/omemo/api.js b/src/plugins/omemo/api.js index 47bb62f3c1..34c677d558 100644 --- a/src/plugins/omemo/api.js +++ b/src/plugins/omemo/api.js @@ -29,8 +29,8 @@ export default { * Returns the {@link _converse.DeviceList} for a particular JID. * The device list will be created if it doesn't exist already. * @method _converse.api.omemo.devicelists.get - * @param { String } jid - The Jabber ID for which the device list will be returned. - * @param { bool } create=false - Set to `true` if the device list + * @param {String} jid - The Jabber ID for which the device list will be returned. + * @param {boolean} create=false - Set to `true` if the device list * should be created if it cannot be found. */ async get (jid, create=false) { diff --git a/src/plugins/omemo/device.js b/src/plugins/omemo/device.js index 7c032a2959..8ed32bf350 100644 --- a/src/plugins/omemo/device.js +++ b/src/plugins/omemo/device.js @@ -1,5 +1,5 @@ -import { IQError } from './errors.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { IQError } from 'shared/errors.js'; +import { Model } from '@converse/skeletor'; import { UNDECIDED } from './consts.js'; import { _converse, api, converse, log } from '@converse/headless'; import { getRandomInt } from '@converse/headless/utils/index.js'; @@ -58,7 +58,7 @@ class Device extends Model { */ getBundle () { if (this.get('bundle')) { - return Promise.resolve(this.get('bundle'), this); + return Promise.resolve(this.get('bundle')); } else { return this.fetchBundleFromServer(); } diff --git a/src/plugins/omemo/devicelist.js b/src/plugins/omemo/devicelist.js index 9949e1ef35..a3f52455e8 100644 --- a/src/plugins/omemo/devicelist.js +++ b/src/plugins/omemo/devicelist.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { _converse, api, converse, log } from '@converse/headless'; import { getOpenPromise } from '@converse/openpromise'; import { initStorage } from '@converse/headless/utils/storage.js'; diff --git a/src/plugins/omemo/devicelists.js b/src/plugins/omemo/devicelists.js index 50da98d9a9..bb8a400f2a 100644 --- a/src/plugins/omemo/devicelists.js +++ b/src/plugins/omemo/devicelists.js @@ -1,5 +1,5 @@ import DeviceList from './devicelist.js'; -import { Collection } from '@converse/skeletor/src/collection'; +import { Collection } from '@converse/skeletor'; class DeviceLists extends Collection { diff --git a/src/plugins/omemo/devices.js b/src/plugins/omemo/devices.js index 47899ae48e..3d01d97a41 100644 --- a/src/plugins/omemo/devices.js +++ b/src/plugins/omemo/devices.js @@ -1,5 +1,5 @@ import Device from './device.js'; -import { Collection } from '@converse/skeletor/src/collection'; +import { Collection } from '@converse/skeletor'; class Devices extends Collection { diff --git a/src/plugins/omemo/errors.js b/src/plugins/omemo/errors.js deleted file mode 100644 index b99c501e94..0000000000 --- a/src/plugins/omemo/errors.js +++ /dev/null @@ -1,7 +0,0 @@ -export class IQError extends Error { - constructor (message, iq) { - super(message, iq); - this.name = 'IQError'; - this.iq = iq; - } -} diff --git a/src/plugins/omemo/fingerprints.js b/src/plugins/omemo/fingerprints.js index 1fe637c4b0..1adcd1768d 100644 --- a/src/plugins/omemo/fingerprints.js +++ b/src/plugins/omemo/fingerprints.js @@ -4,6 +4,11 @@ import { api } from "@converse/headless"; export class Fingerprints extends CustomElement { + constructor () { + super(); + this.jid = null; + } + static get properties () { return { 'jid': { type: String } diff --git a/src/plugins/omemo/index.js b/src/plugins/omemo/index.js index 997c9e9220..7d96a71386 100644 --- a/src/plugins/omemo/index.js +++ b/src/plugins/omemo/index.js @@ -1,6 +1,9 @@ /** * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) + * + * @module plugins-omemo-index + * @typedef {Window & globalThis & {libsignal: any} } WindowWithLibsignal */ import './fingerprints.js'; import './profile.js'; @@ -43,7 +46,7 @@ Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + '.bundles'); converse.plugins.add('converse-omemo', { enabled (_converse) { return ( - window.libsignal && + /** @type WindowWithLibsignal */(window).libsignal && _converse.config.get('trusted') && !api.settings.get('clear_cache_on_logout') && !_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo') diff --git a/src/plugins/omemo/store.js b/src/plugins/omemo/store.js index 455e5b6191..1b483d0332 100644 --- a/src/plugins/omemo/store.js +++ b/src/plugins/omemo/store.js @@ -1,6 +1,8 @@ -/* global libsignal */ +/** + * @typedef {module:plugins-omemo-index.WindowWithLibsignal} WindowWithLibsignal + */ import range from 'lodash-es/range'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { generateDeviceID } from './utils.js'; import { _converse, api, converse, log } from '@converse/headless'; @@ -28,7 +30,7 @@ class OMEMOStore extends Model { return Promise.resolve(parseInt(this.get('device_id'), 10)); } - isTrustedIdentity (identifier, identity_key, direction) { // eslint-disable-line no-unused-vars + isTrustedIdentity (identifier, identity_key, _direction) { if (identifier === null || identifier === undefined) { throw new Error("Can't check identity key for invalid key"); } @@ -53,6 +55,7 @@ class OMEMOStore extends Model { if (identifier === null || identifier === undefined) { throw new Error("Can't save identity_key for invalid identifier"); } + const { libsignal } = /** @type WindowWithLibsignal */(window); const address = new libsignal.SignalProtocolAddress.fromString(identifier); const existing = this.get('identity_key' + address.getName()); const b64_idkey = u.arrayBufferToBase64(identity_key); @@ -97,7 +100,7 @@ class OMEMOStore extends Model { return Promise.resolve(); } - loadSignedPreKey (keyId) { // eslint-disable-line no-unused-vars + loadSignedPreKey (_keyId) { const res = this.get('signed_prekey'); if (res) { return Promise.resolve({ @@ -184,6 +187,9 @@ class OMEMOStore extends Model { } async generateMissingPreKeys () { + const { libsignal } = /** @type WindowWithLibsignal */(window); + const { KeyHelper } = libsignal; + const prekeyIds = Object.keys(this.getPreKeys()); const missing_keys = range(0, _converse.NUM_PREKEYS) .map(id => id.toString()) @@ -193,15 +199,20 @@ class OMEMOStore extends Model { log.warn('No missing prekeys to generate for our own device'); return Promise.resolve(); } + const keys = await Promise.all( - missing_keys.map(id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))) + missing_keys.map(id => KeyHelper.generatePreKey(parseInt(id, 10))) ); keys.forEach(k => this.storePreKey(k.keyId, k.keyPair)); - const marshalled_keys = Object.keys(this.getPreKeys()).map(k => ({ - 'id': k.keyId, - 'key': u.arrayBufferToBase64(k.pubKey) + + const prekeys = this.getPreKeys(); + const marshalled_keys = Object.keys(prekeys).map((id) => ({ + id, + key: prekeys[id].pubKey })); - const devicelist = await api.omemo.devicelists.get(_converse.bare_jid); + + const bare_jid = _converse.session.get('bare_jid'); + const devicelist = await api.omemo.devicelists.get(bare_jid); const device = devicelist.devices.get(this.get('device_id')); const bundle = await device.getBundle(); device.save('bundle', Object.assign(bundle, { 'prekeys': marshalled_keys })); @@ -219,6 +230,7 @@ class OMEMOStore extends Model { */ async generatePreKeys () { const amount = _converse.NUM_PREKEYS; + const { libsignal } = /** @type WindowWithLibsignal */(window); const { KeyHelper } = libsignal; const keys = await Promise.all( range(0, amount).map(id => KeyHelper.generatePreKey(id)) @@ -241,6 +253,8 @@ class OMEMOStore extends Model { * even if we're offline at that time. */ async generateBundle () { + const { libsignal } = /** @type WindowWithLibsignal */(window); + // The first thing that needs to happen if a client wants to // start using OMEMO is they need to generate an IdentityKey // and a Device ID. diff --git a/src/plugins/omemo/tests/omemo.js b/src/plugins/omemo/tests/omemo.js index 89fb7fc673..36f72d0412 100644 --- a/src/plugins/omemo/tests/omemo.js +++ b/src/plugins/omemo/tests/omemo.js @@ -228,11 +228,13 @@ describe("The OMEMO module", function() { it("will create a new device based on a received carbon message", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await u.waitUntil(() => mock.initializedOMEMO(_converse)); await mock.openChatBoxFor(_converse, contact_jid); + let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); const my_devicelist = _converse.devicelists.get({'jid': _converse.bare_jid}); expect(my_devicelist.devices.length).toBe(2); @@ -250,6 +252,8 @@ describe("The OMEMO module", function() { _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); + const { omemo_store } = _converse; + const contact_devicelist = _converse.devicelists.get({'jid': contact_jid}); await u.waitUntil(() => contact_devicelist.devices.length === 1); @@ -275,7 +279,7 @@ describe("The OMEMO module", function() {
- ${u.arrayBufferToBase64(obj.key_and_tag)} ${obj.iv}
@@ -291,6 +295,14 @@ describe("The OMEMO module", function() { _converse.api.connection.get().IQ_stanzas = []; _converse.api.connection.get()._dataRecv(mock.createRequest(carbon)); + // Remove one pre-key to exercise code that generates new ones. + const prekeys = omemo_store.getPreKeys(); + omemo_store.removePreKey(Object.keys(prekeys)[9]); + + let prekey_ids = Object.keys(omemo_store.getPreKeys()); + expect(prekey_ids.length).toBe(99); + expect(prekey_ids.includes('9')).toBe(false); + // The message received is a prekey message, so missing prekeys are // generated and a new bundle published. iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); @@ -331,6 +343,10 @@ describe("The OMEMO module", function() { ``+ ``+ ``); + + prekey_ids = Object.keys(omemo_store.getPreKeys()); + expect(prekey_ids.length).toBe(100); + expect(prekey_ids.includes('9')).toBe(true); })); it("can receive a PreKeySignalMessage", diff --git a/src/plugins/omemo/utils.js b/src/plugins/omemo/utils.js index 08c8fbc920..c9efea03da 100644 --- a/src/plugins/omemo/utils.js +++ b/src/plugins/omemo/utils.js @@ -1,4 +1,9 @@ -/* global libsignal */ +/** + * @typedef {module:plugins-omemo-index.WindowWithLibsignal} WindowWithLibsignal + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes + * @typedef {import('@converse/headless/plugins/chat/model.js').default} ChatBox + */ import tplAudio from 'templates/audio.js'; import tplFile from 'templates/file.js'; import tplImage from 'templates/image.js'; @@ -11,6 +16,7 @@ import { html } from 'lit'; import { initStorage } from '@converse/headless/utils/storage.js'; import { isError } from '@converse/headless/utils/object.js'; import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js'; +import { CHATROOMS_TYPE, PRIVATE_CHAT_TYPE } from '@converse/headless/shared/constants.js'; import { until } from 'lit/directives/until.js'; import { appendArrayBuffer, @@ -21,6 +27,8 @@ import { hexToArrayBuffer, stringToArrayBuffer } from '@converse/headless/utils/arraybuffer.js'; +import MUC from 'headless/plugins/muc/muc.js'; +import {IQError, UserFacingError} from 'shared/errors.js'; const { Strophe, URI, sizzle, u } = converse.env; @@ -33,8 +41,12 @@ export function formatFingerprint (fp) { return fp; } +/** + * @param {Error|IQError|UserFacingError} e + * @param {ChatBox} chat + */ export function handleMessageSendError (e, chat) { - if (e.name === 'IQError') { + if (e instanceof IQError) { chat.save('omemo_supported', false); const err_msgs = []; @@ -58,7 +70,7 @@ export function handleMessageSendError (e, chat) { err_msgs.push(e.iq.outerHTML); } api.alert('error', __('Error'), err_msgs); - } else if (e.user_facing) { + } else if (e instanceof UserFacingError) { api.alert('error', __('Error'), [e.message]); } throw e; @@ -76,6 +88,9 @@ export function getOutgoingMessageAttributes (chat, attrs) { return attrs; } +/** + * @param {string} plaintext + */ async function encryptMessage (plaintext) { // The client MUST use fresh, randomly generated key/IV pairs // with AES-128 in Galois/Counter Mode (GCM). @@ -119,13 +134,18 @@ async function decryptMessage (obj) { return arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher)); } +/** + * @param {File} file + * @returns {Promise} + */ export async function encryptFile (file) { const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt']); const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, }, key, await file.arrayBuffer()); const exported_key = await window.crypto.subtle.exportKey('raw', key); const encrypted_file = new File([encrypted], file.name, { type: file.type, lastModified: file.lastModified }); - encrypted_file.xep454_ivkey = arrayBufferToHex(iv) + arrayBufferToHex(exported_key); + + Object.assign(encrypted_file, { xep454_ivkey: arrayBufferToHex(iv) + arrayBufferToHex(exported_key) }); return encrypted_file; } @@ -296,7 +316,7 @@ export async function parseEncryptedMessage (stanza, attrs) { export function onChatBoxesInitialized () { _converse.chatboxes.on('add', chatbox => { checkOMEMOSupported(chatbox); - if (chatbox.get('type') === _converse.CHATROOMS_TYPE) { + if (chatbox.get('type') === CHATROOMS_TYPE) { chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o)); chatbox.features.on('change', () => checkOMEMOSupported(chatbox)); } @@ -324,8 +344,9 @@ export function onChatInitialized (el) { } export function getSessionCipher (jid, id) { + const { libsignal } = /** @type WindowWithLibsignal */(window); const address = new libsignal.SignalProtocolAddress(jid, id); - return new window.libsignal.SessionCipher(_converse.omemo_store, address); + return new libsignal.SessionCipher(_converse.omemo_store, address); } function getJIDForDecryption (attrs) { @@ -448,11 +469,9 @@ export function addKeysToMessageStanza (stanza, dicts, iv) { stanza.attrs({ 'prekey': prekey }); } stanza.up(); - if (i == dicts.length - 1) { - stanza.c('iv').t(iv).up().up(); - } } } + stanza.c('iv').t(iv).up().up(); return Promise.resolve(stanza); } @@ -496,6 +515,8 @@ export async function getDevicesForContact (jid) { } export async function generateDeviceID () { + const { libsignal } = /** @type WindowWithLibsignal */(window); + /* Generates a device ID, making sure that it's unique */ const devicelist = await api.omemo.devicelists.get(_converse.bare_jid, true); const existing_ids = devicelist.devices.pluck('id'); @@ -515,6 +536,7 @@ export async function generateDeviceID () { } async function buildSession (device) { + const { libsignal } = /** @type WindowWithLibsignal */(window); const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')); const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address); const prekey = device.getRandomPreKey(); @@ -540,6 +562,8 @@ export async function getSession (device) { log.error(`Could not build an OMEMO session for device ${device.get('id')} because we don't have its bundle`); return null; } + + const { libsignal } = /** @type WindowWithLibsignal */(window); const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')); const session = await _converse.omemo_store.loadSession(address.toString()); if (session) { @@ -695,10 +719,10 @@ async function onOccupantAdded (chatroom, occupant) { async function checkOMEMOSupported (chatbox) { let supported; - if (chatbox.get('type') === _converse.CHATROOMS_TYPE) { + if (chatbox.get('type') === CHATROOMS_TYPE) { await api.waitUntil('OMEMOInitialized'); supported = chatbox.features.get('nonanonymous') && chatbox.features.get('membersonly'); - } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) { + } else if (chatbox.get('type') === PRIVATE_CHAT_TYPE) { supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid')); } chatbox.set('omemo_supported', supported); @@ -713,7 +737,7 @@ function toggleOMEMO (ev) { const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar'); if (!toolbar_el.model.get('omemo_supported')) { let messages; - if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) { + if (toolbar_el.model.get('type') === CHATROOMS_TYPE) { messages = [ __( 'Cannot use end-to-end encryption in this groupchat, ' + @@ -735,7 +759,7 @@ function toggleOMEMO (ev) { export function getOMEMOToolbarButton (toolbar_el, buttons) { const model = toolbar_el.model; - const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; + const is_muc = model.get('type') === CHATROOMS_TYPE; let title; if (model.get('omemo_supported')) { const i18n_plaintext = __('Messages are being sent in plaintext'); @@ -774,18 +798,19 @@ export function getOMEMOToolbarButton (toolbar_el, buttons) { } +/** + * @param {MUC|ChatBox} chatbox + */ async function getBundlesAndBuildSessions (chatbox) { const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.'); let devices; - if (chatbox.get('type') === _converse.CHATROOMS_TYPE) { + if (chatbox instanceof MUC) { const collections = await Promise.all(chatbox.occupants.map(o => getDevicesForContact(o.get('jid')))); devices = collections.reduce((a, b) => a.concat(b.models), []); - } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) { + } else if (chatbox.get('type') === PRIVATE_CHAT_TYPE) { const their_devices = await getDevicesForContact(chatbox.get('jid')); if (their_devices.length === 0) { - const err = new Error(no_devices_err); - err.user_facing = true; - throw err; + throw new UserFacingError(no_devices_err); } const own_list = await api.omemo.devicelists.get(_converse.bare_jid) const own_devices = own_list.devices; @@ -803,9 +828,7 @@ async function getBundlesAndBuildSessions (chatbox) { // We couldn't build a session for certain devices. devices = devices.filter(d => sessions[devices.indexOf(d)]); if (devices.length === 0) { - const err = new Error(no_devices_err); - err.user_facing = true; - throw err; + throw new UserFacingError(no_devices_err); } } return devices; diff --git a/src/plugins/profile/modals/chat-status.js b/src/plugins/profile/modals/chat-status.js index 14e67fa2a9..6ada2500c0 100644 --- a/src/plugins/profile/modals/chat-status.js +++ b/src/plugins/profile/modals/chat-status.js @@ -1,26 +1,29 @@ -import BaseModal from "plugins/modal/modal.js"; -import tplChatStatusModal from "../templates/chat-status-modal.js"; +import BaseModal from 'plugins/modal/modal.js'; +import tplChatStatusModal from '../templates/chat-status-modal.js'; import { __ } from 'i18n'; -import { _converse, api, converse } from "@converse/headless"; +import { _converse, api, converse } from '@converse/headless'; const u = converse.env.utils; - export default class ChatStatusModal extends BaseModal { - initialize () { super.initialize(); this.render(); - this.addEventListener('shown.bs.modal', () => { - this.querySelector('input[name="status_message"]').focus(); - }, false); + this.addEventListener( + 'shown.bs.modal', + () => { + /** @type {HTMLInputElement} */ (this.querySelector('input[name="status_message"]')).focus(); + }, + false + ); } renderModal () { return tplChatStatusModal(this); } - getModalTitle () { // eslint-disable-line class-methods-use-this + getModalTitle () { + // eslint-disable-line class-methods-use-this return __('Change chat status'); } @@ -29,7 +32,7 @@ export default class ChatStatusModal extends BaseModal { ev.preventDefault(); u.hideElement(this.querySelector('.clear-input')); } - const roster_filter = this.querySelector('input[name="status_message"]'); + const roster_filter = /** @type {HTMLInputElement} */ (this.querySelector('input[name="status_message"]')); roster_filter.value = ''; } @@ -38,7 +41,7 @@ export default class ChatStatusModal extends BaseModal { const data = new FormData(ev.target); this.model.save({ 'status_message': data.get('status_message'), - 'status': data.get('chat_status') + 'status': data.get('chat_status'), }); this.modal.hide(); } diff --git a/src/plugins/profile/modals/profile.js b/src/plugins/profile/modals/profile.js index 462870258e..1b90bf410d 100644 --- a/src/plugins/profile/modals/profile.js +++ b/src/plugins/profile/modals/profile.js @@ -57,7 +57,7 @@ export default class ProfileModal extends BaseModal { ev.preventDefault(); const reader = new FileReader(); const form_data = new FormData(ev.target); - const image_file = form_data.get('image'); + const image_file = /** @type {File} */(form_data.get('image')); const data = { 'fn': form_data.get('fn'), 'nickname': form_data.get('nickname'), @@ -77,7 +77,7 @@ export default class ProfileModal extends BaseModal { const { photo, } = conversions[0]; reader.onloadend = () => { Object.assign(data, { - 'image': btoa(reader.result), + 'image': btoa(/** @type {string} */(reader.result)), 'image_type': image_file.type }); this.setVCard(data); diff --git a/src/plugins/profile/templates/profile.js b/src/plugins/profile/templates/profile.js index f8f1cf12f3..9640aa7e51 100644 --- a/src/plugins/profile/templates/profile.js +++ b/src/plugins/profile/templates/profile.js @@ -50,7 +50,7 @@ export default (el) => {
` diff --git a/src/plugins/push/utils.js b/src/plugins/push/utils.js index 0c40843b07..8079049e7c 100644 --- a/src/plugins/push/utils.js +++ b/src/plugins/push/utils.js @@ -1,4 +1,5 @@ import { _converse, api, converse, log } from "@converse/headless"; +import { CHATROOMS_TYPE } from "@converse/headless/shared/constants"; const { Strophe, $iq } = converse.env; @@ -87,7 +88,7 @@ export async function enablePush (domain) { } export function onChatBoxAdded (model) { - if (model.get('type') == _converse.CHATROOMS_TYPE) { + if (model.get('type') == CHATROOMS_TYPE) { enablePush(Strophe.getDomainFromJid(model.get('jid'))); } } diff --git a/src/plugins/register/panel.js b/src/plugins/register/panel.js index 756773c34e..6b484cd5c8 100644 --- a/src/plugins/register/panel.js +++ b/src/plugins/register/panel.js @@ -1,3 +1,6 @@ +/** + * @typedef {import("strophe.js/src/request.js").default} Request + */ import tplFormInput from "templates/form_input.js"; import tplFormUrl from "templates/form_url.js"; import tplFormUsername from "templates/form_username.js"; @@ -39,6 +42,9 @@ class RegisterPanel extends CustomElement { constructor () { super(); + this.urls = []; + this.fields = {}; + this.domain = null; this.alert_type = 'info'; this.setErrorMessage = (m) => this.setMessage(m, 'danger'); this.setFeedbackMessage = (m) => this.setMessage(m, 'info'); @@ -84,10 +90,10 @@ class RegisterPanel extends CustomElement { /** * Send an IQ stanza to the XMPP server asking for the registration fields. * @method _converse.RegisterPanel#getRegistrationFields - * @param { Strophe.Request } req - The current request - * @param { Function } callback - The callback function + * @param {Request} req - The current request + * @param {Function} callback - The callback function */ - getRegistrationFields (req, _callback) { + getRegistrationFields (req, callback) { const conn = api.connection.get(); conn.connected = true; @@ -101,7 +107,7 @@ class RegisterPanel extends CustomElement { const register = body.getElementsByTagName("register"); const mechanisms = body.getElementsByTagName("mechanism"); if (register.length === 0 && mechanisms.length === 0) { - conn._proto._no_auth_received(_callback); + conn._proto._no_auth_received(callback); return false; } if (register.length === 0) { @@ -162,14 +168,15 @@ class RegisterPanel extends CustomElement { /** * Event handler when the #converse-register form is submitted. * Depending on the available input fields, we delegate to other methods. - * @param { Event } ev + * @param {Event} ev */ onFormSubmission (ev) { ev?.preventDefault?.(); - if (ev.target.querySelector('input[name=domain]') === null) { - this.submitRegistrationForm(ev.target); + const form = /** @type {HTMLFormElement} */(ev.target); + if (form.querySelector('input[name=domain]') === null) { + this.submitRegistrationForm(form); } else { - this.onProviderChosen(ev.target); + this.onProviderChosen(form); } } @@ -180,7 +187,7 @@ class RegisterPanel extends CustomElement { * @param { HTMLElement } form - The form that was submitted */ onProviderChosen (form) { - const domain = form.querySelector('input[name=domain]')?.value; + const domain = /** @type {HTMLInputElement} */(form.querySelector('input[name=domain]'))?.value; if (domain) this.fetchRegistrationForm(domain.trim()); } @@ -409,17 +416,18 @@ class RegisterPanel extends CustomElement { * @param { Element } stanza - The IQ stanza. */ _onRegisterIQ (stanza) { + const connection = api.connection.get(); if (stanza.getAttribute("type") === "error") { log.error("Registration failed."); this.reportErrors(stanza); - const connection = api.connection.get(); - let error = stanza.getElementsByTagName("error"); - if (error.length !== 1) { + const error_els = stanza.getElementsByTagName("error"); + if (error_els.length !== 1) { connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); return false; } - error = error[0].firstElementChild.tagName.toLowerCase(); + + const error = error_els[0].firstElementChild.tagName.toLowerCase(); if (error === 'conflict') { connection._changeConnectStatus(Strophe.Status.CONFLICT, error); } else if (error === 'not-acceptable') { diff --git a/src/plugins/roomslist/index.js b/src/plugins/roomslist/index.js index 9a6f278823..7d9cc13fef 100644 --- a/src/plugins/roomslist/index.js +++ b/src/plugins/roomslist/index.js @@ -7,7 +7,7 @@ */ import "@converse/headless/plugins/muc/index.js"; import './view.js'; -import { converse } from "@converse/headless"; +import { api, converse } from "@converse/headless"; converse.plugins.add('converse-roomslist', { @@ -19,5 +19,9 @@ converse.plugins.add('converse-roomslist', { "converse-bookmarks" ], - initialize () { } + initialize () { + api.settings.extend({ + 'muc_grouped_by_domain': false, + }); + } }); diff --git a/src/plugins/roomslist/model.js b/src/plugins/roomslist/model.js index 587145d56a..1c5daa5863 100644 --- a/src/plugins/roomslist/model.js +++ b/src/plugins/roomslist/model.js @@ -1,4 +1,4 @@ -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { _converse, api, converse } from "@converse/headless"; const { Strophe } = converse.env; @@ -10,6 +10,7 @@ class RoomsListModel extends Model { 'muc_domain': api.settings.get('muc_domain'), 'nick': _converse.getDefaultMUCNickname(), 'toggle_state': _converse.OPENED, + 'collapsed_domains': [], }; } diff --git a/src/plugins/roomslist/styles/roomsgroups.scss b/src/plugins/roomslist/styles/roomsgroups.scss new file mode 100644 index 0000000000..fda2fd68cb --- /dev/null +++ b/src/plugins/roomslist/styles/roomsgroups.scss @@ -0,0 +1,14 @@ +.conversejs { + #chatrooms { + .muc-domain-group-toggle { + margin: 0.75em 0 0.25em 0; + } + + .muc-domain-group-toggle, .muc-domain-group-toggle .fa { + color: var(--groupchats-header-color); + &:hover { + color: var(--chatroom-head-bg-color-dark); + } + } + } +} diff --git a/src/plugins/roomslist/templates/groups.js b/src/plugins/roomslist/templates/groups.js new file mode 100644 index 0000000000..cead0bdb52 --- /dev/null +++ b/src/plugins/roomslist/templates/groups.js @@ -0,0 +1,41 @@ +import { __ } from 'i18n'; +import { html } from "lit"; +import { tplRoomItem } from 'plugins/roomslist/templates/roomslist.js' + +import '../styles/roomsgroups.scss'; + +function tplRoomDomainGroup (el, domain, rooms) { + const i18n_title = __('Click to hide these rooms'); + const collapsed = el.model.get('collapsed_domains'); + const is_collapsed = collapsed.includes(domain); + return html` +
+ el.toggleDomainList(ev, domain)}> + + ${domain} + + +
`; +} + +export function tplRoomDomainGroupList (el, rooms) { + // The rooms should stay sorted as they are iterated and added in order + const grouped_rooms = new Map(); + for (const room of rooms) { + const roomdomain = room.get('jid').split('@').at(-1).toLowerCase(); + if (grouped_rooms.has(roomdomain)) { + grouped_rooms.get(roomdomain).push(room); + } else { + grouped_rooms.set(roomdomain, [room]); + } + } + const sorted_domains = Array.from(grouped_rooms.keys()); + sorted_domains.sort(); + + return sorted_domains.map(domain => tplRoomDomainGroup(el, domain, grouped_rooms.get(domain))) +} diff --git a/src/plugins/roomslist/templates/roomslist.js b/src/plugins/roomslist/templates/roomslist.js index e20fc5276a..2441c9f9a3 100644 --- a/src/plugins/roomslist/templates/roomslist.js +++ b/src/plugins/roomslist/templates/roomslist.js @@ -5,6 +5,8 @@ import { _converse, api } from "@converse/headless"; import { html } from "lit"; import { isUniView } from '@converse/headless/utils/session.js'; import { addBookmarkViaEvent } from 'plugins/bookmark-views/utils.js'; +import { tplRoomDomainGroupList } from 'plugins/roomslist/templates/groups.js'; +import { CHATROOMS_TYPE, CLOSED } from '@converse/headless/shared/constants.js'; function isCurrentlyOpen (room) { @@ -33,7 +35,7 @@ const tplUnreadIndicator = (room) => html``; -function tplRoomItem (el, room) { +export function tplRoomItem (el, room) { const i18n_leave_room = __('Leave this groupchat'); const has_unread_msgs = room.get('num_unread_general') || room.get('has_activity'); return html` @@ -68,7 +70,8 @@ function tplRoomItem (el, room) { } export default (el) => { - const { chatboxes, CHATROOMS_TYPE, CLOSED } = _converse; + const group_by_domain = api.settings.get('muc_grouped_by_domain'); + const { chatboxes } = _converse; const rooms = chatboxes.filter(m => m.get('type') === CHATROOMS_TYPE); rooms.sort((a, b) => (a.getDisplayName().toLowerCase() <= b.getDisplayName().toLowerCase() ? -1 : 1)); @@ -111,7 +114,10 @@ export default (el) => {
- ${ rooms.map(room => tplRoomItem(el, room)) } + ${ group_by_domain ? + tplRoomDomainGroupList(el, rooms) : + rooms.map(room => tplRoomItem(el, room)) + }
`; } diff --git a/src/plugins/roomslist/tests/grouplists.js b/src/plugins/roomslist/tests/grouplists.js new file mode 100644 index 0000000000..918aa8c0ab --- /dev/null +++ b/src/plugins/roomslist/tests/grouplists.js @@ -0,0 +1,115 @@ +/* global mock, converse */ + +const { $msg, u } = converse.env; + + +describe("The list of MUC domains", function () { + it("is shown in controlbox", mock.initConverse( + ['chatBoxesFetched'], + { muc_grouped_by_domain: true, + allow_bookmarks: false // Makes testing easier, otherwise we + // have to mock stanza traffic. + }, async function (_converse) { + + await mock.waitForRoster(_converse, 'current', 0); + await mock.openControlBox(_converse); + const controlbox = _converse.chatboxviews.get('controlbox'); + let list = controlbox.querySelector('.list-container--openrooms'); + expect(u.hasClass('hidden', list)).toBeTruthy(); + await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC'); + + const lview = controlbox.querySelector('converse-rooms-list'); + // Check that the group is shown + await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length); + let group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(1); + // .children[0] should give the a tag with the domain in it + // there might be a more robust way to do this + // (select for ".muc-domain-group-toggle"?) + // .trim() because there is a space for the arrow/triangle icon first + expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit'); + // Check that the room is shown + await u.waitUntil(() => lview.querySelectorAll(".open-room").length); + let room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit'); + + // Check that a second room in the same domain is shown in the same + // domain group. + await mock.openChatRoom(_converse, 'secondroom', 'conference.shakespeare.lit', 'JC'); + await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1); + group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(1); // still only one group + expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit'); + room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(2); // but two rooms inside it + + + await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 2); + room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(3); + group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(2); + + let view = _converse.chatboxviews.get('room@conference.shakespeare.lit'); + await view.close(); + room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(2); + group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(2); + view = _converse.chatboxviews.get('secondroom@conference.shakespeare.lit'); + await view.close(); + room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(1); + expect(room_els[0].innerText).toBe('lounge@montague.lit'); + expect(group_els[0].children[0].innerText.trim()).toBe('montague.lit'); + list = controlbox.querySelector('.list-container--openrooms'); + u.waitUntil(() => Array.from(list.classList).includes('hidden')); + + view = _converse.chatboxviews.get('lounge@montague.lit'); + await view.close(); + room_els = lview.querySelectorAll(".open-room"); + expect(room_els.length).toBe(0); + group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(0); + + list = controlbox.querySelector('.list-container--openrooms'); + expect(Array.from(list.classList).includes('hidden')).toBeTruthy(); + })); +}); + +describe("A MUC domain group", function () { + it("is collapsible", mock.initConverse( + ['chatBoxesFetched'], + { muc_grouped_by_domain: true, + allow_bookmarks: false // Makes testing easier, otherwise we + // have to mock stanza traffic. + }, async function (_converse) { + + await mock.waitForRoster(_converse, 'current', 0); + await mock.openControlBox(_converse); + const controlbox = _converse.chatboxviews.get('controlbox'); + let list = controlbox.querySelector('.list-container--openrooms'); + await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC'); + + const lview = controlbox.querySelector('converse-rooms-list'); + await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length); + expect(u.hasClass('hidden', list)).toBeFalsy(); + let group_els = lview.querySelectorAll(".muc-domain-group"); + expect(group_els.length).toBe(1); + expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit'); + + // I would have liked to use u.isVisible on the room (.open-room) here, + // but it didn’t seem to work. + expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(false); + lview.querySelector('.muc-domain-group-toggle').click(); + await u.waitUntil(() => u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms")) === true); + expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(true); + lview.querySelector('.muc-domain-group-toggle').click(); + await u.waitUntil(() => u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms")) === false); + expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(false); + })); +}); diff --git a/src/plugins/roomslist/view.js b/src/plugins/roomslist/view.js index e0a3f38e1a..8771c0fe12 100644 --- a/src/plugins/roomslist/view.js +++ b/src/plugins/roomslist/view.js @@ -79,6 +79,16 @@ export class RoomsList extends CustomElement { u.slideIn(list_el).then(() => this.model.save({'toggle_state': _converse.CLOSED})); } } + + toggleDomainList (ev, domain) { + ev?.preventDefault?.(); + const collapsed = this.model.get('collapsed_domains'); + if (collapsed.includes(domain)) { + this.model.save({'collapsed_domains': collapsed.filter(d => d !== domain)}); + } else { + this.model.save({'collapsed_domains': [...collapsed, domain]}); + } + } } api.elements.define('converse-rooms-list', RoomsList); diff --git a/src/plugins/rosterview/contactview.js b/src/plugins/rosterview/contactview.js index 52e7d3179a..95d28b1d5a 100644 --- a/src/plugins/rosterview/contactview.js +++ b/src/plugins/rosterview/contactview.js @@ -13,6 +13,11 @@ export default class RosterContact extends CustomElement { } } + constructor () { + super(); + this.model = null; + } + initialize () { this.listenTo(this.model, 'change', () => this.requestUpdate()); this.listenTo(this.model, 'highlight', () => this.requestUpdate()); diff --git a/src/plugins/rosterview/filterview.js b/src/plugins/rosterview/filterview.js deleted file mode 100644 index 95f33ea89b..0000000000 --- a/src/plugins/rosterview/filterview.js +++ /dev/null @@ -1,92 +0,0 @@ -import debounce from "lodash-es/debounce"; -import tplRosterFilter from "./templates/roster_filter.js"; -import { CustomElement } from 'shared/components/element.js'; -import { _converse, api } from "@converse/headless"; -import { ancestor } from 'utils/html.js'; - - -export class RosterFilterView extends CustomElement { - - async initialize () { - await api.waitUntil('rosterInitialized') - this.model = _converse.roster_filter; - - this.liveFilter = debounce(() => { - this.model.save({'filter_text': this.querySelector('.roster-filter').value}); - }, 250); - - this.listenTo(_converse, 'rosterContactsFetched', () => this.requestUpdate()); - this.listenTo(_converse.presences, 'change:show', () => this.requestUpdate()); - this.listenTo(_converse.roster, "add", () => this.requestUpdate()); - this.listenTo(_converse.roster, "destroy", () => this.requestUpdate()); - this.listenTo(_converse.roster, "remove", () => this.requestUpdate()); - this.listenTo(this.model, 'change', this.dispatchUpdateEvent); - this.listenTo(this.model, 'change', () => this.requestUpdate()); - - this.requestUpdate(); - } - - render () { - return this.model ? - tplRosterFilter( - Object.assign(this.model.toJSON(), { - visible: this.shouldBeVisible(), - changeChatStateFilter: ev => this.changeChatStateFilter(ev), - changeTypeFilter: ev => this.changeTypeFilter(ev), - clearFilter: ev => this.clearFilter(ev), - liveFilter: ev => this.liveFilter(ev), - submitFilter: ev => this.submitFilter(ev), - })) : ''; - } - - dispatchUpdateEvent () { - this.dispatchEvent(new CustomEvent('update', { 'detail': this.model.changed })); - } - - changeChatStateFilter (ev) { - ev && ev.preventDefault(); - this.model.save({'chat_state': this.querySelector('.state-type').value}); - } - - changeTypeFilter (ev) { - ev && ev.preventDefault(); - const type = ancestor(ev.target, 'converse-icon')?.dataset.type || 'contacts'; - if (type === 'state') { - this.model.save({ - 'filter_type': type, - 'chat_state': this.querySelector('.state-type').value - }); - } else { - this.model.save({ - 'filter_type': type, - 'filter_text': this.querySelector('.roster-filter').value - }); - } - } - - submitFilter (ev) { - ev && ev.preventDefault(); - this.liveFilter(); - } - - /** - * Returns true if the filter is enabled (i.e. if the user - * has added values to the filter). - * @private - * @method _converse.RosterFilterView#isActive - */ - isActive () { - return (this.model.get('filter_type') === 'state' || this.model.get('filter_text')); - } - - shouldBeVisible () { - return _converse.roster?.length >= 5 || this.isActive(); - } - - clearFilter (ev) { - ev && ev.preventDefault(); - this.model.save({'filter_text': ''}); - } -} - -api.elements.define('converse-roster-filter', RosterFilterView); diff --git a/src/plugins/rosterview/index.js b/src/plugins/rosterview/index.js index 4dff5a0584..ef55a4d847 100644 --- a/src/plugins/rosterview/index.js +++ b/src/plugins/rosterview/index.js @@ -9,7 +9,6 @@ import "./modals/add-contact.js"; import './rosterview.js'; import RosterContactView from './contactview.js'; import { RosterFilter } from '@converse/headless/plugins/roster/filter.js'; -import { RosterFilterView } from './filterview.js'; import { _converse, api, converse } from "@converse/headless"; import { highlightRosterItem } from './utils.js'; @@ -32,7 +31,6 @@ converse.plugins.add('converse-rosterview', { api.promises.add('rosterViewInitialized'); _converse.RosterFilter = RosterFilter; - _converse.RosterFilterView = RosterFilterView; _converse.RosterContactView = RosterContactView; /* -------- Event Handlers ----------- */ diff --git a/src/plugins/rosterview/modals/add-contact.js b/src/plugins/rosterview/modals/add-contact.js index 608515c7a2..6aa7c5bfdc 100644 --- a/src/plugins/rosterview/modals/add-contact.js +++ b/src/plugins/rosterview/modals/add-contact.js @@ -1,30 +1,34 @@ import 'shared/autocomplete/index.js'; -import BaseModal from "plugins/modal/modal.js"; -import tplAddContactModal from "./templates/add-contact.js"; +import BaseModal from 'plugins/modal/modal.js'; +import tplAddContactModal from './templates/add-contact.js'; import { Strophe } from 'strophe.js'; import { __ } from 'i18n'; -import { _converse, api } from "@converse/headless"; -import {getNamesAutoCompleteList} from '@converse/headless/plugins/roster/utils.js'; +import { _converse, api } from '@converse/headless'; +import { getNamesAutoCompleteList } from '@converse/headless/plugins/roster/utils.js'; export default class AddContactModal extends BaseModal { - initialize () { super.initialize(); this.listenTo(this.model, 'change', () => this.render()); this.render(); - this.addEventListener('shown.bs.modal', () => this.querySelector('input[name="jid"]')?.focus(), false); + this.addEventListener( + 'shown.bs.modal', + () => /** @type {HTMLInputElement} */ (this.querySelector('input[name="jid"]'))?.focus(), + false + ); } renderModal () { return tplAddContactModal(this); } - getModalTitle () { // eslint-disable-line class-methods-use-this + getModalTitle () { + // eslint-disable-line class-methods-use-this return __('Add a Contact'); } validateSubmission (jid) { - if (!jid || jid.split('@').filter(s => !!s).length < 2) { + if (!jid || jid.split('@').filter((s) => !!s).length < 2) { this.model.set('error', __('Please enter a valid XMPP address')); return false; } else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) { @@ -47,8 +51,8 @@ export default class AddContactModal extends BaseModal { async addContactFromForm (ev) { ev.preventDefault(); const data = new FormData(ev.target); - let name = (/** @type {string} */(data.get('name')) || '').trim(); - let jid = (/** @type {string} */(data.get('jid')) || '').trim(); + let name = /** @type {string} */ (data.get('name') || '').trim(); + let jid = /** @type {string} */ (data.get('jid') || '').trim(); if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') { const list = await getNamesAutoCompleteList(name); diff --git a/src/plugins/rosterview/rosterview.js b/src/plugins/rosterview/rosterview.js index ad72929e1c..dfb67457d0 100644 --- a/src/plugins/rosterview/rosterview.js +++ b/src/plugins/rosterview/rosterview.js @@ -1,6 +1,6 @@ import tplRoster from "./templates/roster.js"; import { CustomElement } from 'shared/components/element.js'; -import { Model } from '@converse/skeletor/src/model.js'; +import { Model } from '@converse/skeletor'; import { _converse, api } from "@converse/headless"; import { initStorage } from '@converse/headless/utils/storage.js'; import { slideIn, slideOut } from 'utils/html.js'; @@ -63,7 +63,7 @@ export default class RosterView extends CustomElement { toggleRoster (ev) { ev?.preventDefault?.(); - const list_el = this.querySelector('.list-container.roster-contacts'); + const list_el = /** @type {HTMLElement} */(this.querySelector('.list-container.roster-contacts')); if (this.model.get('toggle_state') === _converse.CLOSED) { slideOut(list_el).then(() => this.model.save({'toggle_state': _converse.OPENED})); } else { diff --git a/src/plugins/rosterview/styles/roster.scss b/src/plugins/rosterview/styles/roster.scss index 207daff2da..f61344945c 100644 --- a/src/plugins/rosterview/styles/roster.scss +++ b/src/plugins/rosterview/styles/roster.scss @@ -39,29 +39,6 @@ } } - .roster-filter-form { - width: 100%; - - .button-group { - padding: 0.2em; - } - - converse-icon { - padding: 0.25em; - } - - .roster-filter { - width: 100%; - margin: 0.2em; - font-size: calc(var(--font-size) - 2px); - } - - .state-type { - font-size: calc(var(--font-size) - 2px); - width: 100%; - } - } - .roster-contacts { padding: 0; margin: 0 0 0.2em 0; diff --git a/src/plugins/rosterview/templates/roster.js b/src/plugins/rosterview/templates/roster.js index b7e6560836..03c2382bf3 100644 --- a/src/plugins/rosterview/templates/roster.js +++ b/src/plugins/rosterview/templates/roster.js @@ -1,4 +1,5 @@ import tplGroup from "./group.js"; +import tplRosterFilter from "./roster_filter.js"; import { __ } from 'i18n'; import { _converse, api } from "@converse/headless"; import { contactsComparator, groupsComparator } from '@converse/headless/plugins/roster/utils.js'; @@ -46,7 +47,13 @@ export default (el) => {
- el.requestUpdate()}> + el.requestUpdate()} + .promise=${api.waitUntil('rosterInitialized')} + .contacts=${_converse.roster} + .template=${tplRosterFilter} + .filter=${_converse.roster_filter}> + ${ repeat(groupnames, (n) => n, (name) => { const contacts = contacts_map[name].filter(c => shouldShowContact(c, name)); contacts.sort(contactsComparator); diff --git a/src/plugins/rosterview/templates/roster_filter.js b/src/plugins/rosterview/templates/roster_filter.js index 44e7ce88a5..0440ec674c 100644 --- a/src/plugins/rosterview/templates/roster_filter.js +++ b/src/plugins/rosterview/templates/roster_filter.js @@ -1,8 +1,10 @@ import { html } from "lit"; import { __ } from 'i18n'; - -export default (o) => { +/** + * @param {import('shared/components/contacts-filter').ContactsFilter} el + */ +export default (el) => { const i18n_placeholder = __('Filter'); const title_contact_filter = __('Filter by contact name'); const title_group_filter = __('Filter by group name'); @@ -16,34 +18,39 @@ export default (o) => { const label_xa = __('Extended Away'); const label_offline = __('Offline'); + const chat_state = el.filter.get('chat_state'); + const filter_text = el.filter.get('filter_text'); + const filter_type = el.filter.get('filter_type'); + return html` -
+ el.submitFilter(ev)}>
- - - + el.changeTypeFilter(ev)} class="fa fa-user clickable ${ (filter_type === 'contacts') ? 'selected' : '' }" data-type="contacts" title="${title_contact_filter}"> + el.changeTypeFilter(ev)} class="fa fa-users clickable ${ (filter_type === 'groups') ? 'selected' : '' }" data-type="groups" title="${title_group_filter}"> + el.changeTypeFilter(ev)} class="fa fa-circle clickable ${ (filter_type === 'state') ? 'selected' : '' }" data-type="state" title="${title_status_filter}">
- el.liveFilter(ev)} + class="contacts-filter form-control ${ (filter_type === 'state') ? 'hidden' : '' }" placeholder="${i18n_placeholder}"/> - + el.clearFilter(ev)}>
- el.changeChatStateFilter(ev)}> - - - - - - - + + + + + + +
` diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index 36dcf62590..9b065d3e16 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/src/plugins/rosterview/tests/roster.js @@ -223,7 +223,7 @@ describe("The Contacts Roster", function () { await mock.waitForRoster(_converse, 'current'); const rosterview = document.querySelector('converse-roster'); - const filter = rosterview.querySelector('.roster-filter'); + const filter = rosterview.querySelector('.contacts-filter'); const roster = rosterview.querySelector('.roster-contacts'); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800); @@ -275,7 +275,7 @@ describe("The Contacts Roster", function () { return el.isConnected && flyout.offsetHeight < panel.scrollHeight; } const rosterview = document.querySelector('converse-roster'); - const filter = rosterview.querySelector('.roster-filter'); + const filter = rosterview.querySelector('.contacts-filter'); const el = rosterview.querySelector('.roster-contacts'); await u.waitUntil(() => hasScrollBar(el) ? u.isVisible(filter) : !u.isVisible(filter), 900); })); @@ -288,7 +288,7 @@ describe("The Contacts Roster", function () { await mock.openControlBox(_converse); await mock.waitForRoster(_converse, 'current'); const rosterview = document.querySelector('converse-roster'); - let filter = rosterview.querySelector('.roster-filter'); + let filter = rosterview.querySelector('.contacts-filter'); const roster = rosterview.querySelector('.roster-contacts'); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); @@ -305,7 +305,7 @@ describe("The Contacts Roster", function () { const visible_group = sizzle('.roster-group', roster).filter(u.isVisible).pop(); expect(visible_group.querySelector('a.group-toggle').textContent.trim()).toBe('friends & acquaintences'); - filter = rosterview.querySelector('.roster-filter'); + filter = rosterview.querySelector('.contacts-filter'); filter.value = "j"; u.triggerEvent(filter, "keydown", "KeyboardEvent"); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 2), 700); @@ -318,14 +318,14 @@ describe("The Contacts Roster", function () { expect(visible_groups[0].textContent.trim()).toBe('friends & acquaintences'); expect(visible_groups[1].textContent.trim()).toBe('Ungrouped'); - filter = rosterview.querySelector('.roster-filter'); + filter = rosterview.querySelector('.contacts-filter'); filter.value = "xxx"; u.triggerEvent(filter, "keydown", "KeyboardEvent"); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 0), 600); visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); expect(visible_groups.length).toBe(0); - filter = rosterview.querySelector('.roster-filter'); + filter = rosterview.querySelector('.contacts-filter'); filter.value = ""; u.triggerEvent(filter, "keydown", "KeyboardEvent"); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); @@ -344,7 +344,7 @@ describe("The Contacts Roster", function () { await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(5); - let filter = rosterview.querySelector('.roster-filter'); + let filter = rosterview.querySelector('.contacts-filter'); filter.value = "colleagues"; u.triggerEvent(filter, "keydown", "KeyboardEvent"); @@ -354,13 +354,13 @@ describe("The Contacts Roster", function () { // Check that all contacts under the group are shown expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(l => !u.isVisible(l)).length).toBe(0); - filter = rosterview.querySelector('.roster-filter'); + filter = rosterview.querySelector('.contacts-filter'); filter.value = "xxx"; u.triggerEvent(filter, "keydown", "KeyboardEvent"); await u.waitUntil(() => (roster.querySelectorAll('.roster-group').length === 0), 700); - filter = rosterview.querySelector('.roster-filter'); + filter = rosterview.querySelector('.contacts-filter'); filter.value = ""; // Check that groups are shown again, when the filter string is cleared. u.triggerEvent(filter, "keydown", "KeyboardEvent"); await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 0), 700); @@ -374,16 +374,16 @@ describe("The Contacts Roster", function () { await mock.waitForRoster(_converse, 'current'); const rosterview = document.querySelector('converse-roster'); - const filter = rosterview.querySelector('.roster-filter'); + const filter = rosterview.querySelector('.contacts-filter'); filter.value = "xxx"; u.triggerEvent(filter, "keydown", "KeyboardEvent"); expect(_.includes(filter.classList, "x")).toBeFalsy(); - expect(u.hasClass('hidden', rosterview.querySelector('.roster-filter-form .clear-input'))).toBeTruthy(); + expect(u.hasClass('hidden', rosterview.querySelector('.contacts-filter-form .clear-input'))).toBeTruthy(); const isHidden = (el) => u.hasClass('hidden', el); - await u.waitUntil(() => !isHidden(rosterview.querySelector('.roster-filter-form .clear-input')), 900); + await u.waitUntil(() => !isHidden(rosterview.querySelector('.contacts-filter-form .clear-input')), 900); rosterview.querySelector('.clear-input').click(); - await u.waitUntil(() => document.querySelector('.roster-filter').value == ''); + await u.waitUntil(() => document.querySelector('.contacts-filter').value == ''); })); // Disabling for now, because since recently this test consistently diff --git a/src/shared/autocomplete/autocomplete.js b/src/shared/autocomplete/autocomplete.js index 79f6757ab0..883650c35e 100644 --- a/src/shared/autocomplete/autocomplete.js +++ b/src/shared/autocomplete/autocomplete.js @@ -1,51 +1,53 @@ /** * @copyright Lea Verou and the Converse.js contributors - * @description - * Started as a fork of Lea Verou's "Awesomplete" - * https://leaverou.github.io/awesomplete/ + * @description Started as a fork of Lea Verou's "Awesomplete" * @license Mozilla Public License (MPLv2) */ import Suggestion from './suggestion.js'; -import { Events } from '@converse/skeletor/src/events.js'; +import { EventEmitter } from '@converse/skeletor'; import { converse } from "@converse/headless"; import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js'; import { siblingIndex } from '@converse/headless/utils/html.js'; - const u = converse.env.utils; -export class AutoComplete { +export class AutoComplete extends EventEmitter(Object) { + /** + * @param {HTMLElement} el + * @param {any} config + */ constructor (el, config={}) { + super(); + this.suggestions = []; this.is_opened = false; + this.auto_evaluate = true; // evaluate automatically without any particular key as trigger + this.match_current_word = false; // Match only the current word, otherwise all input is matched + this.sort = config.sort === false ? null : SORT_BY_QUERY_POSITION; + this.filter = FILTER_CONTAINS; + this.ac_triggers = []; // Array of keys (`ev.key`) values that will trigger auto-complete + this.include_triggers = []; // Array of trigger keys which should be included in the returned value + this.min_chars = 2; + this.max_items = 10; + this.auto_first = false; // Should the first element be automatically selected? + this.data = (a, _v) => a; + this.item = ITEM; if (u.hasClass('suggestion-box', el)) { this.container = el; } else { this.container = el.querySelector('.suggestion-box'); } - this.input = this.container.querySelector('.suggestion-box__input'); + this.input = /** @type {HTMLInputElement} */(this.container.querySelector('.suggestion-box__input')); this.input.setAttribute("aria-autocomplete", "list"); this.ul = this.container.querySelector('.suggestion-box__results'); this.status = this.container.querySelector('.suggestion-box__additions'); - Object.assign(this, { - 'match_current_word': false, // Match only the current word, otherwise all input is matched - 'ac_triggers': [], // Array of keys (`ev.key`) values that will trigger auto-complete - 'include_triggers': [], // Array of trigger keys which should be included in the returned value - 'min_chars': 2, - 'max_items': 10, - 'auto_evaluate': true, // Should evaluation happen automatically without any particular key as trigger? - 'auto_first': false, // Should the first element be automatically selected? - 'data': a => a, - 'filter': FILTER_CONTAINS, - 'sort': config.sort === false ? false : SORT_BY_QUERY_POSITION, - 'item': ITEM - }, config); + Object.assign(this, config); this.index = -1; @@ -163,9 +165,13 @@ export class AutoComplete { this.goto(this.selected && pos !== -1 ? pos : count - 1); } + /** + * @param {number} i + * @param {boolean} scroll=true + */ goto (i, scroll=true) { // Should not be used directly, highlights specific item without any checks! - const list = this.ul.children; + const list = /** @type HTMLElement[] */(Array.from(this.ul.children).filter(el => el instanceof HTMLElement)); if (this.selected) { list[this.index].setAttribute("aria-selected", "false"); } @@ -214,7 +220,7 @@ export class AutoComplete { const li = u.ancestor(ev.target, 'li'); if (li) { ev.preventDefault(); - this.select(li, ev.target); + this.select(li); } } @@ -259,6 +265,9 @@ export class AutoComplete { } } + /** + * @param {KeyboardEvent} [ev] + */ async evaluate (ev) { const selecting = this.selected && ev && ( ev.keyCode === converse.keycodes.UP_ARROW || @@ -296,7 +305,7 @@ export class AutoComplete { .map(item => new Suggestion(this.data(item, value), value)) .filter(item => this.filter(item, value)); - if (this.sort !== false) { + if (this.sort) { this.suggestions = this.suggestions.sort(this.sort); } this.suggestions = this.suggestions.slice(0, this.max_items); @@ -316,7 +325,4 @@ export class AutoComplete { } } -// Make it an event emitter -Object.assign(AutoComplete.prototype, Events); - export default AutoComplete; diff --git a/src/shared/autocomplete/component.js b/src/shared/autocomplete/component.js index 449a33a5c8..2fb6360a02 100644 --- a/src/shared/autocomplete/component.js +++ b/src/shared/autocomplete/component.js @@ -76,6 +76,11 @@ export default class AutoCompleteComponent extends CustomElement { this.max_items = 10; this.min_chars = 1; this.triggers = ''; + this.getAutoCompleteList = null; + this.list = null; + this.name = ''; + this.placeholder = ''; + this.required = false; } render () { @@ -106,7 +111,7 @@ export default class AutoCompleteComponent extends CustomElement { } firstUpdated () { - this.auto_complete = new AutoComplete(this.firstElementChild, { + this.auto_complete = new AutoComplete(/** @type HTMLElement */(this.firstElementChild), { 'ac_triggers': this.triggers.split(' '), 'auto_evaluate': this.auto_evaluate, 'auto_first': this.auto_first, diff --git a/src/shared/autocomplete/suggestion.js b/src/shared/autocomplete/suggestion.js index 40bd132eb5..1f59eb94e5 100644 --- a/src/shared/autocomplete/suggestion.js +++ b/src/shared/autocomplete/suggestion.js @@ -3,7 +3,7 @@ */ class Suggestion extends String { /** - * @param { Any } data - The auto-complete data. Ideally an object e.g. { label, value }, + * @param { any } data - The auto-complete data. Ideally an object e.g. { label, value }, * which specifies the value and human-presentable label of the suggestion. * @param { string } query - The query string being auto-completed */ diff --git a/src/shared/avatar/avatar.js b/src/shared/avatar/avatar.js index 6075680c57..43b6c98735 100644 --- a/src/shared/avatar/avatar.js +++ b/src/shared/avatar/avatar.js @@ -18,6 +18,7 @@ export default class Avatar extends CustomElement { constructor () { super(); + this.data = null; this.width = 36; this.height = 36; } diff --git a/src/shared/chat/baseview.js b/src/shared/chat/baseview.js index d5cd4c8eeb..9c6bae1458 100644 --- a/src/shared/chat/baseview.js +++ b/src/shared/chat/baseview.js @@ -1,6 +1,10 @@ -import { CustomElement } from 'shared/components/element.js'; +/** + * @typedef {import('@converse/skeletor').Model} Model + */ +import { CustomElement } from '../components/element.js'; import { _converse, api } from '@converse/headless'; import { onScrolledDown } from './utils.js'; +import { CHATROOMS_TYPE, INACTIVE } from '@converse/headless/shared/constants.js'; export default class BaseChatView extends CustomElement { @@ -11,6 +15,12 @@ export default class BaseChatView extends CustomElement { } } + constructor () { + super(); + this.jid = /** @type {string} */ null; + this.model = /** @type {Model} */ null; + } + disconnectedCallback () { super.disconnectedCallback(); _converse.chatboxviews.remove(this.jid, this); @@ -38,7 +48,7 @@ export default class BaseChatView extends CustomElement { focus () { const textarea_el = this.getElementsByClassName('chat-textarea')[0]; if (textarea_el && document.activeElement !== textarea_el) { - textarea_el.focus(); + /** @type {HTMLTextAreaElement} */(textarea_el).focus(); } return this; } @@ -51,7 +61,7 @@ export default class BaseChatView extends CustomElement { /** * Triggered when the focus has been removed from a particular chat. * @event _converse#chatBoxBlurred - * @type { _converse.ChatBoxView | _converse.ChatRoomView } + * @type {BaseChatView} * @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... }); */ api.trigger('chatBoxBlurred', this, ev); @@ -65,14 +75,14 @@ export default class BaseChatView extends CustomElement { /** * Triggered when the focus has been moved to a particular chat. * @event _converse#chatBoxFocused - * @type { _converse.ChatBoxView | _converse.ChatRoomView } + * @type {BaseChatView} * @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... }); */ api.trigger('chatBoxFocused', this, ev); } getBottomPanel () { - if (this.model.get('type') === _converse.CHATROOMS_TYPE) { + if (this.model.get('type') === CHATROOMS_TYPE) { return this.querySelector('converse-muc-bottom-panel'); } else { return this.querySelector('converse-chat-bottom-panel'); @@ -80,7 +90,7 @@ export default class BaseChatView extends CustomElement { } getMessageForm () { - if (this.model.get('type') === _converse.CHATROOMS_TYPE) { + if (this.model.get('type') === CHATROOMS_TYPE) { return this.querySelector('converse-muc-message-form'); } else { return this.querySelector('converse-message-form'); @@ -103,14 +113,14 @@ export default class BaseChatView extends CustomElement { onScrolledDown(this.model); } - onWindowStateChanged (data) { - if (data.state === 'visible') { + onWindowStateChanged () { + if (document.hidden) { + this.model.setChatState(INACTIVE, { 'silent': true }); + this.model.sendChatState(); + } else { if (!this.model.isHidden()) { this.model.clearUnreadMsgCounter(); } - } else if (data.state === 'hidden') { - this.model.setChatState(_converse.INACTIVE, { 'silent': true }); - this.model.sendChatState(); } } } diff --git a/src/shared/chat/chat-content.js b/src/shared/chat/chat-content.js index 4372ce34ad..64501639f5 100644 --- a/src/shared/chat/chat-content.js +++ b/src/shared/chat/chat-content.js @@ -1,6 +1,6 @@ import './message-history'; import tplSpinner from "templates/spinner.js"; -import { CustomElement } from 'shared/components/element.js'; +import { CustomElement } from '../components/element.js'; import { api } from '@converse/headless'; import { html } from 'lit'; import { markScrolled } from './utils.js'; @@ -10,6 +10,11 @@ import './styles/chat-content.scss'; export default class ChatContent extends CustomElement { + constructor () { + super(); + this.jid = null; + } + static get properties () { return { jid: { type: String } diff --git a/src/shared/chat/emoji-dropdown.js b/src/shared/chat/emoji-dropdown.js index 950fbb2053..f56db2b065 100644 --- a/src/shared/chat/emoji-dropdown.js +++ b/src/shared/chat/emoji-dropdown.js @@ -4,6 +4,7 @@ import { _converse, api, converse } from "@converse/headless"; import { html } from "lit"; import { initStorage } from '@converse/headless/utils/storage.js'; import { until } from 'lit/directives/until.js'; +import { CHATROOMS_TYPE } from "@converse/headless/shared/constants.js"; const u = converse.env.utils; @@ -12,7 +13,9 @@ export default class EmojiDropdown extends DropdownBase { static get properties() { return { - chatview: { type: Object } + chatview: { type: Object }, + icon_classes: { type: String }, + items: { type: Array } }; } @@ -20,6 +23,7 @@ export default class EmojiDropdown extends DropdownBase { super(); // This is an optimization, we lazily render the emoji picker, otherwise tests slow to a crawl. this.render_emojis = false; + this.chatview = null; } initModel () { @@ -38,7 +42,7 @@ export default class EmojiDropdown extends DropdownBase { } render() { - const is_groupchat = this.chatview.model.get('type') === _converse.CHATROOMS_TYPE; + const is_groupchat = this.chatview.model.get('type') === CHATROOMS_TYPE; const color = is_groupchat ? '--muc-toolbar-btn-color' : '--chat-toolbar-btn-color'; return html`
@@ -94,7 +98,7 @@ export default class EmojiDropdown extends DropdownBase { await this.updateComplete; } super.showMenu(); - setTimeout(() => this.querySelector('.emoji-search')?.focus()); + setTimeout(() => /** @type {HTMLInputElement} */(this.querySelector('.emoji-search'))?.focus()); } } diff --git a/src/shared/chat/emoji-picker-content.js b/src/shared/chat/emoji-picker-content.js index 9078abd2f9..f842971620 100644 --- a/src/shared/chat/emoji-picker-content.js +++ b/src/shared/chat/emoji-picker-content.js @@ -1,105 +1,110 @@ +/** + * @typedef {module:emoji-picker.EmojiPicker} EmojiPicker + */ import { CustomElement } from 'shared/components/element.js'; -import { _converse, converse, api } from "@converse/headless"; -import { html } from "lit"; -import { tplAllEmojis, tplSearchResults } from "./templates/emoji-picker.js"; +import { _converse, converse, api } from '@converse/headless'; +import { html } from 'lit'; +import { tplAllEmojis, tplSearchResults } from './templates/emoji-picker.js'; import { getTonedEmojis } from './utils.js'; const { sizzle } = converse.env; - export default class EmojiPickerContent extends CustomElement { - static get properties () { - return { - 'chatview': { type: Object }, - 'search_results': { type: Array }, - 'current_skintone': { type: String }, - 'model': { type: Object }, - 'query': { type: String }, - } - } + static get properties () { + return { + 'chatview': { type: Object }, + 'search_results': { type: Array }, + 'current_skintone': { type: String }, + 'model': { type: Object }, + 'query': { type: String }, + }; + } + + constructor () { + super(); + this.model = null; + this.current_skintone = null; + this.query = null; + this.search_results = null; + } - render () { - const props = { - 'current_skintone': this.current_skintone, - 'insertEmoji': ev => this.insertEmoji(ev), - 'query': this.query, - 'search_results': this.search_results, - 'shouldBeHidden': shortname => this.shouldBeHidden(shortname), - } - return html` -
- ${tplSearchResults(props)} - ${tplAllEmojis(props)} -
- `; - } + render () { + const props = { + 'current_skintone': this.current_skintone, + 'insertEmoji': (ev) => this.insertEmoji(ev), + 'query': this.query, + 'search_results': this.search_results, + 'shouldBeHidden': (shortname) => this.shouldBeHidden(shortname), + }; + return html`
${tplSearchResults(props)} ${tplAllEmojis(props)}
`; + } - firstUpdated () { - this.initIntersectionObserver(); - } + firstUpdated () { + this.initIntersectionObserver(); + } - initIntersectionObserver () { - if (!window.IntersectionObserver) { - return; - } - if (this.observer) { - this.observer.disconnect(); - } else { - const options = { - root: this.querySelector('.emoji-picker__lists'), - threshold: [0.1] - } - const handler = ev => this.setCategoryOnVisibilityChange(ev); - this.observer = new IntersectionObserver(handler, options); - } - sizzle('.emoji-picker', this).forEach(a => this.observer.observe(a)); - } + initIntersectionObserver () { + if (!window.IntersectionObserver) { + return; + } + if (this.observer) { + this.observer.disconnect(); + } else { + const options = { + root: this.querySelector('.emoji-picker__lists'), + threshold: [0.1], + }; + const handler = (ev) => this.setCategoryOnVisibilityChange(ev); + this.observer = new IntersectionObserver(handler, options); + } + sizzle('.emoji-picker', this).forEach((a) => this.observer.observe(a)); + } - setCategoryOnVisibilityChange (entries) { - const selected = this.parentElement.navigator.selected; - const intersection_with_selected = entries.filter(i => i.target.contains(selected)).pop(); - let current; - // Choose the intersection that contains the currently selected - // element, or otherwise the one with the largest ratio. - if (intersection_with_selected) { - current = intersection_with_selected; - } else { - current = entries.reduce((p, c) => c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p, null); - } - if (current && current.isIntersecting) { - const category = current.target.getAttribute('data-category'); - if (category !== this.model.get('current_category')) { - this.parentElement.preserve_scroll = true; - this.model.save({'current_category': category}); - } - } - } + setCategoryOnVisibilityChange (entries) { + const selected = /** @type {EmojiPicker} */(this.parentElement).navigator.selected; + const intersection_with_selected = entries.filter((i) => i.target.contains(selected)).pop(); + let current; + // Choose the intersection that contains the currently selected + // element, or otherwise the one with the largest ratio. + if (intersection_with_selected) { + current = intersection_with_selected; + } else { + current = entries.reduce((p, c) => (c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p), null); + } + if (current && current.isIntersecting) { + const category = current.target.getAttribute('data-category'); + if (category !== this.model.get('current_category')) { + /** @type {EmojiPicker} */(this.parentElement).preserve_scroll = true; + this.model.save({ 'current_category': category }); + } + } + } - insertEmoji (ev) { - ev.preventDefault(); - ev.stopPropagation(); - const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; - this.parentElement.insertIntoTextArea(target.getAttribute('data-emoji')); - } + insertEmoji (ev) { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; + /** @type EmojiPicker */(this.parentElement).insertIntoTextArea(target.getAttribute('data-emoji')); + } - shouldBeHidden (shortname) { - // Helper method for the template which decides whether an - // emoji should be hidden, based on which skin tone is - // currently being applied. - if (shortname.includes('_tone')) { - if (!this.current_skintone || !shortname.includes(this.current_skintone)) { - return true; - } - } else { - if (this.current_skintone && getTonedEmojis().includes(shortname)) { - return true; - } - } - if (this.query && !_converse.FILTER_CONTAINS(shortname, this.query)) { - return true; - } - return false; - } + shouldBeHidden (shortname) { + // Helper method for the template which decides whether an + // emoji should be hidden, based on which skin tone is + // currently being applied. + if (shortname.includes('_tone')) { + if (!this.current_skintone || !shortname.includes(this.current_skintone)) { + return true; + } + } else { + if (this.current_skintone && getTonedEmojis().includes(shortname)) { + return true; + } + } + if (this.query && !_converse.FILTER_CONTAINS(shortname, this.query)) { + return true; + } + return false; + } } api.elements.define('converse-emoji-picker-content', EmojiPickerContent); diff --git a/src/shared/chat/emoji-picker.js b/src/shared/chat/emoji-picker.js index cb4cb81f69..69f48858d0 100644 --- a/src/shared/chat/emoji-picker.js +++ b/src/shared/chat/emoji-picker.js @@ -1,3 +1,7 @@ +/** + * @module emoji-picker + * @typedef {module:dom-navigator.DOMNavigatorOptions} DOMNavigatorOptions + */ import "./emoji-picker-content.js"; import './emoji-dropdown.js'; import DOMNavigator from "shared/dom-navigator"; @@ -27,14 +31,17 @@ export default class EmojiPicker extends CustomElement { } } - firstUpdated () { - super.firstUpdated(); + firstUpdated (changed) { + super.firstUpdated(changed); this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed)); this.initArrowNavigation(); } constructor () { super(); + this.render_emojis = null; + this.chatview = null; + this.model = null; this.query = ''; this._search_results = []; this.debouncedFilter = debounce(input => this.model.set({'query': input.value}), 250); @@ -85,7 +92,7 @@ export default class EmojiPicker extends CustomElement { } const el = this.querySelector('.emoji-lists__container--browse'); const heading = this.querySelector(`#emoji-picker-${this.current_category}`); - if (heading) { + if (heading instanceof HTMLElement) { // +4 due to 2px padding on list elements el.scrollTop = heading.offsetTop - heading.offsetHeight*3 + 4; } @@ -232,7 +239,7 @@ export default class EmojiPicker extends CustomElement { initArrowNavigation () { if (!this.navigator) { const default_selector = 'li:not(.hidden):not(.emoji-skintone), .emoji-search'; - const options = { + const options = /** @type DOMNavigatorOptions */({ 'jump_to_picked': '.emoji-category', 'jump_to_picked_selector': '.emoji-category.picked', 'jump_to_picked_direction': DOMNavigator.DIRECTION.down, @@ -251,7 +258,7 @@ export default class EmojiPicker extends CustomElement { el.matches('.insert-emoji, .emoji-category') && el.firstElementChild.focus(); el.matches('.emoji-search') && el.focus(); } - }; + }); this.navigator = new DOMNavigator(this, options); } } diff --git a/src/shared/chat/help-messages.js b/src/shared/chat/help-messages.js index a9f373bb74..69773852f6 100644 --- a/src/shared/chat/help-messages.js +++ b/src/shared/chat/help-messages.js @@ -8,6 +8,13 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; export default class ChatHelp extends CustomElement { + constructor () { + super(); + this.messages = []; + this.model = null; + this.type = null; + } + static get properties () { return { chat_type: { type: String }, diff --git a/src/shared/chat/message-actions.js b/src/shared/chat/message-actions.js index 055848ce22..4abe8b5dff 100644 --- a/src/shared/chat/message-actions.js +++ b/src/shared/chat/message-actions.js @@ -1,14 +1,25 @@ import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; -import { _converse, api, converse, log } from '@converse/headless'; +import { api, converse, log } from '@converse/headless'; import { getAppSettings } from '@converse/headless/shared/settings/utils.js'; import { getMediaURLs } from '@converse/headless/shared/chat/utils.js'; +import { CHATROOMS_TYPE } from '@converse/headless/shared/constants'; import { html } from 'lit'; import { isMediaURLDomainAllowed, isDomainWhitelisted } from '@converse/headless/utils/url.js'; import { until } from 'lit/directives/until.js'; import './styles/message-actions.scss'; +/** + * @typedef { Object } MessageActionAttributes + * An object which represents a message action (as shown in the message dropdown); + * @property { String } i18n_text + * @property { Function } handler + * @property { String } button_class + * @property { String } icon_class + * @property { String } name + */ + const { Strophe, u } = converse.env; class MessageActions extends CustomElement { @@ -19,6 +30,12 @@ class MessageActions extends CustomElement { }; } + constructor () { + super(); + this.model = null; + this.is_retracted = null; + } + initialize () { const settings = getAppSettings(); this.listenTo(settings, 'change:allowed_audio_domains', () => this.requestUpdate()); @@ -104,9 +121,7 @@ class MessageActions extends CustomElement { /** * Retract someone else's message in this groupchat. - * @private - * @param { _converse.Message } message - The message which we're retracting. - * @param { string } [reason] - The reason for retracting the message. + * @param {string} [reason] - The reason for retracting the message. */ async retractOtherMessage (reason) { const chatbox = this.model.collection.chatbox; @@ -166,7 +181,7 @@ class MessageActions extends CustomElement { onMessageRetractButtonClicked (ev) { ev?.preventDefault?.(); const chatbox = this.model.collection.chatbox; - if (chatbox.get('type') === _converse.CHATROOMS_TYPE) { + if (chatbox.get('type') === CHATROOMS_TYPE) { this.onMUCMessageRetractButtonClicked(); } else { this.onDirectMessageRetractButtonClicked(); @@ -258,22 +273,13 @@ class MessageActions extends CustomElement { async getActionButtons () { const buttons = []; if (this.model.get('editable')) { - /** - * @typedef { Object } MessageActionAttributes - * An object which represents a message action (as shown in the message dropdown); - * @property { String } i18n_text - * @property { Function } handler - * @property { String } button_class - * @property { String } icon_class - * @property { String } name - */ - buttons.push({ + buttons.push(/** @type {MessageActionAttributes} */({ 'i18n_text': this.model.get('correcting') ? __('Cancel Editing') : __('Edit'), 'handler': ev => this.onMessageEditButtonClicked(ev), 'button_class': 'chat-msg__action-edit', 'icon_class': 'fa fa-pencil-alt', 'name': 'edit', - }); + })); } const may_be_moderated = ['groupchat', 'mep'].includes(this.model.get('type')) && diff --git a/src/shared/chat/message-body.js b/src/shared/chat/message-body.js index fb953f7e99..c2ddf6a008 100644 --- a/src/shared/chat/message-body.js +++ b/src/shared/chat/message-body.js @@ -1,5 +1,4 @@ import 'shared/registry.js'; -import ImageModal from 'shared/modals/image.js'; import renderRichText from 'shared/directives/rich-text.js'; import { CustomElement } from 'shared/components/element.js'; import { api } from "@converse/headless"; @@ -21,6 +20,13 @@ export default class MessageBody extends CustomElement { } } + constructor () { + super(); + this.text = null; + this.model = null; + this.hide_url_previews = null; + } + initialize () { const settings = getAppSettings(); this.listenTo(settings, 'change:allowed_audio_domains', () => this.requestUpdate()); diff --git a/src/shared/chat/message-history.js b/src/shared/chat/message-history.js index 3db922cfa7..be95a6a1af 100644 --- a/src/shared/chat/message-history.js +++ b/src/shared/chat/message-history.js @@ -9,6 +9,12 @@ import { until } from 'lit/directives/until.js'; export default class MessageHistory extends CustomElement { + constructor () { + super(); + this.model = null; + this.messages = []; + } + static get properties () { return { model: { type: Object }, diff --git a/src/shared/chat/message-limit.js b/src/shared/chat/message-limit.js index 0c5d708762..cb8e1b9389 100644 --- a/src/shared/chat/message-limit.js +++ b/src/shared/chat/message-limit.js @@ -4,6 +4,11 @@ import { api } from '@converse/headless'; export default class MessageLimitIndicator extends CustomElement { + constructor () { + super(); + this.model = null; + } + static get properties () { return { model: { type: Object } diff --git a/src/shared/chat/message.js b/src/shared/chat/message.js index f6a77167d9..4ddcc96bdb 100644 --- a/src/shared/chat/message.js +++ b/src/shared/chat/message.js @@ -23,6 +23,12 @@ const { Strophe, dayjs } = converse.env; export default class Message extends CustomElement { + constructor () { + super(); + this.jid = null; + this.mid = null; + } + static get properties () { return { jid: { type: String }, diff --git a/src/shared/chat/styles/message-body.scss b/src/shared/chat/styles/message-body.scss index 4fb0d83a4b..5ff3b152be 100644 --- a/src/shared/chat/styles/message-body.scss +++ b/src/shared/chat/styles/message-body.scss @@ -8,18 +8,7 @@ converse-chat-message-body { audio { display: block; - @include media-breakpoint-down(sm) { - max-width: 95%; - } - @include media-breakpoint-up(md) { - max-width: 70%; - } - @include media-breakpoint-up(lg) { - max-width: 50%; - } - @include media-breakpoint-up(xl) { - max-width: 40%; - } + max-width: 95%; } video { diff --git a/src/shared/chat/toolbar.js b/src/shared/chat/toolbar.js index 6767c3122d..46a1e35463 100644 --- a/src/shared/chat/toolbar.js +++ b/src/shared/chat/toolbar.js @@ -27,6 +27,16 @@ export class ChatToolbar extends CustomElement { } } + constructor () { + super(); + this.model = null; + this.is_groupchat = null; + this.hidden_occupants = null; + this.show_spoiler_button = null; + this.show_call_button = null; + this.show_emoji_button = null; + } + connectedCallback () { super.connectedCallback(); this.listenTo(this.model, 'change:composing_spoiler', () => this.requestUpdate()); @@ -163,11 +173,14 @@ export class ChatToolbar extends CustomElement { toggleFileUpload (ev) { ev?.preventDefault?.(); ev?.stopPropagation?.(); - this.querySelector('.fileupload').click(); + /** @type {HTMLInputElement} */(this.querySelector('.fileupload')).click(); } + /** + * @param {InputEvent} evt + */ onFileSelection (evt) { - this.model.sendFiles(evt.target.files); + this.model.sendFiles(/** @type {HTMLInputElement} */(evt.target).files); } toggleComposeSpoilerMessage (ev) { diff --git a/src/shared/chat/unfurl.js b/src/shared/chat/unfurl.js index 4db15feeea..75e1aecb63 100644 --- a/src/shared/chat/unfurl.js +++ b/src/shared/chat/unfurl.js @@ -18,6 +18,15 @@ export default class MessageUnfurl extends CustomElement { } } + constructor () { + super(); + this.jid = null; + this.url = null; + this.title = null; + this.image = null; + this.description = null; + } + initialize () { const settings = getAppSettings(); this.listenTo(settings, 'change:allowed_image_domains', () => this.requestUpdate()); diff --git a/src/shared/chat/utils.js b/src/shared/chat/utils.js index 95b4c86ed0..613bdd7f47 100644 --- a/src/shared/chat/utils.js +++ b/src/shared/chat/utils.js @@ -1,6 +1,9 @@ +/** + * @typedef {import('../../headless/plugins/chat/message.js').default} Message + */ import debounce from 'lodash-es/debounce'; import tplNewDay from "./templates/new-day.js"; -import { _converse, api, converse } from '@converse/headless'; +import { api, converse } from '@converse/headless'; import { html } from 'lit'; import { until } from 'lit/directives/until.js'; import { @@ -118,7 +121,7 @@ export const markScrolled = debounce((ev) => _markScrolled(ev), 50); /** * Given a message object, returns a TemplateResult indicating a new day if * the passed in message is more than a day later than its predecessor. - * @param { _converse.Message } + * @param {Message} message */ export function getDayIndicator (message) { const messages = message.collection?.models; @@ -172,6 +175,14 @@ export function getTonedEmojis () { return converse.emojis.toned; } +/** + * @typedef {object} EmojiMarkupOptions + * @property {boolean} [unicode_only=false] + * @property {boolean} [add_title_wrapper=false] + * + * @param {object} data + * @param {EmojiMarkupOptions} options + */ export function getEmojiMarkup (data, options={unicode_only: false, add_title_wrapper: false}) { const emoji = data.emoji; const shortname = data.shortname; diff --git a/src/shared/components/contacts-filter.js b/src/shared/components/contacts-filter.js new file mode 100644 index 0000000000..51d004a3e7 --- /dev/null +++ b/src/shared/components/contacts-filter.js @@ -0,0 +1,112 @@ +import debounce from "lodash-es/debounce"; +import { CustomElement } from 'shared/components/element.js'; +import { api } from "@converse/headless"; + +import './styles/contacts-filter.scss'; + + +export class ContactsFilter extends CustomElement { + + constructor () { + super(); + this.contacts = null; + this.filter = null; + this.template = null; + this.promise = Promise.resolve(); + } + + static get properties () { + return { + contacts: { type: Array }, + filter: { type: Object }, + promise: { type: Promise }, + template: { type: Object }, + } + } + + initialize () { + this.liveFilter = debounce((ev) => this.filter.save({'filter_text': ev.target.value}), 250); + + this.listenTo(this.contacts, "add", () => this.requestUpdate()); + this.listenTo(this.contacts, "destroy", () => this.requestUpdate()); + this.listenTo(this.contacts, "remove", () => this.requestUpdate()); + + this.listenTo(this.filter, 'change', () => { + this.dispatchUpdateEvent(); + this.requestUpdate(); + }); + + this.promise.then(() => this.requestUpdate()); + this.requestUpdate(); + } + + render () { + return this.shouldBeVisible() ? this.template(this) : ''; + } + + dispatchUpdateEvent () { + this.dispatchEvent(new CustomEvent('update', { 'detail': this.filter.changed })); + } + + /** + * @param {Event} ev + */ + changeChatStateFilter (ev) { + ev && ev.preventDefault(); + this.filter.save({'chat_state': /** @type {HTMLInputElement} */(this.querySelector('.state-type')).value}); + } + + /** + * @param {Event} ev + */ + changeTypeFilter (ev) { + ev && ev.preventDefault(); + const target = /** @type {HTMLInputElement} */(ev.target); + const type = /** @type {HTMLElement} */(target.closest('converse-icon'))?.dataset.type || 'contacts'; + if (type === 'state') { + this.filter.save({ + 'filter_type': type, + 'chat_state': /** @type {HTMLInputElement} */(this.querySelector('.state-type')).value + }); + } else { + this.filter.save({ + 'filter_type': type, + 'filter_text': /** @type {HTMLInputElement} */(this.querySelector('.contacts-filter')).value + }); + } + } + + /** + * @param {Event} ev + */ + submitFilter (ev) { + ev && ev.preventDefault(); + this.liveFilter(); + } + + /** + * Returns true if the filter is enabled (i.e. if the user + * has added values to the filter). + * @returns {boolean} + */ + isActive () { + return (this.filter.get('filter_type') === 'state' || this.filter.get('filter_text')); + } + + /** + * @returns {boolean} + */ + shouldBeVisible () { + return this.contacts?.length >= 5 || this.isActive(); + } + + /** + * @param {Event} ev + */ + clearFilter (ev) { + ev && ev.preventDefault(); + this.filter.save({'filter_text': ''}); + } +} + +api.elements.define('converse-contacts-filter', ContactsFilter); diff --git a/src/shared/components/dropdown.js b/src/shared/components/dropdown.js index c6d89894ce..7586b6ea8c 100644 --- a/src/shared/components/dropdown.js +++ b/src/shared/components/dropdown.js @@ -1,3 +1,6 @@ +/** + * @typedef {module:dom-navigator.DOMNavigatorOptions} DOMNavigatorOptions + */ import 'shared/components/icons.js'; import DOMNavigator from "shared/dom-navigator.js"; import DropdownBase from 'shared/components/dropdownbase.js'; @@ -13,14 +16,15 @@ export default class Dropdown extends DropdownBase { static get properties () { return { - 'icon_classes': { type: String }, - 'items': { type: Array } + icon_classes: { type: String }, + items: { type: Array } } } constructor () { super(); this.icon_classes = 'fa fa-bars'; + this.items = []; } render () { @@ -57,11 +61,11 @@ export default class Dropdown extends DropdownBase { initArrowNavigation () { if (!this.navigator) { - const options = { + const options = /** @type DOMNavigatorOptions */({ 'selector': '.dropdown-item', 'onSelected': el => el.focus() - }; - this.navigator = new DOMNavigator(this.menu, options); + }); + this.navigator = new DOMNavigator(/** @type HTMLElement */(this.menu), options); } } @@ -71,7 +75,7 @@ export default class Dropdown extends DropdownBase { ev.stopPropagation(); } this.navigator.enable(); - this.navigator.select(this.menu.firstElementChild); + this.navigator.select(/** @type HTMLElement */(this.menu.firstElementChild)); } handleKeyUp (ev) { diff --git a/src/shared/components/dropdownbase.js b/src/shared/components/dropdownbase.js index 073dd63425..f8816911b4 100644 --- a/src/shared/components/dropdownbase.js +++ b/src/shared/components/dropdownbase.js @@ -1,44 +1,42 @@ import { CustomElement } from './element.js'; -import { converse } from "@converse/headless"; +import { converse } from '@converse/headless'; const u = converse.env.utils; - export default class DropdownBase extends CustomElement { - - connectedCallback() { + connectedCallback () { super.connectedCallback(); this.registerEvents(); } - registerEvents() { + registerEvents () { this.clickOutside = (ev) => this._clickOutside(ev); document.addEventListener('click', this.clickOutside); } - firstUpdated () { - super.firstUpdated(); + firstUpdated (changed) { + super.firstUpdated(changed); this.menu = this.querySelector('.dropdown-menu'); this.button = this.querySelector('button'); - this.addEventListener('click', ev => this.toggleMenu(ev)); - this.addEventListener('keyup', ev => this.handleKeyUp(ev)); + this.addEventListener('click', (ev) => this.toggleMenu(ev)); + this.addEventListener('keyup', (ev) => this.handleKeyUp(ev)); } - _clickOutside(ev) { + _clickOutside (ev) { if (!this.contains(ev.composedPath()[0])) { - this.hideMenu(ev); + this.hideMenu(); } } hideMenu () { u.removeClass('show', this.menu); - this.button?.setAttribute('aria-expanded', false); + this.button?.setAttribute('aria-expanded', 'false'); this.button?.blur(); } showMenu () { u.addClass('show', this.menu); - this.button.setAttribute('aria-expanded', true); + this.button.setAttribute('aria-expanded', 'true'); } toggleMenu (ev) { diff --git a/src/shared/components/element.js b/src/shared/components/element.js index 719ce538f1..835bfce43f 100644 --- a/src/shared/components/element.js +++ b/src/shared/components/element.js @@ -1,8 +1,8 @@ +import { EventEmitter } from '@converse/skeletor'; import { LitElement } from 'lit'; -import { Events } from '@converse/skeletor/src/events.js'; -export class CustomElement extends LitElement { +export class CustomElement extends EventEmitter(LitElement) { createRenderRoot () { // Render without the shadow DOM @@ -23,5 +23,3 @@ export class CustomElement extends LitElement { this.stopListening(); } } - -Object.assign(CustomElement.prototype, Events); diff --git a/src/shared/components/font-awesome.js b/src/shared/components/font-awesome.js index 856ec9e4b3..04beaec0c8 100644 --- a/src/shared/components/font-awesome.js +++ b/src/shared/components/font-awesome.js @@ -1,9 +1,9 @@ -import tplIcons from '../templates/icons.js'; +import tplIcons from './templates/icons.js'; import { CustomElement } from './element.js'; import { api } from '@converse/headless'; export class FontAwesome extends CustomElement { - render () { // eslint-disable-line class-methods-use-this + render () { return tplIcons(); } } diff --git a/src/shared/components/gif.js b/src/shared/components/gif.js index 8a289d29d3..89c285142e 100644 --- a/src/shared/components/gif.js +++ b/src/shared/components/gif.js @@ -32,6 +32,7 @@ export default class ConverseGIFElement extends CustomElement { this.autoplay = false; this.noloop = false; this.fallback = 'url'; + this.progress_color = null; } initGIF () { diff --git a/src/shared/components/icons.js b/src/shared/components/icons.js index 2c90566c1e..67a4b71905 100644 --- a/src/shared/components/icons.js +++ b/src/shared/components/icons.js @@ -5,7 +5,6 @@ * https://github.com/obsidiansoft-io/fa-icons/blob/master/LICENSE * @license Mozilla Public License (MPLv2) */ - import { CustomElement } from './element.js'; import { api } from '@converse/headless'; import { html } from 'lit'; @@ -17,17 +16,17 @@ class ConverseIcon extends CustomElement { static get properties () { return { - color: String, + color: { type: String }, class_name: { attribute: "class" }, - style: String, - size: String + css: { type: String }, + size: { type: String } }; } constructor () { super(); this.class_name = ""; - this.style = ""; + this.css = ""; this.size = ""; this.color = ""; } @@ -43,7 +42,7 @@ class ConverseIcon extends CustomElement { ${this.size ? `width: ${this.size};` : ''} ${this.size ? `height: ${this.size};` : ''} ${color ? `fill: ${color};` : ''} - ${this.style} + ${this.css} `; } diff --git a/src/shared/components/image-picker.js b/src/shared/components/image-picker.js index 736b6083f7..35e673109d 100644 --- a/src/shared/components/image-picker.js +++ b/src/shared/components/image-picker.js @@ -8,6 +8,12 @@ const i18n_profile_picture = __('Your profile picture'); export default class ImagePicker extends CustomElement { + constructor () { + super(); + this.width = null; + this.height = null; + } + static get properties () { return { 'height': { type: Number }, @@ -27,7 +33,7 @@ export default class ImagePicker extends CustomElement { openFileSelection (ev) { ev.preventDefault(); - this.querySelector('input[type="file"]').click(); + /** @type {HTMLInputElement} */(this.querySelector('input[type="file"]')).click(); } updateFilePreview (ev) { diff --git a/src/shared/components/rich-text.js b/src/shared/components/rich-text.js index 4351a83177..5be940d024 100644 --- a/src/shared/components/rich-text.js +++ b/src/shared/components/rich-text.js @@ -52,6 +52,10 @@ export default class RichText extends CustomElement { constructor () { super(); + this.nick = null; + this.onImgClick = null; + this.onImgLoad = null; + this.text = null; this.embed_audio = false; this.embed_videos = false; this.hide_media_urls = false; diff --git a/src/shared/components/styles/contacts-filter.scss b/src/shared/components/styles/contacts-filter.scss new file mode 100644 index 0000000000..884bdce026 --- /dev/null +++ b/src/shared/components/styles/contacts-filter.scss @@ -0,0 +1,54 @@ +converse-contacts-filter { + display: block; + margin-bottom: 1em; + + .contacts-filter-form { + width: 100%; + + .button-group { + padding: 0.2em; + } + + converse-icon { + padding: 0.25em; + } + + .contacts-filter { + width: 100%; + margin: 0.2em; + font-size: calc(var(--font-size) - 2px); + + &.form-control { + width: 100%; + } + } + + select.state-type { + font-size: calc(var(--font-size) - 2px); + width: 100% !important; + } + } +} + +.converse-overlayed { + .contacts-filter-form { + .button-group { + padding: 0.1em; + } + + converse-icon { + padding: 0.15em; + } + + .contacts-filter { + width: 100%; + margin: 0.1em; + font-size: calc(var(--font-size) - 4px); + } + + select.state-type { + font-size: calc(var(--font-size) - 4px); + width: 100% !important; + } + } +} diff --git a/src/shared/templates/icons.js b/src/shared/components/templates/icons.js similarity index 100% rename from src/shared/templates/icons.js rename to src/shared/components/templates/icons.js diff --git a/src/shared/directives/retraction.js b/src/shared/directives/retraction.js deleted file mode 100644 index 9d5d02cd85..0000000000 --- a/src/shared/directives/retraction.js +++ /dev/null @@ -1,27 +0,0 @@ -import { __ } from '../i18n'; -import { directive, html } from "lit"; - - -const i18n_retract_message = __('Retract this message'); -const tplRetract = (o) => html` - -`; - - -export const renderRetractionLink = directive(o => async part => { - const may_be_moderated = o.model.get('type') === 'groupchat' && await o.model.mayBeModerated(); - const retractable = !o.is_retracted && (o.model.mayBeRetracted() || may_be_moderated); - - if (retractable) { - part.setValue(tplRetract(o)); - } else { - part.setValue(''); - } - part.commit(); -}); diff --git a/src/shared/dom-navigator.js b/src/shared/dom-navigator.js index 526ac797b3..1b4af34a08 100644 --- a/src/shared/dom-navigator.js +++ b/src/shared/dom-navigator.js @@ -27,8 +27,8 @@ function inViewport(el) { /** * Return the absolute offset top of an element. - * @param el { Element } The element. - * @return { Number } The offset top. + * @param el {HTMLElement} The element. + * @return {Number} The offset top. */ function absoluteOffsetTop(el) { let offsetTop = 0; @@ -36,14 +36,14 @@ function absoluteOffsetTop(el) { if (!isNaN(el.offsetTop)) { offsetTop += el.offsetTop; } - } while ((el = el.offsetParent)); + } while ((el = /** @type {HTMLElement} */(el.offsetParent))); return offsetTop; } /** * Return the absolute offset left of an element. - * @param el { Element } The element. - * @return { Number } The offset left. + * @param el {HTMLElement} The element. + * @return {Number} The offset left. */ function absoluteOffsetLeft(el) { let offsetLeft = 0; @@ -51,10 +51,19 @@ function absoluteOffsetLeft(el) { if (!isNaN(el.offsetLeft)) { offsetLeft += el.offsetLeft; } - } while ((el = el.offsetParent)); + } while ((el = /** @type {HTMLElement} */(el.offsetParent))); return offsetLeft; } +/** + * @typedef {Object} DOMNavigatorDirection + * @property {string} DOMNavigatorOptions.down + * @property {string} DOMNavigatorOptions.end + * @property {string} DOMNavigatorOptions.home + * @property {string} DOMNavigatorOptions.left + * @property {string} DOMNavigatorOptions.right + * @property {string} DOMNavigatorOptions.up + */ /** * Adds the ability to navigate the DOM with the arrow keys @@ -63,33 +72,36 @@ function absoluteOffsetLeft(el) { class DOMNavigator { /** * Directions. - * @returns {{left: string, up: string, right: string, down: string}} + * @returns {DOMNavigatorDirection} * @constructor */ static get DIRECTION () { - return { + return ({ down: 'down', end: 'end', home: 'home', left: 'left', right: 'right', up: 'up' - }; + }); } /** * The default options for the DOM navigator. * @returns {{ - * down: number, + * home: string[], + * end: string[], + * down: number[], * getSelector: null, * jump_to_picked: null, * jump_to_picked_direction: null, * jump_to_picked_selector: string, - * left: number, + * left: number[], * onSelected: null, - * right: number, + * right: number[], * selected: string, - * up: number + * selector: string, + * up: number[] * }} */ static get DEFAULTS () { @@ -130,35 +142,45 @@ class DOMNavigator { } /** - * Create a new DOM Navigator. - * @param { Element } container The container of the element to navigate. - * @param { Object } options The options to configure the DOM navigator. - * @param { Function } options.getSelector - * @param { Number } [options.down] - The keycode for navigating down - * @param { Number } [options.left] - The keycode for navigating left - * @param { Number } [options.right] - The keycode for navigating right - * @param { Number } [options.up] - The keycode for navigating up - * @param { String } [options.selected] - The class that should be added to the currently selected DOM element. - * @param { String } [options.jump_to_picked] - A selector, which if + * @typedef {Object} DOMNavigatorOptions + * @property {Function} DOMNavigatorOptions.getSelector + * @property {string[]} [DOMNavigatorOptions.end] + * @property {string[]} [DOMNavigatorOptions.home] + * @property {number[]} [DOMNavigatorOptions.down] - The keycode for navigating down + * @property {number[]} [DOMNavigatorOptions.left] - The keycode for navigating left + * @property {number[]} [DOMNavigatorOptions.right] - The keycode for navigating right + * @property {number[]} [DOMNavigatorOptions.up] - The keycode for navigating up + * @property {String} [DOMNavigatorOptions.selector] + * @property {String} [DOMNavigatorOptions.selected] - The class that should be added to the currently selected DOM element. + * @property {String} [DOMNavigatorOptions.jump_to_picked] - A selector, which if * matched by the next element being navigated to, based on the direction * given by `jump_to_picked_direction`, will cause navigation * to jump to the element that matches the `jump_to_picked_selector`. * For example, this is useful when navigating to tabs. You want to * immediately navigate to the currently active tab instead of just * navigating to the first tab. - * @param { String } [options.jump_to_picked_selector=picked] - The selector + * @property {String} [DOMNavigatorOptions.jump_to_picked_selector=picked] - The selector * indicating the currently picked element to jump to. - * @param { String } [options.jump_to_picked_direction] - The direction for + * @property {String} [DOMNavigatorOptions.jump_to_picked_direction] - The direction for * which jumping to the picked element should be enabled. - * @param { Function } [options.onSelected] - The callback function which + * @property {Function} [DOMNavigatorOptions.onSelected] - The callback function which * should be called when en element gets selected. - * @constructor + * @property {HTMLElement} [DOMNavigatorOptions.scroll_container] + */ + + /** + * Create a new DOM Navigator. + * @param {HTMLElement} container The container of the element to navigate. + * @param {DOMNavigatorOptions} options The options to configure the DOM navigator. */ constructor (container, options) { this.doc = window.document; this.container = container; this.scroll_container = options.scroll_container || container; + + /** @type {DOMNavigatorOptions} */ this.options = Object.assign({}, DOMNavigator.DEFAULTS, options); + this.init(); } @@ -206,14 +228,11 @@ class DOMNavigator { */ destroy () { this.disable(); - if (this.container.domNavigator) { - delete this.container.domNavigator; - } } /** * @param {'down'|'right'|'left'|'up'} direction - * @returns { HTMLElement } + * @returns {HTMLElement} */ getNextElement (direction) { let el; @@ -263,8 +282,8 @@ class DOMNavigator { /** * Select the given element. - * @param { Element } el The DOM element to select. - * @param { string } [direction] The direction. + * @param {HTMLElement} el The DOM element to select. + * @param {string} [direction] The direction. */ select (el, direction) { if (!el || el === this.selected) { @@ -293,8 +312,8 @@ class DOMNavigator { /** * Scroll the container to an element. - * @param { HTMLElement } el The destination element. - * @param { String } direction The direction of the current navigation. + * @param {HTMLElement} el The destination element. + * @param {String} direction The direction of the current navigation. * @return void. */ scrollTo (el, direction) { @@ -339,8 +358,8 @@ class DOMNavigator { /** * Indicate if an element is in the container viewport. - * @param { HTMLElement } el The element to check. - * @return { Boolean } true if the given element is in the container viewport, otherwise false. + * @param {HTMLElement} el The element to check. + * @return {Boolean} true if the given element is in the container viewport, otherwise false. */ inScrollContainerViewport(el) { const container = this.scroll_container; @@ -376,9 +395,9 @@ class DOMNavigator { /** * Return an array of navigable elements after an offset. - * @param { number } left The left offset. - * @param { number } top The top offset. - * @return { Array } An array of elements. + * @param {number} left The left offset. + * @param {number} top The top offset. + * @return {Array} An array of elements. */ elementsAfter (left, top) { return this.getElements(DOMNavigator.DIRECTION.down).filter(el => el.offsetLeft >= left && el.offsetTop >= top); @@ -386,9 +405,9 @@ class DOMNavigator { /** * Return an array of navigable elements before an offset. - * @param { number } left The left offset. - * @param { number } top The top offset. - * @return { Array } An array of elements. + * @param {number} left The left offset. + * @param {number} top The top offset. + * @return {Array} An array of elements. */ elementsBefore (left, top) { return this.getElements(DOMNavigator.DIRECTION.up).filter(el => el.offsetLeft <= left && el.offsetTop <= top); @@ -396,7 +415,7 @@ class DOMNavigator { /** * Handle the key down event. - * @param { Event } event The event object. + * @param {KeyboardEvent} ev - The event object. */ handleKeydown (ev) { const keys = keycodes; @@ -404,7 +423,7 @@ class DOMNavigator { if (direction) { ev.preventDefault(); ev.stopPropagation(); - const next = this.getNextElement(direction, ev); + const next = this.getNextElement(direction); this.select(next, direction); } } diff --git a/src/shared/errors.js b/src/shared/errors.js new file mode 100644 index 0000000000..1d867d2113 --- /dev/null +++ b/src/shared/errors.js @@ -0,0 +1,23 @@ +export class IQError extends Error { + /** + * @param {string} message + * @param {Element} iq + */ + constructor (message, iq) { + super(message); + this.name = 'IQError'; + this.iq = iq; + } +} + +export class UserFacingError extends Error { + + /** + * @param {string} message + */ + constructor (message) { + super(message); + this.name = 'UserFacingError'; + this.user_facing = true; + } +} diff --git a/src/shared/modals/image.js b/src/shared/modals/image.js index 9e46e6fb4e..5b6ba33f96 100644 --- a/src/shared/modals/image.js +++ b/src/shared/modals/image.js @@ -10,6 +10,11 @@ import './styles/image.scss'; export default class ImageModal extends BaseModal { + constructor (options) { + super(options); + this.src = options.src; + } + renderModal () { return tplImageModal({ 'src': this.src }); } diff --git a/src/shared/modals/user-details.js b/src/shared/modals/user-details.js index 53c35b8c10..f74a0e0f6b 100644 --- a/src/shared/modals/user-details.js +++ b/src/shared/modals/user-details.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('headless/plugins/chat/model.js').default} ChatBox + */ import BaseModal from "plugins/modal/modal.js"; import { tplUserDetailsModal, tplFooter } from "./templates/user-details.js"; import { __ } from 'i18n'; @@ -17,7 +20,7 @@ export default class UserDetailsModal extends BaseModal { /** * Triggered once the UserDetailsModal has been initialized * @event _converse#userDetailsModalInitialized - * @type { _converse.ChatBox } + * @type {ChatBox} * @example _converse.api.listen.on('userDetailsModalInitialized', (chatbox) => { ... }); */ api.trigger('userDetailsModalInitialized', this.model); @@ -48,7 +51,7 @@ export default class UserDetailsModal extends BaseModal { async refreshContact (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } - const refresh_icon = this.el.querySelector('.fa-refresh'); + const refresh_icon = this.querySelector('.fa-refresh'); u.addClass('fa-spin', refresh_icon); try { await api.vcard.update(this.model.contact.vcard, true); diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js index b17b04331a..7b1de80b0b 100644 --- a/src/shared/rich-text.js +++ b/src/shared/rich-text.js @@ -5,7 +5,7 @@ import tplVideo from 'templates/video.js'; import { api } from '@converse/headless'; import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js'; import { getEmojiMarkup } from './chat/utils.js'; -import { getHyperlinkTemplate } from 'utils/html.js'; +import { getHyperlinkTemplate } from '../utils/html.js'; import { getMediaURLs } from '@converse/headless/shared/chat/utils.js'; import { getMediaURLsMetadata } from '@converse/headless/shared/parsers.js'; import { @@ -54,33 +54,34 @@ const tplMention = o => html`${o.menti export class RichText extends String { /** * Create a new {@link RichText} instance. - * @param { String } text - The text to be annotated - * @param { number } offset - The offset of this particular piece of text + * @param {string} text - The text to be annotated + * @param {number} offset - The offset of this particular piece of text * from the start of the original message text. This is necessary because * RichText instances can be nested when templates call directives * which create new RichText instances (as happens with XEP-393 styling directives). - * @param { Object } options - * @param { String } options.nick - The current user's nickname (only relevant if the message is in a XEP-0045 MUC) - * @param { Boolean } options.render_styling - Whether XEP-0393 message styling should be applied to the message - * @param { Boolean } [options.embed_audio] - Whether audio URLs should be rendered as