diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a8b98c626e3..e5a51e84462 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,6 +26,9 @@ updates: eslint: patterns: - '*eslint*' + datadog: + patterns: + - '*@datadog*' ignore: - dependency-name: '@wireapp/avs' diff --git a/.github/workflows/create_docker_image.yml b/.github/workflows/create_docker_image.yml index 26c11f54dfe..9a3573ae8ac 100644 --- a/.github/workflows/create_docker_image.yml +++ b/.github/workflows/create_docker_image.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: 'yarn' diff --git a/.github/workflows/deploy-to-test-env.yml b/.github/workflows/deploy-to-test-env.yml index 405920a336a..0dc92013697 100644 --- a/.github/workflows/deploy-to-test-env.yml +++ b/.github/workflows/deploy-to-test-env.yml @@ -36,7 +36,7 @@ jobs: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: 'yarn' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 96891a035ff..8fa9745e857 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: 'yarn' diff --git a/.github/workflows/semantic-commit-lint.yml b/.github/workflows/semantic-commit-lint.yml index fc4edfb9229..e3e2c694af1 100644 --- a/.github/workflows/semantic-commit-lint.yml +++ b/.github/workflows/semantic-commit-lint.yml @@ -14,7 +14,7 @@ jobs: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - name: Run Semantic Commint Linter - uses: amannn/action-semantic-pull-request@v5.3.0 + uses: amannn/action-semantic-pull-request@v5.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/sync_translations.yml b/.github/workflows/sync_translations.yml index df66e5fa8af..69f2292a52b 100644 --- a/.github/workflows/sync_translations.yml +++ b/.github/workflows/sync_translations.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: 'yarn' @@ -37,7 +37,7 @@ jobs: run: yarn translate:merge - name: Download translations - uses: crowdin/github-action@v1.13.1 + uses: crowdin/github-action@v1.14.1 env: GITHUB_TOKEN: ${{secrets.OTTO_THE_BOT_GH_TOKEN}} CROWDIN_PROJECT_ID: 342359 diff --git a/.github/workflows/test_build_deploy.yml b/.github/workflows/test_build_deploy.yml index 3e6b9129967..66ab07d472b 100644 --- a/.github/workflows/test_build_deploy.yml +++ b/.github/workflows/test_build_deploy.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: 'yarn' diff --git a/package.json b/package.json index c6614763af9..b300f06a4b7 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,15 @@ { "dependencies": { - "@datadog/browser-logs": "^4.50.1", - "@datadog/browser-rum": "^4.50.1", + "@datadog/browser-logs": "^5.1.0", + "@datadog/browser-rum": "^5.1.0", "@emotion/react": "11.11.1", "@lexical/history": "0.12.2", "@lexical/react": "0.12.2", "@peculiar/x509": "1.9.5", "@wireapp/avs": "9.5.2", - "@wireapp/commons": "5.2.1", - "@wireapp/core": "42.17.0", - "@wireapp/lru-cache": "3.8.1", - "@wireapp/react-ui-kit": "9.9.11", + "@wireapp/commons": "5.2.2", + "@wireapp/core": "42.19.2", + "@wireapp/react-ui-kit": "9.9.12", "@wireapp/store-engine-dexie": "2.1.6", "@wireapp/store-engine-sqleet": "1.8.9", "@wireapp/webapp-events": "0.18.3", @@ -18,11 +17,11 @@ "beautiful-react-hooks": "^5.0.0", "classnames": "2.3.2", "copy-webpack-plugin": "11.0.0", - "core-js": "3.33.1", + "core-js": "3.33.2", "countly-sdk-web": "23.6.2", "date-fns": "2.30.0", "dexie-batch": "0.4.3", - "emoji-picker-react": "4.5.3", + "emoji-picker-react": "4.5.14", "highlight.js": "11.9.0", "http-status-codes": "2.3.0", "jimp": "0.22.10", @@ -37,15 +36,15 @@ "long": "5.2.3", "markdown-it": "13.0.2", "murmurhash": "2.0.1", - "oidc-client-ts": "^2.2.5", + "oidc-client-ts": "^2.4.0", "platform": "1.3.6", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.11", "react-intl": "6.5.1", "react-redux": "8.1.3", - "react-router": "6.17.0", - "react-router-dom": "6.17.0", + "react-router": "6.18.0", + "react-router-dom": "6.18.0", "react-transition-group": "4.4.5", "redux": "4.2.1", "redux-logdown": "1.0.4", @@ -56,7 +55,7 @@ "underscore": "1.13.6", "uuidjs": "4.2.13", "webrtc-adapter": "8.2.3", - "zustand": "4.4.4" + "zustand": "4.4.6" }, "devDependencies": { "@babel/core": "7.23.2", @@ -66,16 +65,15 @@ "@babel/preset-react": "7.22.15", "@babel/preset-typescript": "7.23.2", "@emotion/eslint-plugin": "^11.11.0", - "@faker-js/faker": "8.1.0", + "@faker-js/faker": "8.2.0", "@formatjs/cli": "6.2.1", "@koush/wrtc": "0.5.3", "@testing-library/react": "14.0.0", - "@types/adm-zip": "0.5.2", "@types/dexie-batch": "0.4.6", "@types/eslint": "^8", "@types/fs-extra": "11.0.3", "@types/generate-changelog": "1.8.2", - "@types/jest": "29.5.6", + "@types/jest": "29.5.7", "@types/jquery": "^3", "@types/js-cookie": "3.0.5", "@types/jsdom": "21.1.4", @@ -83,25 +81,24 @@ "@types/libsodium-wrappers": "^0", "@types/linkify-it": "3.0.4", "@types/loadable__component": "^5", - "@types/markdown-it": "13.0.4", - "@types/node": "^20.8.7", + "@types/markdown-it": "13.0.5", + "@types/node": "^20.8.10", "@types/open-graph": "0.2.4", "@types/platform": "1.3.5", - "@types/react": "18.2.28", + "@types/react": "18.2.33", "@types/react-dom": "18.2.14", "@types/react-redux": "7.1.28", "@types/react-transition-group": "4.4.8", "@types/redux-mock-store": "1.0.5", "@types/seedrandom": "^3", - "@types/sinon": "10.0.19", + "@types/sinon": "17.0.0", "@types/speakingurl": "13.0.5", "@types/underscore": "1.11.12", "@types/webpack-env": "1.18.3", - "@wireapp/copy-config": "2.1.9", + "@wireapp/copy-config": "2.1.10", "@wireapp/eslint-config": "3.0.4", "@wireapp/prettier-config": "0.6.3", "@wireapp/store-engine": "^5.1.4", - "adm-zip": "0.5.10", "archiver": "^6.0.1", "autoprefixer": "^10.4.16", "babel-loader": "9.1.3", @@ -113,7 +110,7 @@ "dexie": "3.2.4", "dotenv": "16.3.1", "dpdm": "3.14.0", - "eslint": "^8.52.0", + "eslint": "^8.53.0", "eslint-plugin-prettier": "^5.0.1", "fake-indexeddb": "4.0.2", "generate-changelog": "1.8.0", @@ -128,7 +125,7 @@ "jsdom-worker": "0.3.0", "less": "4.2.0", "less-loader": "^11.1.3", - "lint-staged": "15.0.1", + "lint-staged": "15.0.2", "node-fetch": "2.7.0", "os-browserify": "0.3.0", "path-browserify": "1.0.1", @@ -136,7 +133,7 @@ "postcss-import": "^15.1.0", "postcss-less": "6.0.0", "postcss-loader": "^7.3.3", - "postcss-preset-env": "^9.2.0", + "postcss-preset-env": "^9.3.0", "postcss-scss": "4.0.9", "prettier": "^3.0.3", "raf": "3.4.1", @@ -144,8 +141,7 @@ "redux-mock-store": "1.5.4", "seedrandom": "^3.0.5", "simple-git": "3.20.0", - "sinon": "16.1.0", - "snabbdom": "3.5.1", + "sinon": "17.0.1", "style-loader": "^3.3.3", "stylelint": "^15", "stylelint-config-idiomatic-order": "9.0.0", diff --git a/server/package.json b/server/package.json index 63ffac1379e..79da41f9f25 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "main": "dist/index.js", "license": "GPL-3.0", "dependencies": { - "@wireapp/commons": "5.2.1", + "@wireapp/commons": "5.2.2", "dotenv": "16.3.1", "dotenv-extended": "2.9.0", "express": "4.18.2", @@ -22,13 +22,13 @@ "pm2": "5.3.0" }, "devDependencies": { - "@types/express": "4.17.19", + "@types/express": "4.17.20", "@types/express-sitemap-xml": "3.0.3", "@types/express-useragent": "1.0.4", "@types/fs-extra": "11.0.3", "@types/geolite2": "2.0.0", "@types/hbs": "4.0.3", - "@types/jest": "^29.5.6", + "@types/jest": "^29.5.7", "@types/node": "18.11.18", "jest": "29.7.0", "rimraf": "4.4.1", diff --git a/server/yarn.lock b/server/yarn.lock index 0d20d50b06e..9575c09d4e6 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -956,7 +956,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:4.17.19": +"@types/express@npm:*": version: 4.17.19 resolution: "@types/express@npm:4.17.19" dependencies: @@ -968,6 +968,18 @@ __metadata: languageName: node linkType: hard +"@types/express@npm:4.17.20": + version: 4.17.20 + resolution: "@types/express@npm:4.17.20" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: bf8a97d283128e5129f9ccabbeef728ff3f0484465e0ae74a304bd0588fa6cb715ae68845650caba9a641944b7791ba125d02ddbd47a7e62aaefdd036570c6c5 + languageName: node + linkType: hard + "@types/fs-extra@npm:11.0.3": version: 11.0.3 resolution: "@types/fs-extra@npm:11.0.3" @@ -1035,13 +1047,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.6": - version: 29.5.6 - resolution: "@types/jest@npm:29.5.6" +"@types/jest@npm:^29.5.7": + version: 29.5.7 + resolution: "@types/jest@npm:29.5.7" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: fa13a27bd1c8efd0381a419478769d0d6d3a8e93e1952d7ac3a16274e8440af6f73ed6f96ac1ff00761198badf2ee226b5ab5583a5d87a78d609ea78da5c5a24 + checksum: e28624ccb0ef1255a03fbbb4b5bc3e5cbcdc450d39e0739985ff679b124198f808c38c8c3e67859c6efc0e848196deeb8cfed028e12a821c511dfc1112a2d6e9 languageName: node linkType: hard @@ -1142,15 +1154,15 @@ __metadata: languageName: node linkType: hard -"@wireapp/commons@npm:5.2.1": - version: 5.2.1 - resolution: "@wireapp/commons@npm:5.2.1" +"@wireapp/commons@npm:5.2.2": + version: 5.2.2 + resolution: "@wireapp/commons@npm:5.2.2" dependencies: ansi-regex: 5.0.1 fs-extra: 11.1.0 logdown: 3.3.1 platform: 1.3.6 - checksum: 1510b705a40d45ceaf07b12b5a199d94fe977d3b2faaafc298ff167a65b820471f5863f9f93f27d2003f9f44ee3401423d6e12bb38ecd7808f8b2fc72821d411 + checksum: ae78630f8299eaae9ee136136981dabdcb4c100c43ec1430882fc154ae21e9fdb17999c1b892140ca5547625e7f502ba83e85ecf62312c8525098865032b6928 languageName: node linkType: hard @@ -5544,15 +5556,15 @@ __metadata: version: 0.0.0-use.local resolution: "wire-web-server@workspace:." dependencies: - "@types/express": 4.17.19 + "@types/express": 4.17.20 "@types/express-sitemap-xml": 3.0.3 "@types/express-useragent": 1.0.4 "@types/fs-extra": 11.0.3 "@types/geolite2": 2.0.0 "@types/hbs": 4.0.3 - "@types/jest": ^29.5.6 + "@types/jest": ^29.5.7 "@types/node": 18.11.18 - "@wireapp/commons": 5.2.1 + "@wireapp/commons": 5.2.2 dotenv: 16.3.1 dotenv-extended: 2.9.0 express: 4.18.2 diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts index edf45749d6f..68501e7c8bd 100644 --- a/src/__mocks__/@wireapp/core.ts +++ b/src/__mocks__/@wireapp/core.ts @@ -37,14 +37,12 @@ export class Account extends EventEmitter { mls: { schedulePeriodicKeyMaterialRenewals: jest.fn(), registerConversation: jest.fn(), - joinConferenceSubconversation: jest.fn(), getGroupIdFromConversationId: jest.fn(), renewKeyMaterial: jest.fn(), getClientIds: jest.fn(), getEpoch: jest.fn(), conversationExists: jest.fn(), exportSecretKey: jest.fn(), - leaveConferenceSubconversation: jest.fn(), on: this.on, emit: this.emit, off: this.off, @@ -64,6 +62,11 @@ export class Account extends EventEmitter { removeUsersFromMLSConversation: jest.fn(), removeUserFromConversation: jest.fn(), }, + subconversation: { + joinConferenceSubconversation: jest.fn(), + leaveConferenceSubconversation: jest.fn(), + subscribeToEpochUpdates: jest.fn(), + }, client: { deleteClient: jest.fn(), }, diff --git a/src/i18n/ar-SA.json b/src/i18n/ar-SA.json index bc555c0f4a7..3013f2fb1e4 100644 --- a/src/i18n/ar-SA.json +++ b/src/i18n/ar-SA.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "متاح", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "حرفان على الأقل. مسموح بالحروف a—z والأرقام 0—9 والشَرطة السفلية (_) فقط.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "اسمك الكامل", "preferencesDevice": "Device", "preferencesDeviceDetails": "تفاصيل الجهاز", diff --git a/src/i18n/bn-BD.json b/src/i18n/bn-BD.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/bn-BD.json +++ b/src/i18n/bn-BD.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/ca-ES.json b/src/i18n/ca-ES.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/ca-ES.json +++ b/src/i18n/ca-ES.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/cs-CZ.json b/src/i18n/cs-CZ.json index 310fe0524d8..cfb91d36146 100644 --- a/src/i18n/cs-CZ.json +++ b/src/i18n/cs-CZ.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Dostupný", "preferencesAccountUsernameErrorTaken": "Již uděleno", - "preferencesAccountUsernameHint": "Alespoň 2 znaky. Pouze a—z, 0—9 a _", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Celé jméno", "preferencesDevice": "Device", "preferencesDeviceDetails": "Podrobnosti o přístroji", diff --git a/src/i18n/da-DK.json b/src/i18n/da-DK.json index 8c224ee8ae4..74f7a7ca80f 100644 --- a/src/i18n/da-DK.json +++ b/src/i18n/da-DK.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Ledig", "preferencesAccountUsernameErrorTaken": "Allerede i brug", - "preferencesAccountUsernameHint": "Mindst to tegn. Kun a-z, 0-9 og _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Dit fulde navn", "preferencesDevice": "Device", "preferencesDeviceDetails": "Enheds Detaljer", diff --git a/src/i18n/de-DE.json b/src/i18n/de-DE.json index e1fd7fba54b..7f64a7325ea 100644 --- a/src/i18n/de-DE.json +++ b/src/i18n/de-DE.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Benutzername", "preferencesAccountUsernameAvailable": "Verfügbar", "preferencesAccountUsernameErrorTaken": "Bereits vergeben", - "preferencesAccountUsernameHint": "Mindestens zwei Zeichen. a—z, 0—9, und _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Ihr vollständiger Name", "preferencesDevice": "Gerät", "preferencesDeviceDetails": "Gerätedetails", diff --git a/src/i18n/el-GR.json b/src/i18n/el-GR.json index 83b447cc112..91544e1fa2c 100644 --- a/src/i18n/el-GR.json +++ b/src/i18n/el-GR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Διαθέσιμο", "preferencesAccountUsernameErrorTaken": "Χρησιμοποιείται ήδη", - "preferencesAccountUsernameHint": "Τουλάχιστον 2 χαρακτήρες. a—z, 0—9 και _ μόνο.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Ονοματεπώνυμο", "preferencesDevice": "Device", "preferencesDeviceDetails": "Λεπτομέρειες Συσκευής", diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 7fa325f6158..742d012a4d5 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -1153,7 +1153,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/es-ES.json b/src/i18n/es-ES.json index 1e519233a6f..3582e0f286d 100644 --- a/src/i18n/es-ES.json +++ b/src/i18n/es-ES.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Disponible", "preferencesAccountUsernameErrorTaken": "No disponible", - "preferencesAccountUsernameHint": "Al menos 2 caracter Sólo a–z, 0–9 y _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Tu nombre completo", "preferencesDevice": "Device", "preferencesDeviceDetails": "Detalles del dispositivo", diff --git a/src/i18n/et-EE.json b/src/i18n/et-EE.json index 7e692621d01..8ad3ca92398 100644 --- a/src/i18n/et-EE.json +++ b/src/i18n/et-EE.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Saadaval", "preferencesAccountUsernameErrorTaken": "Juba kasutusel", - "preferencesAccountUsernameHint": "Vähemalt 2 tähemärki. Ainult a-z, 0-9 ja _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Sinu täisnimi", "preferencesDevice": "Device", "preferencesDeviceDetails": "Seadme üksikasjad", diff --git a/src/i18n/fa-IR.json b/src/i18n/fa-IR.json index fb24a96edc7..69a295df33b 100644 --- a/src/i18n/fa-IR.json +++ b/src/i18n/fa-IR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "در دسترس", "preferencesAccountUsernameErrorTaken": "در حال حاضر موجود نیست", - "preferencesAccountUsernameHint": "حداقل ۲کاراکتر. حروف a-z، ارقام 0 تا 9 و _ مورد قبول میباشد.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "نام کامل شما", "preferencesDevice": "Device", "preferencesDeviceDetails": "جزییات اطلاعات دستگاه", diff --git a/src/i18n/fi-FI.json b/src/i18n/fi-FI.json index f7ac271e6b1..269ee4d3139 100644 --- a/src/i18n/fi-FI.json +++ b/src/i18n/fi-FI.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Saatavilla", "preferencesAccountUsernameErrorTaken": "On jo käytössä", - "preferencesAccountUsernameHint": "Vähintään 2 merkkiä, vain a - z, 0 - 9 ja _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Koko nimesi", "preferencesDevice": "Device", "preferencesDeviceDetails": "Laitteen yksityiskohdat", diff --git a/src/i18n/fr-FR.json b/src/i18n/fr-FR.json index 22db9a64526..367ea4d396c 100644 --- a/src/i18n/fr-FR.json +++ b/src/i18n/fr-FR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Disponible", "preferencesAccountUsernameErrorTaken": "Déjà pris", - "preferencesAccountUsernameHint": "Au moins 2 caractères. Uniquement a–z, 0–9 et _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Votre nom complet", "preferencesDevice": "Device", "preferencesDeviceDetails": "Informations de l’appareil", diff --git a/src/i18n/ga-IE.json b/src/i18n/ga-IE.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/ga-IE.json +++ b/src/i18n/ga-IE.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/he-IL.json b/src/i18n/he-IL.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/he-IL.json +++ b/src/i18n/he-IL.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/hi-IN.json b/src/i18n/hi-IN.json index 1a73a114438..12a04af2b8f 100644 --- a/src/i18n/hi-IN.json +++ b/src/i18n/hi-IN.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "कम से कम 2 वर्ण| केवल a—z, 0—9 और _|", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/hr-HR.json b/src/i18n/hr-HR.json index 4a566f65c29..0fce573c40e 100644 --- a/src/i18n/hr-HR.json +++ b/src/i18n/hr-HR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Dostupno", "preferencesAccountUsernameErrorTaken": "Već uzeto", - "preferencesAccountUsernameHint": "Najmanje 2 znaka. Samo a-z, 0-9, i _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Vaše puno ime", "preferencesDevice": "Device", "preferencesDeviceDetails": "Detalji o uređaju", diff --git a/src/i18n/hu-HU.json b/src/i18n/hu-HU.json index 3a612a4b054..c9bd97f4f62 100644 --- a/src/i18n/hu-HU.json +++ b/src/i18n/hu-HU.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Elérhető", "preferencesAccountUsernameErrorTaken": "Már foglalt", - "preferencesAccountUsernameHint": "Legalább 2 karakter, és kizárólag a—z, 0—9 és _ karakterek.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Teljes neved", "preferencesDevice": "Device", "preferencesDeviceDetails": "Eszköz részletei", diff --git a/src/i18n/id-ID.json b/src/i18n/id-ID.json index 92bdfb524ee..4e21bb950d0 100644 --- a/src/i18n/id-ID.json +++ b/src/i18n/id-ID.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Tersedia", "preferencesAccountUsernameErrorTaken": "Telah diambil", - "preferencesAccountUsernameHint": "Minimal 2 karakter. a-z, 0-9 dan _ saja.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Nama lengkap Anda", "preferencesDevice": "Device", "preferencesDeviceDetails": "Detil Perangkat", diff --git a/src/i18n/is-IS.json b/src/i18n/is-IS.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/is-IS.json +++ b/src/i18n/is-IS.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/it-IT.json b/src/i18n/it-IT.json index b322efb18e8..856b581ccb2 100644 --- a/src/i18n/it-IT.json +++ b/src/i18n/it-IT.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Disponibile", "preferencesAccountUsernameErrorTaken": "E’ già stato scelto", - "preferencesAccountUsernameHint": "Almeno 2 caratteri. a-z, 0-9 e solo _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Il tuo nome e cognome", "preferencesDevice": "Device", "preferencesDeviceDetails": "Dettagli sul dispositivo", diff --git a/src/i18n/ja-JP.json b/src/i18n/ja-JP.json index 39b6c175c97..1e442e99673 100644 --- a/src/i18n/ja-JP.json +++ b/src/i18n/ja-JP.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "利用できます", "preferencesAccountUsernameErrorTaken": "すでに利用されています", - "preferencesAccountUsernameHint": "少なくとも2文字。a-z, 0-9, および _ のみが利用できます。", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "あなたの氏名", "preferencesDevice": "Device", "preferencesDeviceDetails": "デバイスの詳細", diff --git a/src/i18n/lt-LT.json b/src/i18n/lt-LT.json index de6f6ffbb5d..08bbe6cc9fe 100644 --- a/src/i18n/lt-LT.json +++ b/src/i18n/lt-LT.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Prieinamas", "preferencesAccountUsernameErrorTaken": "Jau užimtas", - "preferencesAccountUsernameHint": "Bent 2 simboliai. Tik a—z, 0—9 ir _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Jūsų vardas ir pavardė", "preferencesDevice": "Device", "preferencesDeviceDetails": "Išsamesnė įrenginio informacija", diff --git a/src/i18n/lv-LV.json b/src/i18n/lv-LV.json index fe4cf67fb2b..80ebfbf8213 100644 --- a/src/i18n/lv-LV.json +++ b/src/i18n/lv-LV.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Pieejams", "preferencesAccountUsernameErrorTaken": "Jau ir aizņemts", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Jūsu pilnais vārds", "preferencesDevice": "Device", "preferencesDeviceDetails": "Ierīces detaļas", diff --git a/src/i18n/ms-MY.json b/src/i18n/ms-MY.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/ms-MY.json +++ b/src/i18n/ms-MY.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/nl-NL.json b/src/i18n/nl-NL.json index a50f9fca030..890ce263be7 100644 --- a/src/i18n/nl-NL.json +++ b/src/i18n/nl-NL.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Beschikbaar", "preferencesAccountUsernameErrorTaken": "Al in gebruik", - "preferencesAccountUsernameHint": "Ten minste 2 tekens. a—z, 0—9, en _ alleen.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Je volledige naam", "preferencesDevice": "Device", "preferencesDeviceDetails": "Apparaat Details", diff --git a/src/i18n/no-NO.json b/src/i18n/no-NO.json index 7c247a5a81f..143e6b9092a 100644 --- a/src/i18n/no-NO.json +++ b/src/i18n/no-NO.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/pl-PL.json b/src/i18n/pl-PL.json index 0628cb9e637..0988b6a7ec2 100644 --- a/src/i18n/pl-PL.json +++ b/src/i18n/pl-PL.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "&Dostępny(a)", "preferencesAccountUsernameErrorTaken": "Jest już w użyciu", - "preferencesAccountUsernameHint": "Co najmniej 2 znaki. Tylko a-z, 0-9, _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Twoje pełne imię i nazwisko", "preferencesDevice": "Device", "preferencesDeviceDetails": "Szczegóły Urządzenia", diff --git a/src/i18n/pt-BR.json b/src/i18n/pt-BR.json index 2e4234308fa..620ae3f8abe 100644 --- a/src/i18n/pt-BR.json +++ b/src/i18n/pt-BR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Nome de usuário", "preferencesAccountUsernameAvailable": "Disponível", "preferencesAccountUsernameErrorTaken": "Já está sendo usado", - "preferencesAccountUsernameHint": "Ao menos 2 caracteres. a—z, 0—9 e _ apenas.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Seu nome completo", "preferencesDevice": "Dispositivo", "preferencesDeviceDetails": "Detalhes do dispositivo", diff --git a/src/i18n/pt-PT.json b/src/i18n/pt-PT.json index 9ab0a5ffd6f..0c7e63cd0c7 100644 --- a/src/i18n/pt-PT.json +++ b/src/i18n/pt-PT.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Disponível", "preferencesAccountUsernameErrorTaken": "Já está ocupado", - "preferencesAccountUsernameHint": "Pelo menos 2 caracteres. a-z, 0-9 e _ apenas.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "O seu nome completo", "preferencesDevice": "Device", "preferencesDeviceDetails": "Detalhes do Dispositivo", diff --git a/src/i18n/ro-RO.json b/src/i18n/ro-RO.json index cbd9c5810ab..16f87998413 100644 --- a/src/i18n/ro-RO.json +++ b/src/i18n/ro-RO.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Disponibil", "preferencesAccountUsernameErrorTaken": "Deja folosit", - "preferencesAccountUsernameHint": "Cel puțin două caractere. Doar a—z, 0—9 și _ sunt permise.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Numele tău complet", "preferencesDevice": "Device", "preferencesDeviceDetails": "Detalii dispozitiv", diff --git a/src/i18n/ru-RU.json b/src/i18n/ru-RU.json index 9687fbfd817..c27472edab7 100644 --- a/src/i18n/ru-RU.json +++ b/src/i18n/ru-RU.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Псевдоним", "preferencesAccountUsernameAvailable": "Доступно", "preferencesAccountUsernameErrorTaken": "Уже занято", - "preferencesAccountUsernameHint": "Не менее 2 символов. Только a—z, 0—9 и _", + "preferencesAccountUsernameHint": "Не менее 2 символов. Только a—z, 0—9 and '.', '-', '_'.", "preferencesAccountUsernamePlaceholder": "Ваше полное имя", "preferencesDevice": "Устройство", "preferencesDeviceDetails": "Сведения об устройстве", @@ -1350,7 +1350,7 @@ "userListSelectedContacts": "Выбрано ({{selectedContacts}})", "userNotFoundMessage": "Возможно, у вас нет разрешения на использование этой учетной записи, либо этот человек отсутствует в {{brandName}}.", "userNotFoundTitle": "{{brandName}} не может найти этого человека.", - "userNotVerified": "Убедитесь в личности {{user}}, прежде чем подключаться.", + "userNotVerified": "Перед добавлением убедитесь в личности {{user}}.", "userProfileButtonConnect": "Связаться", "userProfileButtonIgnore": "Игнорировать", "userProfileButtonUnblock": "Разблокировать", diff --git a/src/i18n/si-LK.json b/src/i18n/si-LK.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/si-LK.json +++ b/src/i18n/si-LK.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/sk-SK.json b/src/i18n/sk-SK.json index e55784af6ba..88f71b47506 100644 --- a/src/i18n/sk-SK.json +++ b/src/i18n/sk-SK.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Dostupné", "preferencesAccountUsernameErrorTaken": "Už obsadené", - "preferencesAccountUsernameHint": "Aspoň 2 znaky. A výhradne a-z, 0-9.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Vaše celé meno", "preferencesDevice": "Device", "preferencesDeviceDetails": "Podrobnosti o zariadení", diff --git a/src/i18n/sl-SI.json b/src/i18n/sl-SI.json index 9ff238e8c3f..3f090930be3 100644 --- a/src/i18n/sl-SI.json +++ b/src/i18n/sl-SI.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Na voljo", "preferencesAccountUsernameErrorTaken": "Že zasedeno", - "preferencesAccountUsernameHint": "Vsaj 2 znaka. Le a—z, 0—9 in _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Vaše polno ime", "preferencesDevice": "Device", "preferencesDeviceDetails": "Podrobnosti naprave", diff --git a/src/i18n/sr-SP.json b/src/i18n/sr-SP.json index 26bfd3833d5..85e95b5acc9 100644 --- a/src/i18n/sr-SP.json +++ b/src/i18n/sr-SP.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "На располагању", "preferencesAccountUsernameErrorTaken": "Већ заузето", - "preferencesAccountUsernameHint": "Бар 2 знака. Само a—z, 0—9 и _", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Ваше пуно име", "preferencesDevice": "Device", "preferencesDeviceDetails": "Детаљи уређаја", diff --git a/src/i18n/sv-SE.json b/src/i18n/sv-SE.json index cc440dda7aa..0e16ac4fe87 100644 --- a/src/i18n/sv-SE.json +++ b/src/i18n/sv-SE.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Tillgängligt", "preferencesAccountUsernameErrorTaken": "Upptaget", - "preferencesAccountUsernameHint": "Minst 2 tecken. a—z, 0—9 och _ endast.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Ditt fullständiga namn", "preferencesDevice": "Device", "preferencesDeviceDetails": "Enhetsdetaljer", diff --git a/src/i18n/th-TH.json b/src/i18n/th-TH.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/th-TH.json +++ b/src/i18n/th-TH.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/tr-TR.json b/src/i18n/tr-TR.json index 900c6fe821c..eeb352fbff3 100644 --- a/src/i18n/tr-TR.json +++ b/src/i18n/tr-TR.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Alınabilir", "preferencesAccountUsernameErrorTaken": "Çoktan alınmış", - "preferencesAccountUsernameHint": "En az 2 karakter. a—z, 0—9, ve yalnızca _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Tam adınız", "preferencesDevice": "Device", "preferencesDeviceDetails": "Cihaz Detayları", diff --git a/src/i18n/uk-UA.json b/src/i18n/uk-UA.json index 07fd45cf502..a626dae4c8f 100644 --- a/src/i18n/uk-UA.json +++ b/src/i18n/uk-UA.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Доступний", "preferencesAccountUsernameErrorTaken": "Уже зарезервований", - "preferencesAccountUsernameHint": "Мінімум 2 символи з множини a—z, 0—9, та _.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Ваше повне ім’я", "preferencesDevice": "Device", "preferencesDeviceDetails": "Подробиці пристрою", diff --git a/src/i18n/uz-UZ.json b/src/i18n/uz-UZ.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/uz-UZ.json +++ b/src/i18n/uz-UZ.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/vi-VN.json b/src/i18n/vi-VN.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/vi-VN.json +++ b/src/i18n/vi-VN.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json index 755fe515b73..a889d289390 100644 --- a/src/i18n/zh-CN.json +++ b/src/i18n/zh-CN.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "可用", "preferencesAccountUsernameErrorTaken": "已被占用", - "preferencesAccountUsernameHint": "至少2个字符,仅可使用a—z, 0—9以及下划线。", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "您的全名", "preferencesDevice": "Device", "preferencesDeviceDetails": "设备详细信息", diff --git a/src/i18n/zh-HK.json b/src/i18n/zh-HK.json index 2d912e9f5cb..7a67c62458b 100644 --- a/src/i18n/zh-HK.json +++ b/src/i18n/zh-HK.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "Available", "preferencesAccountUsernameErrorTaken": "Already taken", - "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and _ only.", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "Your full name", "preferencesDevice": "Device", "preferencesDeviceDetails": "Device Details", diff --git a/src/i18n/zh-TW.json b/src/i18n/zh-TW.json index 429e40c0bd2..3d4539f51a2 100644 --- a/src/i18n/zh-TW.json +++ b/src/i18n/zh-TW.json @@ -1121,7 +1121,7 @@ "preferencesAccountUsername": "Username", "preferencesAccountUsernameAvailable": "可用", "preferencesAccountUsernameErrorTaken": "已經被用了", - "preferencesAccountUsernameHint": "至少要兩個字元,只可使用 a 到 z、0 到 9 或者 _ 這些字元。", + "preferencesAccountUsernameHint": "At least 2 characters. a—z, 0—9 and '.', '-', '_' only.", "preferencesAccountUsernamePlaceholder": "您的全名", "preferencesDevice": "Device", "preferencesDeviceDetails": "設備詳細資訊", diff --git a/src/script/E2EIdentity/E2EIdentity.test.ts b/src/script/E2EIdentity/E2EIdentity.test.ts index 55cf2f2fe54..d8f0853d1e2 100644 --- a/src/script/E2EIdentity/E2EIdentity.test.ts +++ b/src/script/E2EIdentity/E2EIdentity.test.ts @@ -17,6 +17,7 @@ * */ +import {TimeInMillis} from '@wireapp/commons/lib/util/TimeUtil'; import {container} from 'tsyringe'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; @@ -76,8 +77,8 @@ jest.mock('src/script/user/UserState', () => ({ })); describe('E2EIHandler', () => { - const params = {discoveryUrl: 'http://example.com', gracePeriodInMS: 30000}; - const newParams = {discoveryUrl: 'http://new-example.com', gracePeriodInMS: 60000}; + const params = {discoveryUrl: 'http://example.com', gracePeriodInSeconds: 30}; + const newParams = {discoveryUrl: 'http://new-example.com', gracePeriodInSeconds: 60}; const user = {name: () => 'John Doe', username: () => 'johndoe'}; let coreMock: Core; let userStateMock: UserState; @@ -135,11 +136,11 @@ describe('E2EIHandler', () => { // Assuming that the instance exposes getters for discoveryUrl and gracePeriodInMS for testing purposes expect(instance['discoveryUrl']).toEqual(params.discoveryUrl); - expect(instance['gracePeriodInMS']).toEqual(params.gracePeriodInMS); + expect(instance['gracePeriodInMS']).toEqual(params.gracePeriodInSeconds * TimeInMillis.SECOND); instance.updateParams(newParams); expect(instance['discoveryUrl']).toEqual(newParams.discoveryUrl); - expect(instance['gracePeriodInMS']).toEqual(newParams.gracePeriodInMS); + expect(instance['gracePeriodInMS']).toEqual(newParams.gracePeriodInSeconds * TimeInMillis.SECOND); }); it('should return true when supportsMLS returns true and ENABLE_E2EI is true', () => { diff --git a/src/script/E2EIdentity/E2EIdentity.ts b/src/script/E2EIdentity/E2EIdentity.ts index f24986e5a0b..34e1fa08a67 100644 --- a/src/script/E2EIdentity/E2EIdentity.ts +++ b/src/script/E2EIdentity/E2EIdentity.ts @@ -23,6 +23,7 @@ import {PrimaryModal, removeCurrentModal} from 'Components/Modals/PrimaryModal'; import {Config} from 'src/script/Config'; import {Core} from 'src/script/service/CoreSingleton'; import {UserState} from 'src/script/user/UserState'; +import {TIME_IN_MILLIS} from 'Util/TimeUtil'; import {removeUrlParameters} from 'Util/UrlUtil'; import {supportsMLS} from 'Util/util'; @@ -42,7 +43,7 @@ export enum E2EIHandlerStep { interface E2EIHandlerParams { discoveryUrl: string; - gracePeriodInMS: number; + gracePeriodInSeconds: number; } class E2EIHandler { @@ -54,12 +55,12 @@ class E2EIHandler { private gracePeriodInMS: number; private currentStep: E2EIHandlerStep | null = E2EIHandlerStep.UNINITIALIZED; - private constructor({discoveryUrl, gracePeriodInMS}: E2EIHandlerParams) { + private constructor({discoveryUrl, gracePeriodInSeconds}: E2EIHandlerParams) { // ToDo: Do these values need to te able to be updated? Should we use a singleton with update fn? this.discoveryUrl = discoveryUrl; - this.gracePeriodInMS = gracePeriodInMS; + this.gracePeriodInMS = gracePeriodInSeconds * TIME_IN_MILLIS.SECOND; this.timer = DelayTimerService.getInstance({ - gracePeriodInMS, + gracePeriodInMS: this.gracePeriodInMS, gracePeriodExpiredCallback: () => null, delayPeriodExpiredCallback: () => null, }); @@ -92,11 +93,11 @@ class E2EIHandler { /** * @param E2EIHandlerParams The params to create the grace period timer */ - public updateParams({gracePeriodInMS, discoveryUrl}: E2EIHandlerParams) { - this.gracePeriodInMS = gracePeriodInMS; + public updateParams({gracePeriodInSeconds, discoveryUrl}: E2EIHandlerParams) { + this.gracePeriodInMS = gracePeriodInSeconds * TIME_IN_MILLIS.SECOND; this.discoveryUrl = discoveryUrl; this.timer.updateParams({ - gracePeriodInMS, + gracePeriodInMS: this.gracePeriodInMS, gracePeriodExpiredCallback: () => null, delayPeriodExpiredCallback: () => null, }); diff --git a/src/script/assets/AssetRepository.ts b/src/script/assets/AssetRepository.ts index 92081ab626e..ff6b93e6bc5 100644 --- a/src/script/assets/AssetRepository.ts +++ b/src/script/assets/AssetRepository.ts @@ -38,6 +38,7 @@ import {Conversation} from '../entity/Conversation'; import {FileAsset} from '../entity/message/FileAsset'; import type {User} from '../entity/User'; import {Core} from '../service/CoreSingleton'; +import {TeamState} from '../team/TeamState'; interface CompressedImage { compressedBytes: Uint8Array; @@ -64,6 +65,7 @@ export class AssetRepository { constructor( private readonly assetService = container.resolve(AssetService), private readonly core = container.resolve(Core), + private readonly teamState = container.resolve(TeamState), ) { this.logger = getLogger('AssetRepository'); } @@ -225,11 +227,11 @@ export class AssetRepository { } getAssetRetention(userEntity: User, conversationEntity: Conversation): AssetRetentionPolicy { - const isTeamMember = userEntity.inTeam(); - const isTeamConversation = conversationEntity.inTeam(); + const isTeamMember = this.teamState.isInTeam(userEntity); + const isTeamConversation = this.teamState.isInTeam(conversationEntity); const isTeamUserInConversation = conversationEntity .participating_user_ets() - .some(conversationParticipant => conversationParticipant.inTeam()); + .some(conversationParticipant => this.teamState.isInTeam(conversationParticipant)); const isEternalInfrequentAccess = isTeamMember || isTeamConversation || isTeamUserInConversation; return isEternalInfrequentAccess ? AssetRetentionPolicy.ETERNAL_INFREQUENT_ACCESS : AssetRetentionPolicy.EXPIRING; diff --git a/src/script/assets/AssetService.ts b/src/script/assets/AssetService.ts index 3e032f111b3..8322e8ce0e2 100644 --- a/src/script/assets/AssetService.ts +++ b/src/script/assets/AssetService.ts @@ -17,7 +17,6 @@ * */ -import {ProgressCallback} from '@wireapp/api-client/lib/http/'; import {singleton, container} from 'tsyringe'; import {legacyAsset, assetV3, isValidApiPath} from 'Util/ValidationUtil'; @@ -62,26 +61,4 @@ export class AssetService { const cachingParam = forceCaching ? '&forceCaching=true' : ''; return `${url}?access_token=${this.apiClient['accessTokenStore'].accessToken?.access_token}${assetTokenParam}${cachingParam}`; } - - async downloadAssetV1( - assetId: string, - conversationId: string, - forceCaching?: boolean, - progressCallback?: ProgressCallback, - ) { - return this.apiClient.api.asset.getAssetV1(assetId, conversationId, forceCaching, progressCallback); - } - - async downloadAssetV2( - assetId: string, - conversationId: string, - forceCaching?: boolean, - progressCallback?: ProgressCallback, - ) { - return this.apiClient.api.asset.getAssetV2(assetId, conversationId, forceCaching, progressCallback); - } - - async downloadAssetV3(assetId: string, token?: string, forceCaching?: boolean, progressCallback?: ProgressCallback) { - return this.apiClient.api.asset.getAssetV3(assetId, token, forceCaching, progressCallback); - } } diff --git a/src/script/auth/module/action/creator/LanguageActionCreator.ts b/src/script/auth/module/action/creator/LanguageActionCreator.ts index 6bb216f057a..c4fb423e220 100644 --- a/src/script/auth/module/action/creator/LanguageActionCreator.ts +++ b/src/script/auth/module/action/creator/LanguageActionCreator.ts @@ -38,17 +38,3 @@ export interface LanguageSwitchFailedAction extends AppAction { readonly error: Error; readonly type: LANGUAGE_ACTION.SWITCH_LANGUAGE_FAILED; } - -export class LanguageActionCreator { - static startSwitchLanguage = (): LanguageSwitchStartAction => ({ - type: LANGUAGE_ACTION.SWITCH_LANGUAGE_START, - }); - static successfulSwitchLanguage = (language: string): LanguageSwitchSuccessAction => ({ - payload: language, - type: LANGUAGE_ACTION.SWITCH_LANGUAGE_SUCCESS, - }); - static failedSwitchLanguage = (error: Error): LanguageSwitchFailedAction => ({ - error, - type: LANGUAGE_ACTION.SWITCH_LANGUAGE_FAILED, - }); -} diff --git a/src/script/calling/Call.ts b/src/script/calling/Call.ts index f46d258e72c..773c1c6f512 100644 --- a/src/script/calling/Call.ts +++ b/src/script/calling/Call.ts @@ -27,7 +27,6 @@ import {matchQualifiedIds} from 'Util/QualifiedId'; import {sortUsersByPriority} from 'Util/StringUtil'; import {MuteState} from './CallState'; -import {CALL_MESSAGE_TYPE} from './enum/CallMessageType'; import type {ClientId, Participant} from './Participant'; import {Config} from '../Config'; @@ -54,9 +53,10 @@ export class Call { public readonly isCbrEnabled: ko.Observable = ko.observable( Config.getConfig().FEATURE.ENFORCE_CONSTANT_BITRATE, ); + public readonly isConference: boolean; + public readonly isGroupOrConference: boolean; public readonly activeSpeakers: ko.ObservableArray = ko.observableArray([]); public blockMessages: boolean = false; - public type?: CALL_MESSAGE_TYPE; public currentPage: ko.Observable = ko.observable(0); public pages: ko.ObservableArray = ko.observableArray(); readonly maximizedParticipant: ko.Observable; @@ -95,6 +95,8 @@ export class Call { }); this.maximizedParticipant = ko.observable(null); this.muteState(isMuted ? MuteState.SELF_MUTED : MuteState.NOT_MUTED); + this.isConference = [CONV_TYPE.CONFERENCE, CONV_TYPE.CONFERENCE_MLS].includes(this.conversationType); + this.isGroupOrConference = this.isConference || this.conversationType === CONV_TYPE.GROUP; } get hasWorkingAudioInput(): boolean { @@ -208,12 +210,6 @@ export class Call { return this.participants().filter(({user, clientId}) => !user.isMe || this.selfClientId !== clientId); } - removeParticipant(participant: Participant): void { - this.participants.remove(participant); - this.activeSpeakers.remove(participant); - this.updatePages(); - } - updatePages() { const selfParticipant = this.getSelfParticipant(); const remoteParticipants = this.getRemoteParticipants().sort((p1, p2) => sortUsersByPriority(p1.user, p2.user)); diff --git a/src/script/calling/CallState.ts b/src/script/calling/CallState.ts index b9bfade5471..d4e84f139f9 100644 --- a/src/script/calling/CallState.ts +++ b/src/script/calling/CallState.ts @@ -47,7 +47,6 @@ export class CallState { public readonly cbrEncoding: ko.Observable = ko.observable( Config.getConfig().FEATURE.ENFORCE_CONSTANT_BITRATE ? 1 : 0, ); - public readonly videoSpeakersActiveTab: ko.Observable = ko.observable(CallViewTab.ALL); readonly selectableScreens: ko.Observable = ko.observable([]); readonly selectableWindows: ko.Observable = ko.observable([]); /** call that is current active (connecting or connected) */ diff --git a/src/script/calling/CallingRepository.test.ts b/src/script/calling/CallingRepository.test.ts index 3dea6daa268..b01fdf679af 100644 --- a/src/script/calling/CallingRepository.test.ts +++ b/src/script/calling/CallingRepository.test.ts @@ -56,6 +56,9 @@ const createConversation = ( const conversation = new Conversation(createUuid(), '', protocol); conversation.participating_user_ets.push(new User(createUuid())); conversation.type(type); + if (protocol === ConversationProtocol.MLS) { + conversation.groupId = 'group-id'; + } return conversation; }; diff --git a/src/script/calling/CallingRepository.ts b/src/script/calling/CallingRepository.ts index 289f732273c..9e01b8d1db5 100644 --- a/src/script/calling/CallingRepository.ts +++ b/src/script/calling/CallingRepository.ts @@ -18,11 +18,12 @@ */ import type {CallConfigData} from '@wireapp/api-client/lib/account/CallConfigData'; -import type {QualifiedUserClients} from '@wireapp/api-client/lib/conversation'; +import {QualifiedUserClients} from '@wireapp/api-client/lib/conversation'; import type {QualifiedId} from '@wireapp/api-client/lib/user'; import type {WebappProperties} from '@wireapp/api-client/lib/user/data'; import {MessageSendingState} from '@wireapp/core/lib/conversation'; import {flattenUserMap} from '@wireapp/core/lib/conversation/message/UserClientsUtil'; +import {SubconversationEpochInfoMember} from '@wireapp/core/lib/conversation/SubconversationService/SubconversationService'; import {amplify} from 'amplify'; import axios from 'axios'; import ko from 'knockout'; @@ -64,6 +65,7 @@ import {ClientId, Participant, UserId} from './Participant'; import {PrimaryModal} from '../components/Modals/PrimaryModal'; import {Config} from '../Config'; +import {isMLSConversation} from '../conversation/ConversationSelectors'; import {ConversationState} from '../conversation/ConversationState'; import {CallingEvent, EventBuilder} from '../conversation/EventBuilder'; import {CONSENT_TYPE, MessageRepository, MessageSendingOptions} from '../conversation/MessageRepository'; @@ -117,13 +119,7 @@ enum CALL_DIRECTION { OUTGOING = 'outgoing', } -export interface SubconversationEpochInfoMember { - userid: `${string}@${string}`; - clientid: string; - in_subconv: boolean; -} - -type SubconversationData = {epoch: number; secretKey: string}; +type SubconversationData = {epoch: number; secretKey: string; members: SubconversationEpochInfoMember[]}; export class CallingRepository { private readonly acceptVersionWarning: (conversationId: QualifiedId) => void; @@ -341,7 +337,7 @@ export class CallingRepository { } const allClients = await this.core.service!.conversation.fetchAllParticipantsClients(call.conversationId); - if (!conversation.isUsingMLSProtocol) { + if (!isMLSConversation(conversation)) { const qualifiedClients = flattenUserMap(allClients); const clients: Clients = flatten( @@ -476,10 +472,9 @@ export class CallingRepository { private async warmupMediaStreams(call: Call, audio: boolean, camera: boolean): Promise { // if it's a video call we query the video user media in order to display the video preview - const isGroup = [CONV_TYPE.CONFERENCE, CONV_TYPE.GROUP].includes(call.conversationType); try { camera = this.teamState.isVideoCallingEnabled() ? camera : false; - const mediaStream = await this.getMediaStream({audio, camera}, isGroup); + const mediaStream = await this.getMediaStream({audio, camera}, call.isGroupOrConference); if (call.state() !== CALL_STATE.NONE) { call.getSelfParticipant().updateMediaStream(mediaStream, true); if (camera) { @@ -676,8 +671,8 @@ export class CallingRepository { toSecond(new Date(time).getTime()), this.serializeQualifiedId(conversationId), this.serializeQualifiedId(userId), - conversation?.isUsingMLSProtocol ? senderClientId : clientId, - conversation?.isUsingMLSProtocol ? CONV_TYPE.CONFERENCE_MLS : CONV_TYPE.CONFERENCE, + conversation && isMLSConversation(conversation) ? senderClientId : clientId, + conversation && isMLSConversation(conversation) ? CONV_TYPE.CONFERENCE_MLS : CONV_TYPE.CONFERENCE, ); if (res !== 0) { @@ -714,7 +709,7 @@ export class CallingRepository { return CONV_TYPE.ONEONONE; } - if (conversation.isUsingMLSProtocol) { + if (isMLSConversation(conversation)) { return CONV_TYPE.CONFERENCE_MLS; } return this.supportsConferenceCalling ? CONV_TYPE.CONFERENCE : CONV_TYPE.GROUP; @@ -753,7 +748,7 @@ export class CallingRepository { ); this.storeCall(call); const loadPreviewPromise = - [CONV_TYPE.CONFERENCE, CONV_TYPE.GROUP].includes(conversationType) && callType === CALL_TYPE.VIDEO + call.isGroupOrConference && callType === CALL_TYPE.VIDEO ? this.warmupMediaStreams(call, true, true) : Promise.resolve(true); const success = await loadPreviewPromise; @@ -829,8 +824,7 @@ export class CallingRepository { ); } try { - const isGroup = [CONV_TYPE.CONFERENCE, CONV_TYPE.GROUP].includes(call.conversationType); - const mediaStream = await this.getMediaStream({audio: true, screen: true}, isGroup); + const mediaStream = await this.getMediaStream({audio: true, screen: true}, call.isGroupOrConference); // https://stackoverflow.com/a/25179198/451634 mediaStream.getVideoTracks()[0].onended = () => { this.wCall?.setVideoSendState(this.wUser, this.serializeQualifiedId(call.conversationId), VIDEO_STATE.STOPPED); @@ -883,13 +877,9 @@ export class CallingRepository { } } - setEpochInfo( - conversationId: QualifiedId, - subconversationData: SubconversationData, - members: SubconversationEpochInfoMember[], - ) { + setEpochInfo(conversationId: QualifiedId, subconversationData: SubconversationData) { const serializedConversationId = this.serializeQualifiedId(conversationId); - const {epoch, secretKey} = subconversationData; + const {epoch, secretKey, members} = subconversationData; const clients = { convid: serializedConversationId, clients: members, @@ -1155,7 +1145,7 @@ export class CallingRepository { * This message is used to tell your other clients you have answered or * rejected a call and to stop ringing. */ - if (typeof payload === 'string' && conversation.isUsingMLSProtocol && myClientsOnly) { + if (typeof payload === 'string' && isMLSConversation(conversation) && myClientsOnly) { return void this.messageRepository.sendSelfCallingMessage(payload, conversation.qualifiedId); } @@ -1370,7 +1360,9 @@ export class CallingRepository { const canRing = !conversation.showNotificationsNothing() && shouldRing && this.isReady; const selfParticipant = new Participant(this.selfUser, this.selfClientId); const isVideoCall = hasVideo ? CALL_TYPE.VIDEO : CALL_TYPE.NORMAL; - const isMuted = Config.getConfig().FEATURE.CONFERENCE_AUTO_MUTE && conversationType === CONV_TYPE.CONFERENCE; + const isMuted = + Config.getConfig().FEATURE.CONFERENCE_AUTO_MUTE && + [CONV_TYPE.CONFERENCE, CONV_TYPE.CONFERENCE_MLS].includes(conversationType); const call = new Call( qualifiedUserId, conversation.qualifiedId, @@ -1548,13 +1540,12 @@ export class CallingRepository { window.setTimeout(() => resolve(selfParticipant.getMediaStream()), 0); }); } - const isGroup = [CONV_TYPE.CONFERENCE, CONV_TYPE.GROUP].includes(call.conversationType); this.mediaStreamQuery = (async () => { try { if (missingStreams.screen && selfParticipant.sharesScreen()) { return selfParticipant.getMediaStream(); } - const mediaStream = await this.getMediaStream(missingStreams, isGroup); + const mediaStream = await this.getMediaStream(missingStreams, call.isGroupOrConference); this.mediaStreamQuery = undefined; const newStream = selfParticipant.updateMediaStream(mediaStream, true); return newStream; diff --git a/src/script/calling/mlsConference.ts b/src/script/calling/mlsConference.ts deleted file mode 100644 index f6cc66921d1..00000000000 --- a/src/script/calling/mlsConference.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import {SUBCONVERSATION_ID} from '@wireapp/api-client/lib/conversation/Subconversation'; -import {QualifiedId} from '@wireapp/api-client/lib/user'; -import {MLSService} from '@wireapp/core/lib/messagingProtocols/mls'; -import {constructFullyQualifiedClientId} from '@wireapp/core/lib/util/fullyQualifiedClientIdUtils'; - -import {SubconversationEpochInfoMember} from './CallingRepository'; - -import {ConversationState} from '../conversation/ConversationState'; - -const KEY_LENGTH = 32; - -const generateSubconversationMembers = async ( - {mlsService}: {mlsService: MLSService}, - subconversationGroupId: string, - parentGroupId: string, -): Promise => { - const subconversationMemberIds = await mlsService.getClientIds(subconversationGroupId); - const parentMemberIds = await mlsService.getClientIds(parentGroupId); - - return parentMemberIds.map(parentMember => { - const isSubconversationMember = subconversationMemberIds.some( - ({userId, clientId, domain}) => - constructFullyQualifiedClientId(userId, clientId, domain) === - constructFullyQualifiedClientId(parentMember.userId, parentMember.clientId, parentMember.domain), - ); - - return { - userid: `${parentMember.userId}@${parentMember.domain}`, - clientid: parentMember.clientId, - in_subconv: isSubconversationMember, - }; - }); -}; - -export const getSubconversationEpochInfo = async ( - {mlsService}: {mlsService: MLSService}, - conversationId: QualifiedId, - shouldAdvanceEpoch = false, -): Promise<{ - members: SubconversationEpochInfoMember[]; - epoch: number; - secretKey: string; - keyLength: number; -}> => { - const subconversationGroupId = await mlsService.getGroupIdFromConversationId( - conversationId, - SUBCONVERSATION_ID.CONFERENCE, - ); - - const parentGroupId = await mlsService.getGroupIdFromConversationId(conversationId); - - // this method should not be called if the subconversation (and its parent conversation) is not established - if (!subconversationGroupId || !parentGroupId) { - throw new Error( - `Could not obtain epoch info for conference subconversation of conversation ${JSON.stringify( - conversationId, - )}: parent or subconversation group ID is missing`, - ); - } - - const members = await generateSubconversationMembers({mlsService}, subconversationGroupId, parentGroupId); - - if (shouldAdvanceEpoch) { - await mlsService.renewKeyMaterial(subconversationGroupId); - } - - const epoch = Number(await mlsService.getEpoch(subconversationGroupId)); - - const secretKey = await mlsService.exportSecretKey(subconversationGroupId, KEY_LENGTH); - - return {members, epoch, keyLength: KEY_LENGTH, secretKey}; -}; - -export const subscribeToEpochUpdates = async ( - {mlsService, conversationState}: {mlsService: MLSService; conversationState: ConversationState}, - conversationId: QualifiedId, - onEpochUpdate: (info: { - members: SubconversationEpochInfoMember[]; - epoch: number; - secretKey: string; - keyLength: number; - }) => void, -): Promise<() => void> => { - const {epoch: initialEpoch, groupId: subconversationGroupId} = - await mlsService.joinConferenceSubconversation(conversationId); - - const forwardNewEpoch = async ({groupId, epoch}: {groupId: string; epoch: number}) => { - if (groupId !== subconversationGroupId) { - // if the epoch update did not happen in the subconversation directly, check if it happened in the parent conversation - const parentConversation = conversationState.findConversationByGroupId(groupId); - if (!parentConversation) { - return; - } - - const foundSubconversationGroupId = await mlsService.getGroupIdFromConversationId?.( - parentConversation.qualifiedId, - SUBCONVERSATION_ID.CONFERENCE, - ); - - // if the conference subconversation of parent conversation is not known, ignore the epoch update - if (foundSubconversationGroupId !== subconversationGroupId) { - return; - } - } - - const {keyLength, secretKey, members} = await getSubconversationEpochInfo({mlsService}, conversationId); - - return onEpochUpdate({epoch: Number(epoch), keyLength, secretKey, members}); - }; - - mlsService.on('newEpoch', forwardNewEpoch); - - await forwardNewEpoch({groupId: subconversationGroupId, epoch: initialEpoch}); - - return () => mlsService.off('newEpoch', forwardNewEpoch); -}; diff --git a/src/script/client/ClientService.ts b/src/script/client/ClientService.ts index 04af8834ab6..edc3cae5f96 100644 --- a/src/script/client/ClientService.ts +++ b/src/script/client/ClientService.ts @@ -17,12 +17,7 @@ * */ -import type { - CreateClientPayload, - RegisteredClient, - QualifiedUserClientMap, - ClientCapabilityData, -} from '@wireapp/api-client/lib/client'; +import type {RegisteredClient, QualifiedUserClientMap, ClientCapabilityData} from '@wireapp/api-client/lib/client'; import type {QualifiedId} from '@wireapp/api-client/lib/user'; import {container} from 'tsyringe'; @@ -34,14 +29,6 @@ import {StorageSchemata} from '../storage/StorageSchemata'; export class ClientService { private readonly CLIENT_STORE_NAME: string; - static get URL_CLIENTS(): string { - return '/clients'; - } - - static get URL_USERS(): string { - return '/users'; - } - constructor( private readonly storageService = container.resolve(StorageService), private readonly apiClient = container.resolve(APIClient), @@ -105,15 +92,6 @@ export class ClientService { return listedClients.qualified_user_map; } - /** - * Register a new client. - * @param newClient Client payload - * @returns Resolves with the registered client information - */ - postClients(newClient: CreateClientPayload): Promise { - return this.apiClient.api.client.postClient(newClient); - } - //############################################################################## // Database requests //############################################################################## diff --git a/src/script/components/Conversation/Conversation.tsx b/src/script/components/Conversation/Conversation.tsx index f0355768cd9..6c90f651dd4 100644 --- a/src/script/components/Conversation/Conversation.tsx +++ b/src/script/components/Conversation/Conversation.tsx @@ -99,7 +99,7 @@ export const Conversation = ({ 'isFileSharingSendingEnabled', ]); const {is1to1, isRequest} = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest']); - const {inTeam} = useKoSubscribableChildren(selfUser, ['inTeam']); + const inTeam = teamState.isInTeam(selfUser); const {activeCalls} = useKoSubscribableChildren(callState, ['activeCalls']); const [isMsgElementsFocusable, setMsgElementsFocusable] = useState(true); diff --git a/src/script/components/InputBar/InputBar.tsx b/src/script/components/InputBar/InputBar.tsx index 14b5c8cdbc1..10f7f969cce 100644 --- a/src/script/components/InputBar/InputBar.tsx +++ b/src/script/components/InputBar/InputBar.tsx @@ -170,6 +170,7 @@ export const InputBar = ({ const isReplying = !!replyMessageEntity; const isConnectionRequest = isOutgoingRequest || isIncomingRequest; const hasLocalEphemeralTimer = isSelfDeletingMessagesEnabled && !!localMessageTimer && !hasGlobalMessageTimer; + const isTypingRef = useRef(false); // To be changed when design chooses a breakpoint, the conditional can be integrated to the ui-kit directly const isScaledDown = useMatchMedia('max-width: 768px'); @@ -192,6 +193,7 @@ export const InputBar = ({ text: textValue, onTypingChange: useCallback( isTyping => { + isTypingRef.current = isTyping; if (isTyping) { void conversationRepository.sendTypingStart(conversation); } else { @@ -253,9 +255,10 @@ export const InputBar = ({ cancelMessageEditing(true); setEditedMessage(messageEntity); - if (messageEntity.quote() && conversation) { + const quote = messageEntity.quote(); + if (quote && conversation) { void messageRepository - .getMessageInConversationById(conversation, messageEntity.quote().messageId) + .getMessageInConversationById(conversation, quote.messageId) .then(quotedMessage => setReplyMessageEntity(quotedMessage)); } } @@ -567,7 +570,7 @@ export const InputBar = ({ loadDraftState={loadDraft} onShiftTab={onShiftTab} onSend={sendMessage} - onBlur={() => isTypingIndicatorEnabled && conversationRepository.sendTypingStop(conversation)} + onBlur={() => isTypingRef.current && conversationRepository.sendTypingStop(conversation)} > {isScaledDown ? ( <> diff --git a/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx b/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx index da96641e786..8471a36ca37 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx @@ -29,7 +29,6 @@ import {useRelativeTimestamp} from 'src/script/hooks/useRelativeTimestamp'; import {StatusType} from 'src/script/message/StatusType'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {getMessageAriaLabel} from 'Util/conversationMessages'; -import {groupByReactionUsers} from 'Util/ReactionUtil'; import {ContentAsset} from './asset'; import {MessageActionsMenu} from './MessageActions/MessageActions'; @@ -65,7 +64,7 @@ export interface ContentMessageProps extends Omit void; } -const ContentMessageComponent: React.FC = ({ +export const ContentMessageComponent: React.FC = ({ conversation, message, findMessage, @@ -102,18 +101,19 @@ const ContentMessageComponent: React.FC = ({ reactions, status, user, + quote, } = useKoSubscribableChildren(message, [ 'senderName', 'timestamp', 'ephemeral_caption', 'ephemeral_status', 'assets', - 'other_likes', 'was_edited', 'failedToSend', 'reactions', 'status', 'user', + 'quote', ]); const shouldShowAvatar = (): boolean => { @@ -140,9 +140,6 @@ const ContentMessageComponent: React.FC = ({ setActionMenuVisibility(isMessageFocused || msgFocusState); }, [msgFocusState, isMessageFocused]); - const reactionGroupedByUser = groupByReactionUsers(reactions); - const reactionsTotalCount = Array.from(reactionGroupedByUser).length; - return (
= ({
)} - {message.quote() && ( + {quote && ( = ({ handleActionMenuVisibility={setActionMenuVisibility} contextMenu={contextMenu} isMessageFocused={msgFocusState} - messageWithSection={hasMarker} handleReactionClick={onClickReaction} - reactionsTotalCount={reactionsTotalCount} + reactionsTotalCount={reactions.length} isRemovedFromConversation={conversation.removed_from_conversation()} /> )} @@ -253,7 +249,7 @@ const ContentMessageComponent: React.FC = ({ onClickReactionDetails(message)} @@ -263,5 +259,3 @@ const ContentMessageComponent: React.FC = ({ ); }; - -export {ContentMessageComponent}; diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.styles.ts b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.styles.ts index 2f408f149db..9be4af5c601 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.styles.ts +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.styles.ts @@ -33,6 +33,7 @@ export const messageBodyActions: CSSObject = { position: 'absolute', right: '16px', top: '-20px', + userSelect: 'none', '@media (max-width: @screen-md-min)': { height: '45px', flexDirection: 'column', diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.test.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.test.tsx index bec180349a6..f6c8ecf343a 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.test.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.test.tsx @@ -31,7 +31,6 @@ const defaultProps: MessageActionsMenuProps = { contextMenu: {entries: ko.observable([{label: 'option1', text: 'option1'}])}, isMessageFocused: true, handleActionMenuVisibility: jest.fn(), - messageWithSection: false, handleReactionClick: jest.fn(), reactionsTotalCount: 0, isRemovedFromConversation: false, diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.tsx index cd8299b1d42..272cfaa4ee6 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.tsx @@ -59,7 +59,6 @@ export interface MessageActionsMenuProps { contextMenu: {entries: ko.Subscribable}; isMessageFocused: boolean; handleActionMenuVisibility: (isVisible: boolean) => void; - messageWithSection: boolean; handleReactionClick: (emoji: string) => void; reactionsTotalCount: number; isRemovedFromConversation: boolean; @@ -71,7 +70,6 @@ const MessageActionsMenu: FC = ({ isMessageFocused, handleActionMenuVisibility, message, - messageWithSection, handleReactionClick, reactionsTotalCount, isRemovedFromConversation, @@ -181,7 +179,6 @@ const MessageActionsMenu: FC = ({ handleKeyDown={handleKeyDown} resetActionMenuStates={resetActionMenuStates} wrapperRef={wrapperRef} - message={message} handleReactionClick={handleReactionClick} /> {message.isReplyable() && ( diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiPicker.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiPicker.tsx index d27e54ebf74..b8265d58b3e 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiPicker.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiPicker.tsx @@ -117,11 +117,17 @@ const EmojiPickerContainer: FC = ({ } }} > + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
{ + event.stopPropagation(); + }} > (); const defaultProps: MessageReactionsProps = { - message: new ContentMessage(), handleReactionClick: jest.fn(), messageFocusedTabIndex: 0, currentMsgActionName: '', diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.tsx index 2d613020179..cb296eaa04d 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.tsx @@ -17,9 +17,8 @@ * */ -import {useState, useCallback, RefObject, FC, useRef} from 'react'; +import {useState, RefObject, FC, useRef} from 'react'; -import {ContentMessage} from 'src/script/entity/message/ContentMessage'; import {KEY} from 'Util/KeyboardUtil'; import {t} from 'Util/LocalizerUtil'; @@ -46,7 +45,6 @@ export interface MessageReactionsProps { handleCurrentMsgAction: (actionName: string) => void; resetActionMenuStates: () => void; wrapperRef: RefObject; - message: ContentMessage; handleReactionClick: (emoji: string) => void; } @@ -58,7 +56,6 @@ const MessageReactions: FC = ({ handleKeyDown, resetActionMenuStates, wrapperRef, - message, handleReactionClick, }) => { const isThumbUpAction = currentMsgActionName === MessageActionsId.THUMBSUP; @@ -84,20 +81,30 @@ const MessageReactions: FC = ({ setShowEmojis(false); }; - const handleReactionCurrentState = useCallback( - (actionName = '') => { - const isActive = !!actionName; - handleCurrentMsgAction(actionName); - handleMenuOpen(isActive); - setShowEmojis(isActive); - }, - [handleCurrentMsgAction, handleMenuOpen], - ); + const handleReactionCurrentState = (actionName = '') => { + const isActive = !!actionName; + handleCurrentMsgAction(actionName); + handleMenuOpen(isActive); + setShowEmojis(isActive); + }; + + const handleEmojiBtnClick = (event: React.MouseEvent) => { + event.stopPropagation(); + const selectedMsgActionName = event.currentTarget.dataset.uieName; + if (currentMsgActionName === selectedMsgActionName) { + // reset on double click + handleReactionCurrentState(''); + } else if (selectedMsgActionName) { + handleReactionCurrentState(selectedMsgActionName); + showReactions(event.currentTarget.getBoundingClientRect()); + } + }; - const handleEmojiBtnClick = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation(); - const selectedMsgActionName = event.currentTarget.dataset.uieName; + const handleEmojiKeyDown = (event: React.KeyboardEvent) => { + event.stopPropagation(); + const selectedMsgActionName = event.currentTarget.dataset.uieName; + handleKeyDown(event); + if ([KEY.SPACE, KEY.ENTER].includes(event.key)) { if (currentMsgActionName === selectedMsgActionName) { // reset on double click handleReactionCurrentState(''); @@ -105,72 +112,47 @@ const MessageReactions: FC = ({ handleReactionCurrentState(selectedMsgActionName); showReactions(event.currentTarget.getBoundingClientRect()); } - }, - [currentMsgActionName, handleReactionCurrentState], - ); - - const handleEmojiKeyDown = useCallback( - (event: React.KeyboardEvent) => { - event.stopPropagation(); - const selectedMsgActionName = event.currentTarget.dataset.uieName; - handleKeyDown(event); - if ([KEY.SPACE, KEY.ENTER].includes(event.key)) { - if (currentMsgActionName === selectedMsgActionName) { - // reset on double click - handleReactionCurrentState(''); - } else if (selectedMsgActionName) { - handleReactionCurrentState(selectedMsgActionName); - showReactions(event.currentTarget.getBoundingClientRect()); - } - } - }, - [currentMsgActionName, handleKeyDown, handleReactionCurrentState], - ); + } + }; const showReactions = (rect: DOMRect) => { setPOSX(rect.x); setPOSY(rect.y); }; - const handleMsgActionClick = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation(); - const actionType = event.currentTarget.dataset.uieName; - switch (actionType) { - case MessageActionsId.EMOJI: - handleEmojiBtnClick(event); - break; - case MessageActionsId.THUMBSUP: - toggleActiveMenu(event); - handleReactionClick(thumbsUpEmoji); - break; - case MessageActionsId.HEART: - toggleActiveMenu(event); - handleReactionClick(likeEmoji); - break; - } - }, - [handleEmojiBtnClick, handleReactionClick, toggleActiveMenu], - ); + const handleMsgActionClick = (event: React.MouseEvent) => { + event.stopPropagation(); + const actionType = event.currentTarget.dataset.uieName; + switch (actionType) { + case MessageActionsId.EMOJI: + handleEmojiBtnClick(event); + break; + case MessageActionsId.THUMBSUP: + toggleActiveMenu(event); + handleReactionClick(thumbsUpEmoji); + break; + case MessageActionsId.HEART: + toggleActiveMenu(event); + handleReactionClick(likeEmoji); + break; + } + }; - const handleMsgActionKeyDown = useCallback( - (event: React.KeyboardEvent) => { - event.stopPropagation(); - const actionType = event.currentTarget.dataset.uieName; - switch (actionType) { - case MessageActionsId.EMOJI: - handleEmojiKeyDown(event); - break; - case MessageActionsId.THUMBSUP: - handleKeyDown(event); - break; - case MessageActionsId.HEART: - handleKeyDown(event); - break; - } - }, - [handleEmojiKeyDown, handleKeyDown], - ); + const handleMsgActionKeyDown = (event: React.KeyboardEvent) => { + event.stopPropagation(); + const actionType = event.currentTarget.dataset.uieName; + switch (actionType) { + case MessageActionsId.EMOJI: + handleEmojiKeyDown(event); + break; + case MessageActionsId.THUMBSUP: + handleKeyDown(event); + break; + case MessageActionsId.HEART: + handleKeyDown(event); + break; + } + }; return ( <> diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.test.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.test.tsx index 19b1a74d45d..0bd8c913920 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.test.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.test.tsx @@ -20,15 +20,20 @@ import {render, fireEvent, within} from '@testing-library/react'; import {withTheme} from 'src/script/auth/util/test/TestUtil'; -import {createUuid} from 'Util/uuid'; +import {ReactionMap} from 'src/script/storage'; +import {generateQualifiedId} from 'test/helper/UserGenerator'; import {MessageReactionsList, MessageReactionsListProps} from './MessageReactionsList'; -const reactions = { - '1': '😇,😊', - '2': '😊,👍,😉,😇', - '3': '😇', -}; +const user1 = generateQualifiedId(); +const user2 = generateQualifiedId(); +const user3 = generateQualifiedId(); +const reactions: ReactionMap = [ + ['😇', [user1, user2, user3]], + ['😊', [user1, user2]], + ['👍', [user2]], + ['😉', [user2]], +]; const defaultProps: MessageReactionsListProps = { reactions: reactions, @@ -37,7 +42,7 @@ const defaultProps: MessageReactionsListProps = { isMessageFocused: false, onLastReactionKeyEvent: jest.fn(), isRemovedFromConversation: false, - userId: createUuid(), + selfUserId: generateQualifiedId(), }; describe('MessageReactionsList', () => { diff --git a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.tsx b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.tsx index 4c3b49442cf..5a2b95d7897 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactionsList.tsx @@ -19,16 +19,19 @@ import {FC} from 'react'; +import type {QualifiedId} from '@wireapp/api-client/lib/user/'; + +import {ReactionMap} from 'src/script/storage'; import {getEmojiUnicode} from 'Util/EmojiUtil'; -import {Reactions, groupByReactionUsers, sortReactionsByUserCount} from 'Util/ReactionUtil'; +import {matchQualifiedIds} from 'Util/QualifiedId'; import {EmojiPill} from './EmojiPill'; import {messageReactionWrapper} from './MessageReactions.styles'; export interface MessageReactionsListProps { - reactions: Reactions; + reactions: ReactionMap; handleReactionClick: (emoji: string) => void; - userId: string; + selfUserId: QualifiedId; isMessageFocused: boolean; onTooltipReactionCountClick: () => void; onLastReactionKeyEvent: () => void; @@ -36,20 +39,14 @@ export interface MessageReactionsListProps { } const MessageReactionsList: FC = ({reactions, ...props}) => { - const reactionGroupedByUser = groupByReactionUsers(reactions); - const reactionsGroupedByUserArray = Array.from(reactionGroupedByUser); - const reactionsList = - reactionsGroupedByUserArray.length > 1 - ? sortReactionsByUserCount(reactionsGroupedByUserArray) - : reactionsGroupedByUserArray; - const {userId, ...emojiPillProps} = props; + const {selfUserId, ...emojiPillProps} = props; return (
- {reactionsList.map(([emoji, users], index) => { + {reactions.map(([emoji, users], index) => { const emojiUnicode = getEmojiUnicode(emoji); - const emojiListCount = reactionsList.length; - const hasUserReacted = users.includes(userId); + const emojiListCount = users.length; + const hasUserReacted = users.some(user => matchQualifiedIds(selfUserId, user)); return ( = ({
- {is1to1 && selfUser?.inTeam() ? ( + {is1to1 && selfUser?.teamId ? ( void; selfUser: User; user: User; + teamState?: TeamState; } function createPlaceholder1to1Conversation(user: User, selfUser: User) { @@ -96,13 +99,13 @@ const UserActions: React.FC = ({ onAction, conversationRoleRepository, selfUser, + teamState = container.resolve(TeamState), }) => { const { isAvailable, isBlocked, isCanceled, isRequest, - isTeamMember, isTemporaryGuest, isUnknown, isConnected, @@ -111,7 +114,6 @@ const UserActions: React.FC = ({ } = useKoSubscribableChildren(user, [ 'isAvailable', 'isTemporaryGuest', - 'isTeamMember', 'isBlocked', 'isOutgoingRequest', 'isIncomingRequest', @@ -120,6 +122,7 @@ const UserActions: React.FC = ({ 'isUnknown', 'isConnected', ]); + const isTeamMember = teamState.isInTeam(user); const isNotMe = !user.isMe && isSelfActivated; diff --git a/src/script/components/panel/UserDetails.tsx b/src/script/components/panel/UserDetails.tsx index 70007ca0bff..becc0b73d6a 100644 --- a/src/script/components/panel/UserDetails.tsx +++ b/src/script/components/panel/UserDetails.tsx @@ -21,6 +21,7 @@ import React, {useEffect} from 'react'; import {amplify} from 'amplify'; import {ErrorBoundary} from 'react-error-boundary'; +import {container} from 'tsyringe'; import {WebAppEvents} from '@wireapp/webapp-events'; @@ -30,6 +31,7 @@ import {ErrorFallback} from 'Components/ErrorFallback'; import {Icon} from 'Components/Icon'; import {UserClassifiedBar} from 'Components/input/ClassifiedBar'; import {UserName} from 'Components/UserName'; +import {TeamState} from 'src/script/team/TeamState'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; @@ -43,6 +45,7 @@ interface UserDetailsProps { isVerified?: boolean; participant: User; avatarStyles?: React.CSSProperties; + teamState?: TeamState; } const UserDetailsComponent: React.FC = ({ @@ -52,9 +55,9 @@ const UserDetailsComponent: React.FC = ({ isGroupAdmin, avatarStyles, classifiedDomains, + teamState = container.resolve(TeamState), }) => { const user = useKoSubscribableChildren(participant, [ - 'inTeam', 'isGuest', 'isTemporaryGuest', 'expirationText', @@ -69,13 +72,10 @@ const UserDetailsComponent: React.FC = ({ amplify.publish(WebAppEvents.USER.UPDATE, participant.qualifiedId); }, [participant]); - const isFederated = participant.isFederated; - const isGuest = !isFederated && user.isGuest; - return (
- {user.inTeam ? ( + {teamState.isInTeam(participant) ? ( = ({
)} - {isFederated && ( + {participant.isFederated && (
{t('conversationFederationIndicator')}
)} - {isGuest && user.isAvailable && !isFederated && ( + {user.isGuest && user.isAvailable && (
{t('conversationGuestIndicator')} diff --git a/src/script/components/userDevices/DeviceDetails.tsx b/src/script/components/userDevices/DeviceDetails.tsx index b9d35a0ebbb..126fec3705b 100644 --- a/src/script/components/userDevices/DeviceDetails.tsx +++ b/src/script/components/userDevices/DeviceDetails.tsx @@ -24,6 +24,7 @@ import type {DexieError} from 'dexie'; import {container} from 'tsyringe'; import {Icon} from 'Components/Icon'; +import {isMLSConversation} from 'src/script/conversation/ConversationSelectors'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; import type {Logger} from 'Util/Logger'; @@ -100,7 +101,8 @@ const DeviceDetails: React.FC = ({ } }; - const isMLSConversation = !!conversationState.activeConversation()?.isUsingMLSProtocol; + const activeConversation = conversationState.activeConversation(); + const isConversationMLS = activeConversation && isMLSConversation(activeConversation); return (
@@ -157,7 +159,7 @@ const DeviceDetails: React.FC = ({ style={{display: isResettingSession ? 'initial' : 'none'}} data-uie-name="status-loading" /> - {!isMLSConversation && ( + {!isConversationMLS && (
)} diff --git a/src/script/page/MainContent/panels/preferences/AboutPreferences.tsx b/src/script/page/MainContent/panels/preferences/AboutPreferences.tsx index 4b59e66f162..e84f03cb97a 100644 --- a/src/script/page/MainContent/panels/preferences/AboutPreferences.tsx +++ b/src/script/page/MainContent/panels/preferences/AboutPreferences.tsx @@ -19,9 +19,11 @@ import React, {useMemo} from 'react'; +import {container} from 'tsyringe'; + import {Link, LinkVariant} from '@wireapp/react-ui-kit'; -import {useKoSubscribableChildren} from 'Util/ComponentUtil'; +import {TeamState} from 'src/script/team/TeamState'; import {t} from 'Util/LocalizerUtil'; import {PreferencesPage} from './components/PreferencesPage'; @@ -33,10 +35,11 @@ import {getPrivacyPolicyUrl, getTermsOfUsePersonalUrl, getTermsOfUseTeamUrl, URL interface AboutPreferencesProps { selfUser: User; + teamState: TeamState; } -const AboutPreferences: React.FC = ({selfUser}) => { - const {inTeam} = useKoSubscribableChildren(selfUser, ['inTeam']); +const AboutPreferences: React.FC = ({selfUser, teamState = container.resolve(TeamState)}) => { + const inTeam = teamState.isInTeam(selfUser); const config = Config.getConfig(); const websiteUrl = URL.WEBSITE; const privacyPolicyUrl = getPrivacyPolicyUrl(); diff --git a/src/script/page/MainContent/panels/preferences/accountPreferences/UsernameInput.tsx b/src/script/page/MainContent/panels/preferences/accountPreferences/UsernameInput.tsx index 972f8a1d268..49bcf6b864a 100644 --- a/src/script/page/MainContent/panels/preferences/accountPreferences/UsernameInput.tsx +++ b/src/script/page/MainContent/panels/preferences/accountPreferences/UsernameInput.tsx @@ -123,7 +123,7 @@ const UsernameInput: React.FC = ({username, domain, userRepo isDone={usernameInputDone.isDone} onValueChange={changeUsername} maxLength={256 - (domain?.length ?? 0)} - allowedChars="0-9a-zA-Z_" + allowedChars="0-9a-zA-Z_.-" fieldName="username" /> {canEditProfile && ( diff --git a/src/script/page/RightSidebar/ConversationDetails/ConversationDetails.tsx b/src/script/page/RightSidebar/ConversationDetails/ConversationDetails.tsx index 8ba1633d693..ab4bd729287 100644 --- a/src/script/page/RightSidebar/ConversationDetails/ConversationDetails.tsx +++ b/src/script/page/RightSidebar/ConversationDetails/ConversationDetails.tsx @@ -136,7 +136,7 @@ const ConversationDetails = forwardRef 'firstUserEntity', ]); - const teamId = activeConversation.team_id; + const teamId = activeConversation.teamId; const { isTeam, @@ -438,7 +438,7 @@ const ConversationDetails = forwardRef !!( isSingleUserMode && firstParticipant && - (firstParticipant.isConnected() || firstParticipant.inTeam()) + (firstParticipant.isConnected() || teamState.isInTeam(firstParticipant)) ) } showDevices={openParticipantDevices} diff --git a/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts b/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts index 697b0687e07..23889d37e2c 100644 --- a/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts +++ b/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts @@ -61,7 +61,7 @@ const getConversationActions = ( }, }, { - condition: true, + condition: !conversationEntity.is_archived(), item: { click: async () => actionsViewModel.archiveConversation(conversationEntity), icon: 'archive-icon', @@ -69,6 +69,15 @@ const getConversationActions = ( label: t('conversationDetailsActionArchive'), }, }, + { + condition: conversationEntity.is_archived(), + item: { + click: async () => actionsViewModel.unarchiveConversation(conversationEntity), + icon: 'archive-icon', + identifier: 'do-unarchive', + label: t('conversationsPopoverUnarchive'), + }, + }, { condition: conversationEntity.isRequest(), item: { diff --git a/src/script/page/RightSidebar/GuestServicesOptions/components/GuestOptions/GuestOptions.tsx b/src/script/page/RightSidebar/GuestServicesOptions/components/GuestOptions/GuestOptions.tsx index 0702a0fdeae..93242d22a50 100644 --- a/src/script/page/RightSidebar/GuestServicesOptions/components/GuestOptions/GuestOptions.tsx +++ b/src/script/page/RightSidebar/GuestServicesOptions/components/GuestOptions/GuestOptions.tsx @@ -20,6 +20,7 @@ import {FC, useCallback, useEffect, useMemo, useState} from 'react'; import cx from 'classnames'; +import {container} from 'tsyringe'; import {Button, ButtonVariant} from '@wireapp/react-ui-kit'; @@ -28,6 +29,7 @@ import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {RadioGroup} from 'Components/Radio'; import {SelectText} from 'Components/SelectText'; import {BaseToggle} from 'Components/toggle/BaseToggle'; +import {TeamState} from 'src/script/team/TeamState'; import {copyText} from 'Util/ClipboardUtil'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; @@ -56,6 +58,7 @@ interface GuestOptionsProps { isTeamStateGuestLinkEnabled?: boolean; isToggleDisabled?: boolean; isPasswordSupported?: boolean; + teamState?: TeamState; } const GuestOptions: FC = ({ @@ -68,23 +71,25 @@ const GuestOptions: FC = ({ isTeamStateGuestLinkEnabled = false, isToggleDisabled = false, isPasswordSupported = false, + teamState = container.resolve(TeamState), }) => { const [isLinkCopied, setIsLinkCopied] = useState(false); const [conversationHasGuestLinkEnabled, setConversationHasGuestLinkEnabled] = useState(false); const [optionPasswordSecured, setOptionPasswordSecured] = useState( PasswordPreference.PASSWORD_SECURED, ); - const {accessCode, accessCodeHasPassword, hasGuest, inTeam, isGuestAndServicesRoom, isGuestRoom, isServicesRoom} = + const {accessCode, accessCodeHasPassword, hasGuest, isGuestAndServicesRoom, isGuestRoom, isServicesRoom} = useKoSubscribableChildren(activeConversation, [ 'accessCode', 'accessCodeHasPassword', 'hasGuest', - 'inTeam', 'isGuestAndServicesRoom', 'isGuestRoom', 'isServicesRoom', ]); + const inTeam = teamState.isInTeam(activeConversation); + const isGuestEnabled = isGuestRoom || isGuestAndServicesRoom; const isGuestLinkEnabled = inTeam ? isTeamStateGuestLinkEnabled diff --git a/src/script/page/RightSidebar/MessageDetails/MessageDetails.test.tsx b/src/script/page/RightSidebar/MessageDetails/MessageDetails.test.tsx index 56e56bf67e4..6c01fe67082 100644 --- a/src/script/page/RightSidebar/MessageDetails/MessageDetails.test.tsx +++ b/src/script/page/RightSidebar/MessageDetails/MessageDetails.test.tsx @@ -66,7 +66,7 @@ const getDefaultParams = (showReactions: boolean = false) => { describe('MessageDetails', () => { it('renders no reactions view', async () => { const conversation = new Conversation(); - conversation.team_id = 'mock-team-id'; + conversation.teamId = 'mock-team-id'; const timestamp = new Date('2022-01-21T15:08:14.225Z').getTime(); const userName = 'Jan Kowalski'; @@ -78,12 +78,12 @@ describe('MessageDetails', () => { message.timestamp(timestamp); message.user(user); - const getUsersById = jest.fn(async (ids: QualifiedId[]) => { + const findUsersByIds = jest.fn((ids: QualifiedId[]) => { return ids.map(id => new User(id.id, 'test-domain.mock')); }); const userRepository = { - getUsersById, + findUsersByIds, } as unknown as UserRepository; const defaultProps = getDefaultParams(); diff --git a/src/script/page/RightSidebar/MessageDetails/MessageDetails.tsx b/src/script/page/RightSidebar/MessageDetails/MessageDetails.tsx index c39aa5c8c5f..e2edd85031e 100644 --- a/src/script/page/RightSidebar/MessageDetails/MessageDetails.tsx +++ b/src/script/page/RightSidebar/MessageDetails/MessageDetails.tsx @@ -17,40 +17,25 @@ * */ -import {FC, Fragment, useCallback, useEffect, useMemo, useState} from 'react'; +import {FC, useMemo, useState} from 'react'; -import type {QualifiedId} from '@wireapp/api-client/lib/user/'; -import {amplify} from 'amplify'; import cx from 'classnames'; -import {WebAppEvents} from '@wireapp/webapp-events'; - import {FadingScrollbar} from 'Components/FadingScrollbar'; import {Icon} from 'Components/Icon'; -import {EmojiImg} from 'Components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiImg'; -import { - messageReactionDetailsMargin, - reactionsCountAlignment, -} from 'Components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.styles'; import {UserSearchableList} from 'Components/UserSearchableList'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; -import {getEmojiTitleFromEmojiUnicode, getEmojiUnicode} from 'Util/EmojiUtil'; import {t} from 'Util/LocalizerUtil'; -import {getEmojiUrl, groupByReactionUsers} from 'Util/ReactionUtil'; -import {capitalizeFirstChar} from 'Util/StringUtil'; import {formatLocale} from 'Util/TimeUtil'; -import {panelContentTitleStyles} from './MessageDetails.styles'; +import {UsersReactions} from './UserReactions'; import {ConversationRepository} from '../../../conversation/ConversationRepository'; import {Conversation} from '../../../entity/Conversation'; import {ContentMessage} from '../../../entity/message/ContentMessage'; -import {Message} from '../../../entity/message/Message'; import {User} from '../../../entity/User'; -import {isContentMessage} from '../../../guards/Message'; import {SuperType} from '../../../message/SuperType'; import {SearchRepository} from '../../../search/SearchRepository'; -import {UserReactionMap} from '../../../storage'; import {TeamRepository} from '../../../team/TeamRepository'; import {UserRepository} from '../../../user/UserRepository'; import {PanelHeader} from '../PanelHeader'; @@ -66,19 +51,6 @@ const MESSAGE_STATES = { const formatUserCount = (users: User[]): string => (users.length ? ` (${users.length})` : ''); -const getTotalReactionUsersCount = (reactions: Map): number => { - let total = 0; - reactions.forEach(reaction => { - total += reaction.length; - }); - return total; -}; - -const formatReactionCount = (reactions: Map): string => { - const total = getTotalReactionUsersCount(reactions); - return total ? ` (${total})` : ''; -}; - const sortUsers = (userA: User, userB: User): number => userA.name().localeCompare(userB.name(), undefined, {sensitivity: 'base'}); @@ -94,7 +66,6 @@ interface MessageDetailsProps { userRepository: UserRepository; showReactions?: boolean; selfUser: User; - updateEntity: (message: Message) => void; togglePanel: (state: PanelState, entity: PanelEntity, addMode?: boolean) => void; } @@ -108,13 +79,8 @@ const MessageDetails: FC = ({ userRepository, selfUser, onClose, - updateEntity, togglePanel, }) => { - const [receiptUsers, setReceiptUsers] = useState([]); - const [reactionUsers, setReactionUsers] = useState>(new Map()); - const [messageId, setMessageId] = useState(messageEntity.id); - const [isReceiptsOpen, setIsReceiptsOpen] = useState(!showReactions); const { @@ -124,10 +90,15 @@ const MessageDetails: FC = ({ readReceipts, edited_timestamp: editedTimestamp, } = useKoSubscribableChildren(messageEntity, ['timestamp', 'user', 'reactions', 'readReceipts', 'edited_timestamp']); + const totalNbReactions = reactions.reduce((acc, [, users]) => acc + users.length, 0); - const teamId = activeConversation.team_id; + const teamId = activeConversation.teamId; const supportsReceipts = messageSender.isMe && teamId; + const receiptUsers = userRepository + .findUsersByIds(readReceipts.map(({userId, domain}) => ({domain: domain || '', id: userId}))) + .sort(sortUsers); + const supportsReactions = useMemo(() => { const isPing = messageEntity.super_type === SuperType.PING; const isEphemeral = messageEntity?.isEphemeral(); @@ -144,36 +115,10 @@ const MessageDetails: FC = ({ return receiptUsers.length ? MESSAGE_STATES.RECEIPTS : MESSAGE_STATES.NO_RECEIPTS; } - return getTotalReactionUsersCount(reactionUsers) ? MESSAGE_STATES.REACTIONS : MESSAGE_STATES.NO_REACTIONS; - }, [supportsReceipts, isReceiptsOpen, messageEntity, receiptUsers, reactionUsers]); - - const getReactions = useCallback(async (reactions: UserReactionMap) => { - const usersMap = new Map(); - const currentReactions = Object.keys(reactions); - const usersReactions = await userRepository.getUsersById( - currentReactions.map(userId => ({domain: '', id: userId})), - ); - usersReactions.forEach(user => { - usersMap.set(user.id, user); - }); - const reactionsGroupByUser = groupByReactionUsers(reactions); - const reactionsGroupByUserMap = new Map(); - reactionsGroupByUser.forEach((userIds, reaction) => { - reactionsGroupByUserMap.set( - reaction, - userIds.map(userId => usersMap.get(userId)!), - ); - }); - - setReactionUsers(reactionsGroupByUserMap); - }, []); + return reactions.length > 0 ? MESSAGE_STATES.REACTIONS : MESSAGE_STATES.NO_REACTIONS; + }, [supportsReceipts, isReceiptsOpen, reactions.length, messageEntity.expectsReadConfirmation, receiptUsers.length]); const receiptTimes = useMemo(() => { - const userIds: QualifiedId[] = readReceipts.map(({userId, domain}) => ({domain: domain || '', id: userId})); - userRepository.getUsersById(userIds).then((users: User[]) => { - setReceiptUsers(users.sort(sortUsers)); - }); - return readReceipts.reduce>((times, {userId, time}) => { times[userId] = formatTime(time); return times; @@ -186,7 +131,7 @@ const MessageDetails: FC = ({ 'messageDetailsTitleReceipts', messageEntity?.expectsReadConfirmation ? formatUserCount(receiptUsers) : '', ); - const reactionsTitle = t('messageDetailsTitleReactions', formatReactionCount(reactionUsers)); + const reactionsTitle = t('messageDetailsTitleReactions', totalNbReactions > 0 ? ` (${totalNbReactions})` : ''); const panelTitle = useMemo(() => { if (!supportsReceipts) { @@ -208,28 +153,6 @@ const MessageDetails: FC = ({ const onReactions = () => setIsReceiptsOpen(false); - useEffect(() => { - if (supportsReactions && reactions) { - getReactions(reactions); - } - }, [getReactions, supportsReactions, reactions]); - - useEffect(() => { - amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.UPDATED, (oldId: string, updatedMessageEntity: Message) => { - // listen for any changes to local message entities. - // if the id of the message being viewed has changed, we store the new ID. - if (oldId === messageId) { - updateEntity(updatedMessageEntity); - setMessageId(updatedMessageEntity.id); - - if (supportsReactions && isContentMessage(updatedMessageEntity)) { - const messageReactions = updatedMessageEntity.reactions(); - getReactions(messageReactions); - } - } - }); - }, [messageId, supportsReactions]); - const onParticipantClick = (userEntity: User) => togglePanel(PanelState.GROUP_PARTICIPANT_USER, userEntity); return ( @@ -275,35 +198,14 @@ const MessageDetails: FC = ({ /> )} - {messageState === MESSAGE_STATES.REACTIONS && - Array.from(reactionUsers).map(reactions => { - const [reactionKey, users] = reactions; - const emojiUnicode = getEmojiUnicode(reactionKey); - const emojiUrl = getEmojiUrl(emojiUnicode); - const emojiName = getEmojiTitleFromEmojiUnicode(emojiUnicode); - const capitalizedEmojiName = capitalizeFirstChar(emojiName); - const emojiCount = users.length; - return ( - -
- - {capitalizedEmojiName} - ({emojiCount}) -
- -
- ); - })} + {messageState === MESSAGE_STATES.REACTIONS && ( + userRepository.findUsersByIds(ids)} + onParticipantClick={onParticipantClick} + /> + )} {messageState === MESSAGE_STATES.NO_RECEIPTS && (
diff --git a/src/script/page/RightSidebar/MessageDetails/UserReactions.tsx b/src/script/page/RightSidebar/MessageDetails/UserReactions.tsx new file mode 100644 index 00000000000..9e4df5d5b68 --- /dev/null +++ b/src/script/page/RightSidebar/MessageDetails/UserReactions.tsx @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {Fragment} from 'react'; + +import type {QualifiedId} from '@wireapp/api-client/lib/user'; + +import {EmojiImg} from 'Components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/EmojiImg'; +import { + messageReactionDetailsMargin, + reactionsCountAlignment, +} from 'Components/MessagesList/Message/ContentMessage/MessageActions/MessageReactions/MessageReactions.styles'; +import {UserList} from 'Components/UserList'; +import {User} from 'src/script/entity/User'; +import {ReactionMap} from 'src/script/storage'; +import {getEmojiTitleFromEmojiUnicode, getEmojiUnicode} from 'Util/EmojiUtil'; +import {getEmojiUrl} from 'Util/ReactionUtil'; +import {capitalizeFirstChar} from 'Util/StringUtil'; + +import {panelContentTitleStyles} from './MessageDetails.styles'; + +interface UsersReactionsProps { + reactions: ReactionMap; + findUsers: (userId: QualifiedId[]) => User[]; + selfUser: User; + onParticipantClick: (user: User) => void; +} + +export function UsersReactions({reactions, selfUser, findUsers, onParticipantClick}: UsersReactionsProps) { + return reactions.map(reaction => { + const [reactionKey, userIds] = reaction; + const emojiUnicode = getEmojiUnicode(reactionKey); + const emojiUrl = getEmojiUrl(emojiUnicode); + const emojiName = getEmojiTitleFromEmojiUnicode(emojiUnicode); + const capitalizedEmojiName = capitalizeFirstChar(emojiName); + const users = findUsers(userIds); + const emojiCount = users.length; + + return ( + +
+ + {capitalizedEmojiName} + ({emojiCount}) +
+
+ +
+
+ ); + }); +} diff --git a/src/script/page/RightSidebar/RightSidebar.tsx b/src/script/page/RightSidebar/RightSidebar.tsx index fd94d94ffc9..e4851ba6e5e 100644 --- a/src/script/page/RightSidebar/RightSidebar.tsx +++ b/src/script/page/RightSidebar/RightSidebar.tsx @@ -312,7 +312,6 @@ const RightSidebar: FC = ({ selfUser={selfUser} conversationRepository={conversationRepository} messageEntity={messageEntity} - updateEntity={rightSidebar.updateEntity} teamRepository={teamRepository} searchRepository={searchRepository} showReactions={rightSidebar.showReactions} diff --git a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeHandler/Features/E2EIdentity.ts b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeHandler/Features/E2EIdentity.ts index 8e5344cf84e..474bd00cf5a 100644 --- a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeHandler/Features/E2EIdentity.ts +++ b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeHandler/Features/E2EIdentity.ts @@ -22,13 +22,13 @@ import {FeatureStatus, FEATURE_KEY, FeatureList} from '@wireapp/api-client/lib/t import {E2EIHandler} from 'src/script/E2EIdentity'; import {Logger} from 'Util/Logger'; -import {isFeatureMLSE2EI, isFeatureMLS} from '../../guards'; +import {hasE2EIVerificationExpiration, hasMLSDefaultProtocol} from '../../guards'; export const handleE2EIdentityFeatureChange = (logger: Logger, config: FeatureList) => { const e2eiConfig = config[FEATURE_KEY.MLSE2EID]; const mlsConfig = config[FEATURE_KEY.MLS]; // Check if MLS or MLS E2EIdentity feature is existent - if (!isFeatureMLSE2EI(e2eiConfig) && !isFeatureMLS(mlsConfig)) { + if (!hasE2EIVerificationExpiration(e2eiConfig) || !hasMLSDefaultProtocol(mlsConfig)) { return; } @@ -47,7 +47,7 @@ export const handleE2EIdentityFeatureChange = (logger: Logger, config: FeatureLi // Either get the current E2EIdentity handler instance or create a new one const e2eHandler = E2EIHandler.getInstance({ discoveryUrl: e2eiConfig.config.acmeDiscoveryUrl!, - gracePeriodInMS: e2eiConfig.config.verificationExpiration, + gracePeriodInSeconds: e2eiConfig.config.verificationExpiration, }); e2eHandler.initialize(); } diff --git a/src/script/page/components/FeatureConfigChange/guards.ts b/src/script/page/components/FeatureConfigChange/guards.ts index e304293950a..d07f2104988 100644 --- a/src/script/page/components/FeatureConfigChange/guards.ts +++ b/src/script/page/components/FeatureConfigChange/guards.ts @@ -19,8 +19,12 @@ import {FeatureMLS, FeatureMLSE2EId} from '@wireapp/api-client/lib/team'; -export const isFeatureMLSE2EI = (feature: any): feature is FeatureMLSE2EId => - 'config' in feature && 'verificationExpiration' in feature.config; +const isObject = (value: unknown): value is {} => typeof value === 'object' && value !== null; +const isFeatureWithConfig = (feature: unknown): feature is {config: {}} => + isObject(feature) && 'config' in feature && isObject(feature.config); -export const isFeatureMLS = (feature: any): feature is FeatureMLS => - 'config' in feature && 'defaultProtocol' in feature.config; +export const hasE2EIVerificationExpiration = (feature: unknown): feature is FeatureMLSE2EId => + isFeatureWithConfig(feature) && 'verificationExpiration' in feature.config; + +export const hasMLSDefaultProtocol = (feature: unknown): feature is FeatureMLS => + isFeatureWithConfig(feature) && 'defaultProtocol' in feature.config; diff --git a/src/script/properties/PropertiesRepository.ts b/src/script/properties/PropertiesRepository.ts index cf34afe4b42..79092b64530 100644 --- a/src/script/properties/PropertiesRepository.ts +++ b/src/script/properties/PropertiesRepository.ts @@ -115,7 +115,7 @@ export class PropertiesRepository { const isCheckConsentDisabled = !Config.getConfig().FEATURE.CHECK_CONSENT; const isPrivacyPreferenceSet = this.getPreference(PROPERTIES_TYPE.PRIVACY) !== undefined; const isTelemetryPreferenceSet = this.getPreference(PROPERTIES_TYPE.TELEMETRY_SHARING) !== undefined; - const isTeamAccount = this.selfUser().inTeam(); + const isTeamAccount = !!this.selfUser().teamId; const enablePrivacy = () => { this.savePreference(PROPERTIES_TYPE.PRIVACY, true); this.publishProperties(); diff --git a/src/script/search/SearchRepository.test.ts b/src/script/search/SearchRepository.test.ts new file mode 100644 index 00000000000..b3cc35fae6e --- /dev/null +++ b/src/script/search/SearchRepository.test.ts @@ -0,0 +1,251 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {User} from 'src/script/entity/User'; +import {generateUser} from 'test/helper/UserGenerator'; + +import {SearchRepository} from './SearchRepository'; + +import {randomInt} from '../auth/util/randomUtil'; +import {generateUsers} from '../auth/util/test/TestUtil'; +import {APIClient} from '../service/APIClientSingleton'; +import {Core} from '../service/CoreSingleton'; +import {UserRepository} from '../user/UserRepository'; + +function buildSearchRepository() { + const userRepository = {getUsersById: jest.fn(() => [])} as unknown as jest.Mocked; + const core = {backendFeatures: {isFederated: false}} as unknown as jest.Mocked; + const apiClient = {api: {user: {getSearchContacts: jest.fn()}}} as unknown as jest.Mocked; + const searchRepository = new SearchRepository(userRepository, core, apiClient); + return [searchRepository, {userRepository, core, apiClient}] as const; +} + +describe('SearchRepository', () => { + describe('searchUserInSet', () => { + const sabine = createUser('jesuissabine', 'Sabine Duchemin'); + const janina = createUser('yosoyjanina', 'Janina Felix'); + const felixa = createUser('iamfelix', 'Felix Abo'); + const felix = createUser('iamfelix', 'Felix Oulala'); + const felicien = createUser('ichbinfelicien', 'Felicien Delatour'); + const lastguy = createUser('lastfelicien', 'lastsabine lastjanina'); + const jeanpierre = createUser('jean-pierre', 'Jean-Pierre Sansbijou'); + const pierre = createUser('pierrot', 'Pierre Monsouci'); + const noMatch1 = createUser(undefined, 'yyy yyy'); + const noMatch2 = createUser('xxx', undefined); + const users = [lastguy, noMatch1, felix, felicien, sabine, janina, noMatch2, felixa, jeanpierre, pierre]; + + const tests = [ + {expected: users, term: '', testCase: 'returns the whole user list if no term is given'}, + {expected: [jeanpierre, janina, sabine, lastguy], term: 'j', testCase: 'matches multiple results'}, + { + expected: [janina, lastguy], + term: 'ja', + testCase: 'puts matches that start with the pattern on top of the list', + }, + { + expected: [felicien, felixa, felix, janina, lastguy], + term: 'fel', + testCase: 'sorts by name, handle, inside match and alphabetically', + }, + { + expected: [felixa, felix, janina], + term: 'felix', + testCase: 'sorts by firstname and lastname', + }, + { + expected: [felicien, lastguy], + term: 'felici', + testCase: 'sorts by name and inside match', + }, + { + expected: [sabine, jeanpierre, lastguy, pierre, janina], + term: 's', + testCase: 'sorts by name, handle and inside match', + }, + { + expected: [sabine, jeanpierre, lastguy], + term: 'sa', + testCase: 'puts matches that start with the pattern on top of the list', + }, + { + expected: [sabine, lastguy], + term: 'sabine', + testCase: 'puts matches that start with the pattern on top of the list', + }, + { + expected: [sabine, lastguy], + term: 'sabine', + testCase: 'puts matches that start with the pattern on top of the list', + }, + {expected: [felicien, lastguy], term: 'ic', testCase: 'matches inside the properties'}, + { + expected: [jeanpierre], + term: 'jean-pierre', + testCase: 'finds compound names', + }, + { + expected: [pierre, jeanpierre], + term: 'pierre', + testCase: 'matches compound names and prioritize matches from start', + }, + ]; + + const [searchRepository] = buildSearchRepository(); + + tests.forEach(({expected, term, testCase}) => { + it(`${testCase} term: ${term}`, () => { + const suggestions = searchRepository.searchUserInSet(term, users); + + expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); + }); + }); + + it('does not replace numbers with emojis', () => { + const [searchRepository] = buildSearchRepository(); + const felix10 = createUser('simple10', 'Felix10'); + const unsortedUsers = [felix10]; + const suggestions = searchRepository.searchUserInSet('😋', unsortedUsers); + + expect(suggestions.map(serializeUser)).toEqual([]); + }); + + it('prioritize exact matches with special characters', () => { + const [searchRepository] = buildSearchRepository(); + const smilyFelix = createUser('smily', '😋Felix'); + const atFelix = createUser('at', '@Felix'); + const simplyFelix = createUser('simple', 'Felix'); + + const unsortedUsers = [atFelix, smilyFelix, simplyFelix]; + + let suggestions = searchRepository.searchUserInSet('felix', unsortedUsers); + let expected = [simplyFelix, smilyFelix, atFelix]; + + expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); + suggestions = searchRepository.searchUserInSet('😋', unsortedUsers); + expected = [smilyFelix]; + + expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); + }); + + it('only search by handle when a handle is given', () => { + const [searchRepository] = buildSearchRepository(); + const felix = createUser('felix', 'Felix'); + const notmatching1 = createUser('notix', 'Felix'); + const notmatching2 = createUser('simple', 'Felix'); + + const unsortedUsers = [notmatching1, felix, notmatching2]; + + const suggestions = searchRepository.searchUserInSet('@felix', unsortedUsers); + const expected = [felix]; + + expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); + }); + + it('handles sorting matching results', () => { + const [searchRepository] = buildSearchRepository(); + const first = createUser('xxx', '_surname'); + const second = createUser('xxx', 'surname _lastname'); + const third = createUser('_xxx', 'surname lastname'); + const fourth = createUser('xxx', 'sur_name lastname'); + const fifth = createUser('xxx', 'surname last_name'); + const sixth = createUser('x_xx', 'surname lastname'); + + const unsortedUsers = [sixth, fifth, third, second, first, fourth]; + const expectedUsers = [first, second, third, fourth, fifth, sixth]; + + const suggestions = searchRepository.searchUserInSet('_', unsortedUsers); + + expect(suggestions.map(serializeUser)).toEqual(expectedUsers.map(serializeUser)); + }); + }); + + describe('searchByName', () => { + it('returns empty array if no users are found', async () => { + const [searchRepository, {apiClient}] = buildSearchRepository(); + jest.spyOn(apiClient.api.user, 'getSearchContacts').mockResolvedValue({response: {documents: []}} as any); + + const suggestions = await searchRepository.searchByName('term'); + + expect(suggestions).toEqual([]); + }); + + it('matches remote results with local users', async () => { + const [searchRepository, {apiClient, userRepository}] = buildSearchRepository(); + const nbUsers = randomInt(10); + const localUsers = generateUsers(nbUsers, 'domain'); + + userRepository.getUsersById.mockResolvedValue(localUsers); + const searchResults = localUsers.map(({qualifiedId}) => qualifiedId); + jest + .spyOn(apiClient.api.user, 'getSearchContacts') + .mockResolvedValue({response: {documents: searchResults}} as any); + + const suggestions = await searchRepository.searchByName('term'); + + expect(suggestions).toHaveLength(nbUsers); + }); + + it('matches exact handle match', async () => { + const [searchRepository, {apiClient, userRepository}] = buildSearchRepository(); + const localUsers = [createUser('felix', 'felix'), createUser('notfelix', 'notfelix')]; + + userRepository.getUsersById.mockResolvedValue(localUsers); + const searchResults = localUsers.map(({qualifiedId}) => qualifiedId); + jest + .spyOn(apiClient.api.user, 'getSearchContacts') + .mockResolvedValue({response: {documents: searchResults}} as any); + + const suggestions = await searchRepository.searchByName('@felix'); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toBe(localUsers[0]); + }); + + it('filters out selfUser', async () => { + const [searchRepository, {apiClient, userRepository}] = buildSearchRepository(); + const selfUser = generateUser(); + selfUser.isMe = true; + const localUsers = [generateUser(), generateUser(), generateUser(), selfUser]; + userRepository.getUsersById.mockResolvedValue(localUsers); + + const searchResults = localUsers.map(({qualifiedId}) => qualifiedId); + jest + .spyOn(apiClient.api.user, 'getSearchContacts') + .mockResolvedValue({response: {documents: searchResults}} as any); + + const suggestions = await searchRepository.searchByName('term'); + + expect(suggestions.length).toEqual(localUsers.length - 1); + }); + }); +}); + +function createUser(handle: string | undefined, name: string | undefined) { + const user = new User(); + if (handle) { + user.username(handle); + } + if (name) { + user.name(name); + } + return user; +} +function serializeUser(userEntity: User) { + return {name: userEntity.name(), username: userEntity.username()}; +} diff --git a/src/script/search/SearchRepository.ts b/src/script/search/SearchRepository.ts index b76d91481ed..21778d89445 100644 --- a/src/script/search/SearchRepository.ts +++ b/src/script/search/SearchRepository.ts @@ -17,11 +17,10 @@ * */ -import type {QualifiedId} from '@wireapp/api-client/lib/user/'; +import type {QualifiedId, SearchResult} from '@wireapp/api-client/lib/user/'; import {container} from 'tsyringe'; import {EMOJI_RANGES} from 'Util/EmojiUtil'; -import {getLogger, Logger} from 'Util/Logger'; import { computeTransliteration, replaceAccents, @@ -30,82 +29,61 @@ import { transliterationIndex, } from 'Util/StringUtil'; -import type {SearchService} from './SearchService'; - import type {User} from '../entity/User'; +import {APIClient} from '../service/APIClientSingleton'; import {Core} from '../service/CoreSingleton'; import {validateHandle} from '../user/UserHandleGenerator'; import type {UserRepository} from '../user/UserRepository'; -export class SearchRepository { - logger: Logger; - private readonly searchService: SearchService; - private readonly userRepository: UserRepository; - - static get CONFIG() { - return { - MAX_DIRECTORY_RESULTS: 30, - MAX_SEARCH_RESULTS: 10, - SEARCHABLE_FIELDS: { - NAME: 'name', - USERNAME: 'username', - }, - }; - } - - /** - * Trim and remove @. - * @param query Search string - * @returns Normalized search query - */ - static normalizeQuery(query: string): string { - if (typeof query !== 'string') { - return ''; - } - return query.trim().replace(/^[@]/, '').toLowerCase(); - } +const CONFIG = { + MAX_DIRECTORY_RESULTS: 30, + MAX_SEARCH_RESULTS: 10, + SEARCHABLE_FIELDS: { + NAME: 'name', + USERNAME: 'username', + }, +} as const; +export class SearchRepository { /** * @param searchService SearchService * @param userRepository Repository for all user interactions */ constructor( - searchService: SearchService, - userRepository: UserRepository, + private readonly userRepository: UserRepository, private readonly core = container.resolve(Core), - ) { - this.searchService = searchService; - this.userRepository = userRepository; - this.logger = getLogger('SearchRepository'); - } + private readonly apiClient = container.resolve(APIClient), + ) {} /** * Search for a user in the given user list and given a search term. * Doesn't sort the results and keep the initial order of the given user list. * - * @param term the search term - * @param userEntities entities to match the search term against - * @param properties list of properties that will be matched against the search term - * the order of the properties in the array indicates the priorities by which results will be sorted + * @param query the search term + * @param users entities to match the search term against * @returns the filtered list of users */ - searchUserInSet( - term: string, - userEntities: User[], - properties = [SearchRepository.CONFIG.SEARCHABLE_FIELDS.NAME, SearchRepository.CONFIG.SEARCHABLE_FIELDS.USERNAME], - ): User[] { - if (term === '') { - return userEntities; + searchUserInSet(term: string, users: User[]): User[] { + const {isHandleQuery, query: domainQuery} = this.normalizeQuery(term); + if (domainQuery === '') { + return users; } - const excludedEmojis = Array.from(term).reduce>((emojis, char) => { + // If the user typed a domain, we will just ignore it when searching for the user locally + const [query] = domainQuery.split('@'); + const properties = isHandleQuery + ? [CONFIG.SEARCHABLE_FIELDS.USERNAME] + : [CONFIG.SEARCHABLE_FIELDS.NAME, CONFIG.SEARCHABLE_FIELDS.USERNAME]; + + const excludedEmojis = Array.from(query).reduce>((emojis, char) => { const isEmoji = EMOJI_RANGES.includes(char); if (isEmoji) { emojis[char] = char; } return emojis; }, {}); - const termSlug = computeTransliteration(term, excludedEmojis); - const weightedResults = userEntities.reduce<{user: User; weight: number}[]>((results, userEntity) => { + + const termSlug = computeTransliteration(query, excludedEmojis); + const weightedResults = users.reduce<{user: User; weight: number}[]>((results, userEntity) => { /* given user of name Bardia and username of bardia_wire this mapping will get name & username properties and return an array value like ['Bardia', 'bardia_wire'] @@ -123,7 +101,7 @@ export class SearchRepository { const uniqueValues = Array.from(new Set(values)); const matchWeight = uniqueValues.reduce((weight, value, index) => { const propertyWeight = 10 * index + 1; - const propertyMatchWeight = this.matches(term, termSlug, excludedEmojis, value); + const propertyMatchWeight = this.matches(query, termSlug, excludedEmojis, value); return weight + propertyMatchWeight * propertyWeight; }, 0); @@ -140,6 +118,19 @@ export class SearchRepository { .map(result => result.user); } + /** + * Trim and remove @. + * @param query Search string + * @returns Normalized search query + */ + public normalizeQuery(query: string): {isHandleQuery: boolean; query: string} { + const normalizeQuery = query.trim().replace(/^[@]/, '').toLowerCase(); + return { + isHandleQuery: query.startsWith('@') && validateHandle(normalizeQuery), + query: normalizeQuery, + }; + } + private matches(term: string, termSlug: string, excludedChars?: Record, value: string = ''): number { const isStrictMatch = (value || '').toLowerCase().startsWith(term.toLowerCase()); if (isStrictMatch) { @@ -177,6 +168,11 @@ export class SearchRepository { }, 0); } + private async getContacts(query: string, numberOfRequestedUser: number, domain?: string): Promise { + const request = await this.apiClient.api.user.getSearchContacts(query, numberOfRequestedUser, domain); + return request.response; + } + /** * Search for users on the backend by name. * @note We skip a few results as connection changes need a while to reflect on the backend. @@ -186,35 +182,28 @@ export class SearchRepository { * @param maxResults Maximum number of results * @returns Resolves with the search results */ - async searchByName( - query: string, - isHandle?: boolean, - maxResults = SearchRepository.CONFIG.MAX_SEARCH_RESULTS, - ): Promise { - const [rawName, rawDomain] = this.core.backendFeatures.isFederated ? query.replace(/^@/, '').split('@') : [query]; + async searchByName(term: string, maxResults = CONFIG.MAX_SEARCH_RESULTS): Promise { + const {query, isHandleQuery} = this.normalizeQuery(term); + const [rawName, rawDomain] = this.core.backendFeatures.isFederated ? query.split('@') : [query]; const [name, domain] = validateHandle(rawName, rawDomain) ? [rawName, rawDomain] : [query]; - const matchedUserIdsFromDirectorySearch: QualifiedId[] = await this.searchService - .getContacts(name, SearchRepository.CONFIG.MAX_DIRECTORY_RESULTS, domain) - .then(({documents}) => documents.map(match => ({domain: match.qualified_id?.domain || '', id: match.id}))); - - const userIds: QualifiedId[] = [...matchedUserIdsFromDirectorySearch]; - const userEntities = await this.userRepository.getUsersById(userIds); - - return Promise.resolve(userEntities) - .then(userEntities => userEntities.filter(userEntity => !userEntity.isMe)) - .then(userEntities => { - if (isHandle) { - userEntities = userEntities.filter(userEntity => startsWith(userEntity.username(), query)); - } - - return userEntities - .sort((userA, userB) => { - return isHandle - ? sortByPriority(userA.username(), userB.username(), query) - : sortByPriority(userA.name(), userB.name(), query); - }) - .slice(0, maxResults); - }); + const userIds: QualifiedId[] = await this.getContacts(name, CONFIG.MAX_DIRECTORY_RESULTS, domain).then( + ({documents}) => documents.map(match => ({domain: match.qualified_id?.domain || '', id: match.id})), + ); + + const users = await this.userRepository.getUsersById(userIds); + + return ( + users + // Filter out selfUser + .filter(user => !user.isMe) + .filter(user => !isHandleQuery || startsWith(user.username(), query)) + .sort((userA, userB) => { + return isHandleQuery + ? sortByPriority(userA.username(), userB.username(), query) + : sortByPriority(userA.name(), userB.name(), query); + }) + .slice(0, maxResults) + ); } } diff --git a/src/script/search/SearchService.ts b/src/script/search/SearchService.ts deleted file mode 100644 index 1d4b32eeb79..00000000000 --- a/src/script/search/SearchService.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import type {SearchResult} from '@wireapp/api-client/lib/user/'; -import {container} from 'tsyringe'; - -import {APIClient} from '../service/APIClientSingleton'; - -export class SearchService { - constructor(private readonly apiClient = container.resolve(APIClient)) {} - - async getContacts(query: string, numberOfRequestedUser: number, domain?: string): Promise { - const request = await this.apiClient.api.user.getSearchContacts(query, numberOfRequestedUser, domain); - return request.response; - } -} diff --git a/src/script/storage/record/EventRecord.ts b/src/script/storage/record/EventRecord.ts index 90294702bef..ae3a6333383 100644 --- a/src/script/storage/record/EventRecord.ts +++ b/src/script/storage/record/EventRecord.ts @@ -40,7 +40,9 @@ export interface AssetRecord { token?: string; } +/** @deprecated as of Oct 2023, this is the old format we stored reactions in */ export type UserReactionMap = {[userId: string]: ReactionType}; +export type ReactionMap = [reaction: string, userIds: QualifiedId[]][]; /** * Represent an event that has been sent by the current device @@ -96,7 +98,7 @@ export type LegacyEventRecord = { previews?: string[]; qualified_conversation?: QualifiedId; qualified_from?: QualifiedId; - reactions?: UserReactionMap; + reactions?: ReactionMap | UserReactionMap; read_receipts?: ReadReceipt[]; selected_button_id?: string; server_time?: string; diff --git a/src/script/team/TeamEntity.ts b/src/script/team/TeamEntity.ts index dc5a0dd28ae..43ad1c5bcaa 100644 --- a/src/script/team/TeamEntity.ts +++ b/src/script/team/TeamEntity.ts @@ -20,7 +20,6 @@ import ko from 'knockout'; import {AssetRemoteData} from '../assets/AssetRemoteData'; -import type {User} from '../entity/User'; import {assetV3} from '../util/ValidationUtil'; export class TeamEntity { @@ -30,14 +29,12 @@ export class TeamEntity { /** Team icon (asset key) */ iconKey?: string; id?: string; - members: ko.ObservableArray; name: ko.Observable; constructor(id?: string) { this.creator = undefined; this.icon = ''; this.iconKey = undefined; - this.members = ko.observableArray([]); this.id = id; this.name = ko.observable(''); } @@ -46,7 +43,7 @@ export class TeamEntity { let hasIcon = false; try { - hasIcon = this.icon && assetV3(this.icon); + hasIcon = !!this.icon && assetV3(this.icon); } catch (error) {} if (hasIcon) { diff --git a/src/script/team/TeamMapper.ts b/src/script/team/TeamMapper.ts index 7080095b688..d8439a9c29f 100644 --- a/src/script/team/TeamMapper.ts +++ b/src/script/team/TeamMapper.ts @@ -19,14 +19,10 @@ import type {MemberData, TeamData} from '@wireapp/api-client/lib/team/'; import type {TeamUpdateData} from '@wireapp/api-client/lib/team/data/'; -import type {PermissionsData} from '@wireapp/api-client/lib/team/member/PermissionsData'; import {TeamEntity} from './TeamEntity'; import {TeamMemberEntity} from './TeamMemberEntity'; -import type {User} from '../entity/User'; -import {roleFromTeamPermissions} from '../user/UserPermission'; - export class TeamMapper { mapTeamFromObject(data: TeamData, teamEntity?: TeamEntity): TeamEntity { return this.updateTeamFromObject(data, teamEntity); @@ -81,11 +77,4 @@ export class TeamMapper { return member; } - - mapRole(userEntity: User, permissions?: PermissionsData): void { - if (permissions) { - const teamRole = roleFromTeamPermissions(permissions); - userEntity.teamRole(teamRole); - } - } } diff --git a/src/script/team/TeamRepository.ts b/src/script/team/TeamRepository.ts index d6734d10c6f..776a315756b 100644 --- a/src/script/team/TeamRepository.ts +++ b/src/script/team/TeamRepository.ts @@ -30,6 +30,7 @@ import type { } from '@wireapp/api-client/lib/event'; import {TEAM_EVENT} from '@wireapp/api-client/lib/event/TeamEvent'; import {FeatureStatus, FeatureList} from '@wireapp/api-client/lib/team/feature/'; +import type {PermissionsData} from '@wireapp/api-client/lib/team/member/PermissionsData'; import type {TeamData} from '@wireapp/api-client/lib/team/team/TeamData'; import {QualifiedId} from '@wireapp/api-client/lib/user'; import {amplify} from 'amplify'; @@ -102,44 +103,44 @@ export class TeamRepository extends TypedEventEmitter { this.userRepository = userRepository; this.userRepository.getTeamMembersFromUsers = this.getTeamMembersFromUsers; - this.teamState.teamMembers.subscribe(() => this.userRepository.mapGuestStatus()); - - this.isSelfConnectedTo = userId => { - return ( - this.teamState.memberRoles()[userId] !== ROLE.PARTNER || - this.teamState.memberInviters()[userId] === this.userState.self().id - ); - }; amplify.subscribe(WebAppEvents.TEAM.EVENT_FROM_BACKEND, this.onTeamEvent); amplify.subscribe(WebAppEvents.EVENT.NOTIFICATION_HANDLING_STATE, this.updateTeamConfig); amplify.subscribe(WebAppEvents.TEAM.UPDATE_INFO, this.sendAccountInfo.bind(this)); } - readonly getRoleBadge = (userId: string): string => { + getRoleBadge(userId: string): string { return this.teamState.isExternal(userId) ? t('rolePartner') : ''; - }; + } - readonly isSelfConnectedTo = (userId: string): boolean => { + isSelfConnectedTo(userId: string): boolean { return ( this.teamState.memberRoles()[userId] !== ROLE.PARTNER || this.teamState.memberInviters()[userId] === this.userState.self().id ); - }; + } - initTeam = async ( - teamId?: string, - ): Promise<{team: TeamEntity; members: QualifiedId[]} | {team: undefined; members: never[]}> => { + async initTeam(teamId?: string): Promise { const team = await this.getTeam(); // get the fresh feature config from backend await this.updateFeatureConfig(); if (!teamId) { - return {team: undefined, members: []}; + return []; } + this.teamState.teamMembers.subscribe(members => { + // Subscribe to team members change and update the user role and guest status + this.userRepository.mapGuestStatus(members); + const roles = this.teamState.memberRoles(); + members.forEach(user => { + if (roles[user.id]) { + user.teamRole(roles[user.id]); + } + }); + }); const members = await this.loadTeamMembers(team); this.scheduleTeamRefresh(); - return {team, members}; - }; + return members; + } private async updateFeatureConfig(): Promise<{newFeatureList: FeatureList; prevFeatureList?: FeatureList}> { const prevFeatureList = this.teamState.teamFeatures(); @@ -185,7 +186,7 @@ export class TeamRepository extends TypedEventEmitter { async getSelfMember(teamId: string): Promise { const memberEntity = await this.getTeamMember(teamId, this.userState.self().id); - this.teamMapper.mapRole(this.userState.self(), memberEntity.permissions); + this.updateUserRole(this.userState.self(), memberEntity.permissions); return memberEntity; } @@ -201,7 +202,7 @@ export class TeamRepository extends TypedEventEmitter { return this.teamService.conversationHasGuestLink(conversationId); } - getTeamMembersFromUsers = async (users: User[]): Promise => { + private getTeamMembersFromUsers = async (users: User[]): Promise => { const selfTeamId = this.userState.self().teamId; if (!selfTeamId) { return; @@ -337,18 +338,8 @@ export class TeamRepository extends TypedEventEmitter { this.teamState.memberRoles({}); this.teamState.memberInviters({}); } - const userEntities = await this.userRepository.getUsersById( - memberIds.map(memberId => ({domain: this.teamState.teamDomain(), id: memberId})), - ); - if (append) { - const knownUserIds = teamEntity.members().map(({id}) => id); - const newUserEntities = userEntities.filter(({id}) => !knownUserIds.includes(id)); - teamEntity.members.push(...newUserEntities); - } else { - teamEntity.members(userEntities); - } - this.updateMemberRoles(teamEntity, mappedMembers); + this.updateMemberRoles(mappedMembers); } private async loadTeamMembers(teamEntity: TeamEntity): Promise { @@ -356,20 +347,12 @@ export class TeamRepository extends TypedEventEmitter { this.teamState.memberRoles({}); this.teamState.memberInviters({}); - this.updateMemberRoles(teamEntity, teamMembers); + this.updateMemberRoles(teamMembers); return teamMembers .filter(({userId}) => userId !== this.userState.self().id) .map(memberEntity => ({domain: this.teamState.teamDomain() ?? '', id: memberEntity.userId})); } - private addUserToTeam(userEntity: User): void { - const members = this.teamState.team().members; - - if (!members().find(member => member.id === userEntity.id)) { - members.push(userEntity); - } - } - private getTeamById(teamId: string): Promise { return this.teamService.getTeamById(teamId); } @@ -391,7 +374,7 @@ export class TeamRepository extends TypedEventEmitter { amplify.publish(WebAppEvents.CONVERSATION.DELETE, {domain: '', id: conversationId}); } - private _onMemberJoin(eventJson: TeamMemberJoinEvent): void { + private async _onMemberJoin(eventJson: TeamMemberJoinEvent) { const { data: {user: userId}, team: teamId, @@ -400,10 +383,9 @@ export class TeamRepository extends TypedEventEmitter { const isOtherUser = this.userState.self().id !== userId; if (isLocalTeam && isOtherUser) { - this.userRepository - .getUserById({domain: this.userState.self().domain, id: userId}) - .then(userEntity => this.addUserToTeam(userEntity)); - this.getTeamMember(teamId, userId).then(member => this.updateMemberRoles(this.teamState.team(), [member])); + await this.userRepository.getUserById({domain: this.userState.self().domain, id: userId}); + const member = await this.getTeamMember(teamId, userId); + this.updateMemberRoles([member]); } } @@ -443,7 +425,6 @@ export class TeamRepository extends TypedEventEmitter { return this.onDelete(eventJson); } - this.teamState.team().members.remove(member => member.id === userId); amplify.publish(WebAppEvents.TEAM.MEMBER_LEAVE, teamId, {domain: '', id: userId}, new Date(time).toISOString()); } } @@ -454,34 +435,36 @@ export class TeamRepository extends TypedEventEmitter { team: teamId, } = eventJson; const isLocalTeam = this.teamState.team().id === teamId; + if (!isLocalTeam) { + return; + } + const isSelfUser = this.userState.self().id === userId; - if (isLocalTeam && isSelfUser) { + if (isSelfUser) { const memberEntity = permissions ? {permissions} : await this.getTeamMember(teamId, userId); - this.teamMapper.mapRole(this.userState.self(), memberEntity.permissions); + this.updateUserRole(this.userState.self(), memberEntity.permissions); await this.sendAccountInfo(); - } - if (isLocalTeam && !isSelfUser) { + } else { const member = await this.getTeamMember(teamId, userId); - this.updateMemberRoles(this.teamState.team(), [member]); + this.updateMemberRoles([member]); } } - private updateMemberRoles(team: TeamEntity, members: TeamMemberEntity[] = []): void { - members.forEach(member => { - const user = team.members().find(({id}) => member.userId === id); - if (user) { - this.teamMapper.mapRole(user, member.permissions); - } - }); + private updateUserRole(user: User, permissions: PermissionsData): void { + user.teamRole(roleFromTeamPermissions(permissions)); + } + private updateMemberRoles(members: TeamMemberEntity[] = []): void { const memberRoles = members.reduce((accumulator, member) => { accumulator[member.userId] = member.permissions ? roleFromTeamPermissions(member.permissions) : ROLE.INVALID; return accumulator; }, this.teamState.memberRoles()); const memberInvites = members.reduce((accumulator, member) => { - accumulator[member.userId] = member.invitedBy; + if (member.invitedBy) { + accumulator[member.userId] = member.invitedBy; + } return accumulator; }, this.teamState.memberInviters()); diff --git a/src/script/team/TeamState.ts b/src/script/team/TeamState.ts index d9a8d7e6d74..26f76802467 100644 --- a/src/script/team/TeamState.ts +++ b/src/script/team/TeamState.ts @@ -25,6 +25,7 @@ import {sortUsersByPriority} from 'Util/StringUtil'; import {TeamEntity} from './TeamEntity'; +import {Conversation} from '../entity/Conversation'; import {User} from '../entity/User'; import {ROLE} from '../user/UserPermission'; import {UserState} from '../user/UserState'; @@ -32,8 +33,8 @@ import {UserState} from '../user/UserState'; @singleton() export class TeamState { public readonly isTeamDeleted: ko.Observable; - public readonly memberInviters: ko.Observable; - public readonly memberRoles: ko.Observable; + public readonly memberInviters: ko.Observable>; + public readonly memberRoles: ko.Observable>; public readonly supportsLegalHold: ko.Observable; public readonly teamName: ko.PureComputed; public readonly teamFeatures: ko.Observable; @@ -51,21 +52,21 @@ export class TeamState { public readonly isAppLockEnabled: ko.PureComputed; public readonly isAppLockEnforced: ko.PureComputed; public readonly appLockInactivityTimeoutSecs: ko.PureComputed; + /** all the members of the team */ readonly teamMembers: ko.PureComputed; + /** all the members of the team + the users the selfUser is connected with */ readonly teamUsers: ko.PureComputed; readonly isTeam: ko.PureComputed; - readonly team: ko.Observable; + readonly team = ko.observable(new TeamEntity()); readonly teamDomain: ko.PureComputed; readonly teamSize: ko.PureComputed; constructor(private readonly userState = container.resolve(UserState)) { - this.team = ko.observable(); - this.isTeam = ko.pureComputed(() => !!this.team()?.id); this.isTeamDeleted = ko.observable(false); /** Note: this does not include the self user */ - this.teamMembers = ko.pureComputed(() => this.userState.users().filter(user => !user.isMe && user.isTeamMember())); + this.teamMembers = ko.pureComputed(() => this.userState.users().filter(user => !user.isMe && this.isInTeam(user))); this.memberRoles = ko.observable({}); this.memberInviters = ko.observable({}); this.teamFeatures = ko.observable(); @@ -82,10 +83,6 @@ export class TeamState { this.supportsLegalHold = ko.observable(false); - this.userState.isTeam = this.isTeam; - this.userState.teamMembers = this.teamMembers; - this.userState.teamUsers = this.teamUsers; - this.isFileSharingSendingEnabled = ko.pureComputed(() => { const status = this.teamFeatures()?.fileSharing?.status; return status ? status === FeatureStatus.ENABLED : true; @@ -136,7 +133,12 @@ export class TeamState { ); } - readonly isExternal = (userId: string): boolean => { + isInTeam(entity: User | Conversation): boolean { + const team = this.team(); + return !!team.id && entity.domain === this.teamDomain() && entity.teamId === team.id; + } + + isExternal(userId: string): boolean { return this.memberRoles()[userId] === ROLE.PARTNER; - }; + } } diff --git a/src/script/tracking/EventTrackingRepository.ts b/src/script/tracking/EventTrackingRepository.ts index d693b5e8eb8..39e9ad38fb4 100644 --- a/src/script/tracking/EventTrackingRepository.ts +++ b/src/script/tracking/EventTrackingRepository.ts @@ -39,6 +39,7 @@ import {URLParameter} from '../auth/URLParameter'; import {Config} from '../Config'; import type {ContributedSegmentations, MessageRepository} from '../conversation/MessageRepository'; import {ClientEvent} from '../event/Client'; +import {TeamState} from '../team/TeamState'; import {ROLE as TEAM_ROLE} from '../user/UserPermission'; import {UserState} from '../user/UserState'; @@ -68,6 +69,7 @@ export class EventTrackingRepository { constructor( private readonly messageRepository: MessageRepository, private readonly userState = container.resolve(UserState), + private readonly teamState = container.resolve(TeamState), ) { this.logger = getLogger('EventTrackingRepository'); @@ -78,7 +80,7 @@ export class EventTrackingRepository { readonly onUserEvent = (eventJson: any, source: EventSource) => { const type = eventJson.type; - if (type === ClientEvent.USER.DATA_TRANSFER && this.userState.isTeam()) { + if (type === ClientEvent.USER.DATA_TRANSFER && this.teamState.isTeam()) { this.migrateDeviceId(eventJson.data.trackingIdentifier); } }; @@ -159,7 +161,7 @@ export class EventTrackingRepository { } } - const isTeam = this.userState.isTeam(); + const isTeam = this.teamState.isTeam(); if (!isTeam) { return; // Countly should not be enabled for non-team users } @@ -262,10 +264,12 @@ export class EventTrackingRepository { private trackProductReportingEvent(eventName: string, customSegmentations?: ContributedSegmentations): void { if (this.isProductReportingActivated === true) { + const contacts = this.teamState.isTeam() ? this.teamState.teamUsers() : this.userState.connectedUsers(); + const nbContacts = contacts.filter(userEntity => !userEntity.isService).length; const userData = { - [UserData.IS_TEAM]: this.userState.isTeam(), - [UserData.CONTACTS]: roundLogarithmic(this.userState.numberOfContacts(), 6), - [UserData.TEAM_SIZE]: roundLogarithmic(this.userState.teamMembers().length, 6), + [UserData.IS_TEAM]: this.teamState.isTeam(), + [UserData.CONTACTS]: roundLogarithmic(nbContacts, 6), + [UserData.TEAM_SIZE]: roundLogarithmic(this.teamState.teamMembers().length, 6), [UserData.TEAM_ID]: this.userState.self().teamId, [UserData.USER_TYPE]: this.getUserType(), }; diff --git a/src/script/tracking/Helpers.ts b/src/script/tracking/Helpers.ts index 8de27259bf3..7f20d1380b6 100644 --- a/src/script/tracking/Helpers.ts +++ b/src/script/tracking/Helpers.ts @@ -42,7 +42,7 @@ export function getConversationType(conversationEntity: any): ConversationType | } } export function getGuestAttributes(conversationEntity: Conversation): GuestAttributes { - const isTeamConversation = !!conversationEntity.team_id; + const isTeamConversation = !!conversationEntity.teamId; if (isTeamConversation) { const isAllowGuests = !conversationEntity.isTeamOnly(); const _getUserType = (_conversationEntity: Conversation) => { diff --git a/src/script/user/UserMapper.test.ts b/src/script/user/UserMapper.test.ts index f4d887be017..f68ed7f25bd 100644 --- a/src/script/user/UserMapper.test.ts +++ b/src/script/user/UserMapper.test.ts @@ -81,7 +81,6 @@ describe('User Mapper', () => { ); expect(user.isFederated).toBe(true); - expect(user.inTeam()).toBe(false); }); it('can convert users with profile images marked as non public', () => { diff --git a/src/script/user/UserMapper.ts b/src/script/user/UserMapper.ts index ea047ab9ef2..3439fbd7443 100644 --- a/src/script/user/UserMapper.ts +++ b/src/script/user/UserMapper.ts @@ -17,8 +17,6 @@ * */ -import {container} from 'tsyringe'; - import {getLogger, Logger} from 'Util/Logger'; import {isSelfAPIUser} from './UserGuards'; @@ -26,7 +24,6 @@ import {isSelfAPIUser} from './UserGuards'; import {mapProfileAssets, mapProfileAssetsV1, updateUserEntityAssets} from '../assets/AssetMapper'; import {User} from '../entity/User'; import {UserRecord} from '../storage'; -import {TeamState} from '../team/TeamState'; import type {ServerTimeHandler} from '../time/serverTimeHandler'; import '../view_model/bindings/CommonBindings'; @@ -37,10 +34,7 @@ export class UserMapper { * Construct a new User Mapper. * @param serverTimeHandler Handles time shift between server and client */ - constructor( - private readonly serverTimeHandler: ServerTimeHandler, - private teamState = container.resolve(TeamState), - ) { + constructor(private readonly serverTimeHandler: ServerTimeHandler) { this.logger = getLogger('UserMapper'); } @@ -187,12 +181,8 @@ export class UserMapper { } } - const currentTeam = this.teamState.team()?.id; if (teamId) { userEntity.teamId = teamId; - if (!userEntity.isFederated && currentTeam && currentTeam === teamId) { - userEntity.inTeam(true); - } } if (deleted) { diff --git a/src/script/user/UserRepository.test.ts b/src/script/user/UserRepository.test.ts index fa8dd48bb2e..d0fa3853573 100644 --- a/src/script/user/UserRepository.test.ts +++ b/src/script/user/UserRepository.test.ts @@ -18,7 +18,7 @@ */ import {RECEIPT_MODE} from '@wireapp/api-client/lib/conversation/data'; -import {QualifiedId} from '@wireapp/api-client/lib/user'; +import type {User as APIClientUser} from '@wireapp/api-client/lib/user'; import {amplify} from 'amplify'; import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; @@ -32,34 +32,71 @@ import {matchQualifiedIds} from 'Util/QualifiedId'; import {ConsentValue} from './ConsentValue'; import {UserRepository} from './UserRepository'; +import {UserService} from './UserService'; import {UserState} from './UserState'; +import {AssetRepository} from '../assets/AssetRepository'; +import {ClientRepository} from '../client'; import {ClientMapper} from '../client/ClientMapper'; import {ConnectionEntity} from '../connection/ConnectionEntity'; import {User} from '../entity/User'; import {EventRepository} from '../event/EventRepository'; import {PropertiesRepository} from '../properties/PropertiesRepository'; - -describe('UserRepository', () => { - const testFactory = new TestFactory(); - let userRepository: UserRepository; - let userState: UserState; - - beforeAll(async () => { - userRepository = await testFactory.exposeUserActors(); - userState = userRepository['userState']; - }); - - afterEach(() => { - userRepository['userState'].users.removeAll(); +import {SelfService} from '../self/SelfService'; +import {TeamState} from '../team/TeamState'; +import {serverTimeHandler} from '../time/serverTimeHandler'; + +const testFactory = new TestFactory(); +async function buildUserRepository() { + const storageRepo = await testFactory.exposeStorageActors(); + + const userService = new UserService(storageRepo['storageService']); + const assetRepository = new AssetRepository(); + const selfService = new SelfService(); + const clientRepository = new ClientRepository({} as any, {} as any); + const propertyRepository = new PropertiesRepository({} as any, {} as any); + const userState = new UserState(); + const teamState = new TeamState(); + + const userRepository = new UserRepository( + userService, + assetRepository, + selfService, + clientRepository, + serverTimeHandler, + propertyRepository, + userState, + teamState, + ); + return [ + userRepository, + { + userService, + assetRepository, + selfService, + clientRepository, + serverTimeHandler, + propertyRepository, + userState, + teamState, + }, + ] as const; +} + +function createConnections(users: APIClientUser[]) { + return users.map(user => { + const connection = new ConnectionEntity(); + connection.userId = user.qualified_id; + return connection; }); +} +describe('UserRepository', () => { describe('Account preferences', () => { describe('Data usage permissions', () => { - it('syncs the "Send anonymous data" preference through WebSocket events', () => { - const setPropertyMock = jest - .spyOn(userRepository['propertyRepository'], 'setProperty') - .mockReturnValue(undefined); + it('syncs the "Send anonymous data" preference through WebSocket events', async () => { + const [, {propertyRepository}] = await buildUserRepository(); + const setPropertyMock = jest.spyOn(propertyRepository, 'setProperty').mockReturnValue(undefined); const turnOnErrorReporting = { key: 'webapp', type: 'user.properties-set', @@ -97,10 +134,9 @@ describe('UserRepository', () => { expect(setPropertyMock).toHaveBeenCalledWith(turnOffErrorReporting.key, turnOffErrorReporting.value); }); - it('syncs the "Receive newsletter" preference through WebSocket events', () => { - const setPropertyMock = jest - .spyOn(userRepository['propertyRepository'], 'setProperty') - .mockReturnValue(undefined); + it('syncs the "Receive newsletter" preference through WebSocket events', async () => { + const [userRepository, {propertyRepository}] = await buildUserRepository(); + const setPropertyMock = jest.spyOn(propertyRepository, 'setProperty').mockReturnValue(undefined); const deletePropertyMock = jest .spyOn(userRepository['propertyRepository'], 'deleteProperty') @@ -129,14 +165,11 @@ describe('UserRepository', () => { }); describe('Privacy', () => { - it('syncs the "Read receipts" preference through WebSocket events', () => { - const setPropertyMock = jest - .spyOn(userRepository['propertyRepository'], 'setProperty') - .mockReturnValue(undefined); + it('syncs the "Read receipts" preference through WebSocket events', async () => { + const [, {propertyRepository}] = await buildUserRepository(); + const setPropertyMock = jest.spyOn(propertyRepository, 'setProperty').mockReturnValue(undefined); - const deletePropertyMock = jest - .spyOn(userRepository['propertyRepository'], 'deleteProperty') - .mockReturnValue(undefined); + const deletePropertyMock = jest.spyOn(propertyRepository, 'deleteProperty').mockReturnValue(undefined); const turnOnReceiptMode = { key: PropertiesRepository.CONFIG.WIRE_RECEIPT_MODE.key, @@ -163,16 +196,14 @@ describe('UserRepository', () => { describe('User handling', () => { describe('findUserById', () => { let user: User; + let userRepository: UserRepository; - beforeEach(() => { + beforeEach(async () => { + [userRepository] = await buildUserRepository(); user = new User(entities.user.john_doe.id); return userRepository['saveUser'](user); }); - afterEach(() => { - userState.users.removeAll(); - }); - it('should find an existing user', () => { const userEntity = userRepository.findUserById({id: user.id, domain: ''}); @@ -187,7 +218,8 @@ describe('UserRepository', () => { }); describe('saveUser', () => { - it('saves a user', () => { + it('saves a user', async () => { + const [userRepository, {userState}] = await buildUserRepository(); const user = new User(entities.user.jane_roe.id); userRepository['saveUser'](user); @@ -196,7 +228,8 @@ describe('UserRepository', () => { expect(userState.users()[0]).toBe(user); }); - it('saves self user', () => { + it('saves self user', async () => { + const [userRepository, {userState}] = await buildUserRepository(); const user = new User(entities.user.jane_roe.id); userRepository['saveUser'](user, true); @@ -209,21 +242,27 @@ describe('UserRepository', () => { describe('loadUsers', () => { const localUsers = [generateAPIUser(), generateAPIUser(), generateAPIUser()]; + let userRepository: UserRepository; + let userState: UserState; + let userService: UserService; + beforeEach(async () => { + [userRepository, {userState, userService}] = await buildUserRepository(); jest.resetAllMocks(); - jest.spyOn(userRepository['userService'], 'loadUserFromDb').mockResolvedValue(localUsers); + jest.spyOn(userService, 'loadUserFromDb').mockResolvedValue(localUsers); const selfUser = new User('self'); selfUser.isMe = true; + userState.self(selfUser); userState.users([selfUser]); }); it('loads all users from backend even when they are already known locally', async () => { const newUsers = [generateAPIUser(), generateAPIUser()]; const users = [...localUsers, ...newUsers]; - const userIds = users.map(user => user.qualified_id!); - const fetchUserSpy = jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: users}); + const connections = createConnections(users); + const fetchUserSpy = jest.spyOn(userService, 'getUsers').mockResolvedValue({found: users}); - await userRepository.loadUsers(new User('self'), [], [], userIds); + await userRepository.loadUsers(new User('self'), connections, [], []); expect(userState.users()).toHaveLength(users.length + 1); expect(fetchUserSpy).toHaveBeenCalledWith(users.map(user => user.qualified_id!)); @@ -232,18 +271,10 @@ describe('UserRepository', () => { it('assigns connections with users', async () => { const newUsers = [generateAPIUser(), generateAPIUser()]; const users = [...localUsers, ...newUsers]; - const userIds = users.map(user => user.qualified_id!); - jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: users}); + const connections = createConnections(users); + jest.spyOn(userService, 'getUsers').mockResolvedValue({found: users}); - const createConnectionWithUser = (userId: QualifiedId) => { - const connection = new ConnectionEntity(); - connection.userId = userId; - return connection; - }; - - const connections = users.map(user => createConnectionWithUser(user.qualified_id)); - - await userRepository.loadUsers(new User('self'), connections, [], userIds); + await userRepository.loadUsers(new User('self'), connections, [], []); expect(userState.users()).toHaveLength(users.length + 1); users.forEach(user => { @@ -254,17 +285,16 @@ describe('UserRepository', () => { it('loads users that are partially stored in the DB and maps availability', async () => { const userIds = localUsers.map(user => user.qualified_id!); + const connections = createConnections(localUsers); const partialUsers = [ {id: userIds[0].id, availability: Availability.Type.AVAILABLE}, {id: userIds[1].id, availability: Availability.Type.BUSY}, ]; jest.spyOn(userRepository['userService'], 'loadUserFromDb').mockResolvedValue(partialUsers as any); - const fetchUserSpy = jest - .spyOn(userRepository['userService'], 'getUsers') - .mockResolvedValue({found: localUsers}); + const fetchUserSpy = jest.spyOn(userService, 'getUsers').mockResolvedValue({found: localUsers}); - await userRepository.loadUsers(new User('self'), [], [], userIds); + await userRepository.loadUsers(new User('self'), connections, [], []); expect(userState.users()).toHaveLength(localUsers.length + 1); expect(fetchUserSpy).toHaveBeenCalledWith(userIds); @@ -275,11 +305,11 @@ describe('UserRepository', () => { it('deletes users that are not needed', async () => { const newUsers = [generateAPIUser(), generateAPIUser()]; - const userIds = newUsers.map(user => user.qualified_id!); - const removeUserSpy = jest.spyOn(userRepository['userService'], 'removeUserFromDb').mockResolvedValue(); - jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: newUsers}); + const connections = createConnections(newUsers); + const removeUserSpy = jest.spyOn(userService, 'removeUserFromDb').mockResolvedValue(); + jest.spyOn(userService, 'getUsers').mockResolvedValue({found: newUsers}); - await userRepository.loadUsers(new User(), [], [], userIds); + await userRepository.loadUsers(new User(), connections, [], []); expect(userState.users()).toHaveLength(newUsers.length + 1); expect(removeUserSpy).toHaveBeenCalledTimes(localUsers.length); @@ -290,12 +320,10 @@ describe('UserRepository', () => { }); describe('assignAllClients', () => { - let userJaneRoe: User; - let userJohnDoe: User; - - beforeEach(() => { - userJaneRoe = new User(entities.user.jane_roe.id); - userJohnDoe = new User(entities.user.john_doe.id); + it('assigns all available clients to the users', async () => { + const [userRepository, {clientRepository}] = await buildUserRepository(); + const userJaneRoe = new User(entities.user.jane_roe.id); + const userJohnDoe = new User(entities.user.john_doe.id); userRepository['saveUsers']([userJaneRoe, userJohnDoe]); const permanent_client = ClientMapper.mapClient(entities.clients.john_doe.permanent, false); @@ -306,12 +334,10 @@ describe('UserRepository', () => { [entities.user.jane_roe.id]: [plain_client], }; - spyOn(testFactory.client_repository!, 'getAllClientsFromDb').and.returnValue(Promise.resolve(recipients)); - }); + jest.spyOn(clientRepository, 'getAllClientsFromDb').mockResolvedValue(recipients); - it('assigns all available clients to the users', () => { return userRepository.assignAllClients().then(() => { - expect(testFactory.client_repository!.getAllClientsFromDb).toHaveBeenCalled(); + expect(clientRepository.getAllClientsFromDb).toHaveBeenCalled(); expect(userJaneRoe.devices().length).toBe(1); expect(userJaneRoe.devices()[0].id).toBe(entities.clients.jane_roe.plain.id); expect(userJohnDoe.devices().length).toBe(2); @@ -323,41 +349,22 @@ describe('UserRepository', () => { describe('verify_username', () => { it('resolves with username when username is not taken', async () => { + const [userRepository, {userService}] = await buildUserRepository(); const expectedUsername = 'john_doe'; const notFoundError = new Error('not found') as any; notFoundError.response = {status: HTTP_STATUS.NOT_FOUND}; - const userRepo = new UserRepository( - { - checkUserHandle: jest.fn().mockImplementation(() => Promise.reject(notFoundError)), - } as any, // UserService - {} as any, // AssetRepository, - {} as any, // SelfService, - {} as any, // ClientRepository, - {} as any, // ServerTimeHandler, - {} as any, // PropertiesRepository, - {} as any, // UserState - ); - - const actualUsername = await userRepo.verifyUserHandle(expectedUsername); + jest.spyOn(userService, 'checkUserHandle').mockRejectedValue(notFoundError); + + const actualUsername = await userRepository.verifyUserHandle(expectedUsername); expect(actualUsername).toBe(expectedUsername); }); it('rejects when username is taken', async () => { + const [userRepository, {userService}] = await buildUserRepository(); const username = 'john_doe'; + jest.spyOn(userService, 'checkUserHandle').mockResolvedValue(undefined); - const userRepo = new UserRepository( - { - checkUserHandle: jest.fn().mockImplementation(() => Promise.resolve()), - } as any, // UserService - {} as any, // AssetRepository, - {} as any, // SelfService, - {} as any, // ClientRepository, - {} as any, // ServerTimeHandler, - {} as any, // PropertiesRepository, - {} as any, // UserState - ); - - await expect(userRepo.verifyUserHandle(username)).rejects.toMatchObject({ + await expect(userRepository.verifyUserHandle(username)).rejects.toMatchObject({ message: 'User related backend request failure', name: 'UserError', type: 'REQUEST_FAILURE', @@ -368,7 +375,8 @@ describe('UserRepository', () => { describe('updateUsers', () => { it('should update local users', async () => { - const userService = userRepository['userService']; + const [userRepository, {userService, userState}] = await buildUserRepository(); + userState.self(new User()); const user = new User(entities.user.jane_roe.id); user.name('initial name'); user.isMe = true; diff --git a/src/script/user/UserRepository.ts b/src/script/user/UserRepository.ts index 1d33c941e00..e911eb60cf6 100644 --- a/src/script/user/UserRepository.ts +++ b/src/script/user/UserRepository.ts @@ -76,6 +76,7 @@ import type {EventSource} from '../event/EventSource'; import type {PropertiesRepository} from '../properties/PropertiesRepository'; import type {SelfService} from '../self/SelfService'; import {UserRecord} from '../storage'; +import {TeamState} from '../team/TeamState'; import type {ServerTimeHandler} from '../time/serverTimeHandler'; type GetUserOptions = { @@ -126,6 +127,7 @@ export class UserRepository { serverTimeHandler: ServerTimeHandler, private readonly propertyRepository: PropertiesRepository, private readonly userState = container.resolve(UserState), + private readonly teamState = container.resolve(TeamState), ) { this.logger = getLogger('UserRepository'); @@ -561,7 +563,7 @@ export class UserRepository { userId => new User(userId.id, userId.domain), ); const mappedUsers = this.userMapper.mapUsersFromJson(found, this.userState.self().domain).concat(failedToLoad); - if (this.userState.isTeam()) { + if (this.teamState.isTeam()) { this.mapGuestStatus(mappedUsers); } return mappedUsers; @@ -587,6 +589,10 @@ export class UserRepository { return fetchedUserEntities; } + findUsersByIds(userIds: QualifiedId[]): User[] { + return this.userState.users().filter(user => userIds.find(userId => matchQualifiedIds(user.qualifiedId, userId))); + } + /** * Find a local user. */ @@ -786,10 +792,10 @@ export class UserRepository { // update the user in db await this.updateUser(userId, user); - if (this.userState.isTeam()) { + if (this.teamState.isTeam()) { this.mapGuestStatus([updatedUser]); } - if (updatedUser && updatedUser.inTeam() && updatedUser.isDeleted) { + if (updatedUser && this.teamState.isInTeam(updatedUser) && updatedUser.isDeleted) { amplify.publish(WebAppEvents.TEAM.MEMBER_LEAVE, updatedUser.teamId, userId); } return updatedUser; @@ -905,8 +911,8 @@ export class UserRepository { const selfTeamId = this.userState.self().teamId; userEntities.forEach(userEntity => { if (!userEntity.isMe && selfTeamId) { - const isTeamMember = selfTeamId === userEntity.teamId; - const isGuest = !userEntity.isService && !isTeamMember && selfTeamId !== userEntity.teamId; + const isTeamMember = this.teamState.isInTeam(userEntity); + const isGuest = !userEntity.isService && !isTeamMember; userEntity.isGuest(isGuest); userEntity.isTeamMember(isTeamMember); } diff --git a/src/script/user/UserState.ts b/src/script/user/UserState.ts index bf4637664aa..84dbbdc260a 100644 --- a/src/script/user/UserState.ts +++ b/src/script/user/UserState.ts @@ -27,21 +27,14 @@ import {User} from '../entity/User'; @singleton() export class UserState { - public directlyConnectedUsers: ko.PureComputed; - public isTeam: ko.Observable | ko.PureComputed; + public readonly self = ko.observable(); + /** All the users we know of (connected users, conversation users, team members, users we have searched for...) */ + public readonly users = ko.observableArray([]); /** All the users that are directly connect to the self user (do not include users that are connected through conversations) */ public readonly connectedUsers: ko.PureComputed; - public readonly users: ko.ObservableArray; - public teamMembers: ko.PureComputed; - /** Note: this does not include the self user */ - public teamUsers: ko.PureComputed; public readonly connectRequests: ko.PureComputed; - public readonly numberOfContacts: ko.PureComputed; - public readonly self = ko.observable(); constructor() { - this.users = ko.observableArray([]); - this.connectRequests = ko .pureComputed(() => this.users().filter(userEntity => userEntity.isIncomingRequest())) .extend({rateLimit: 50}); @@ -53,16 +46,5 @@ export class UserState { .sort(sortUsersByPriority); }) .extend({rateLimit: TIME_IN_MILLIS.SECOND}); - - this.isTeam = ko.observable(); - this.teamMembers = ko.pureComputed((): User[] => []); - this.teamUsers = ko.pureComputed((): User[] => []); - - this.directlyConnectedUsers = ko.pureComputed((): User[] => []); - - this.numberOfContacts = ko.pureComputed(() => { - const contacts = this.isTeam() ? this.teamUsers() : this.connectedUsers(); - return contacts.filter(userEntity => !userEntity.isService).length; - }); } } diff --git a/src/script/util/DataDog.ts b/src/script/util/DataDog.ts index 4ae606c62f7..413ed86d3c3 100644 --- a/src/script/util/DataDog.ts +++ b/src/script/util/DataDog.ts @@ -67,6 +67,7 @@ export async function initializeDataDog(config: Configuration, user: {id?: strin beforeSend(event, context) { delete event.view.referrer; event.view.url = '/'; + return true; }, }); @@ -83,7 +84,7 @@ export async function initializeDataDog(config: Configuration, user: {id?: strin } log.view = {url: '/'}; log.message = replaceDomains(replaceAllStrings(removeTimestamp(removeColors(log.message)))); - return undefined; + return true; }, }); diff --git a/src/script/util/EmojiUtil.ts b/src/script/util/EmojiUtil.ts index a449abedbe3..214faf89938 100644 --- a/src/script/util/EmojiUtil.ts +++ b/src/script/util/EmojiUtil.ts @@ -61,11 +61,32 @@ Object.keys(emojiesList).forEach(key => { const emojiObject = emojiValue[0]; const emojiNames = emojiObject.n; - emojiDictionary.set(key, emojiNames[0].replaceAll('-', ' ')); + // Replace hyphens with spaces, but only if not followed by a number + // example- thumbs down emoji name is -1 + const formattedEmojiName = emojiNames[0].replace(/-(?![0-9])/g, ' '); + + emojiDictionary.set(key, formattedEmojiName); }); +// Function to get the emoji without skintone modifiers +const removeSkinToneModifiers = (emojiUnicode: string): string => { + const skinToneModifiers = new Set(['1f3fd', '1f3fe', '1f3ff', '1f3fc', '1f3fb']); + if (!emojiUnicode) { + return ''; + } + const emojiUnicodeSplitted = emojiUnicode.split('-'); + const unicodeWithoutSkinModifier = emojiUnicodeSplitted.filter(part => !skinToneModifiers.has(part)); + + return unicodeWithoutSkinModifier.join('-'); +}; export const getEmojiTitleFromEmojiUnicode = (emojiUnicode: string): string => { - return emojiDictionary.has(emojiUnicode) ? emojiDictionary.get(emojiUnicode)! : ''; + if (emojiDictionary.has(emojiUnicode)) { + return emojiDictionary.get(emojiUnicode)!; + } + + const unicodeWithoutSkinModifier = removeSkinToneModifiers(emojiUnicode); + + return emojiDictionary.get(unicodeWithoutSkinModifier) || ''; }; export function getEmojiUnicode(emojis: string) { diff --git a/src/script/util/ReactionUtil.test.ts b/src/script/util/ReactionUtil.test.ts new file mode 100644 index 00000000000..c95e5610b2c --- /dev/null +++ b/src/script/util/ReactionUtil.test.ts @@ -0,0 +1,114 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {generateQualifiedId} from 'test/helper/UserGenerator'; + +import {addReaction, userReactionMapToReactionMap} from './ReactionUtil'; +import {createUuid} from './uuid'; + +import {ReactionMap} from '../storage'; + +describe('ReactionUtil', () => { + describe('userReactionMapToReactionMap', () => { + it('converts a user reaction map to a reaction map', () => { + const userId = {id: createUuid(), domain: ''}; + const userReactions = {[userId.id]: '👍,👎'}; + const reactionMap = userReactionMapToReactionMap(userReactions); + + expect(reactionMap).toEqual([ + ['👍', [userId]], + ['👎', [userId]], + ]); + }); + + it('converts multiple users reactions to a reaction map', () => { + const userId = {id: createUuid(), domain: ''}; + const otherUserId = {id: createUuid(), domain: ''}; + const userReactions = {[userId.id]: '👍,👎', [otherUserId.id]: '👍,❤️'}; + const reactionMap = userReactionMapToReactionMap(userReactions); + + expect(reactionMap).toEqual([ + ['👍', [userId, otherUserId]], + ['👎', [userId]], + ['❤️', [otherUserId]], + ]); + }); + }); + + describe('addReaction', () => { + it('adds a single reaction', () => { + const userId = generateQualifiedId(); + const updatedReactions = addReaction([], '👍', userId); + + expect(updatedReactions).toEqual([['👍', [userId]]]); + }); + + it('adds a new reaction to a reaction list', () => { + const userId = generateQualifiedId(); + const reactions: ReactionMap = [['👎', [generateQualifiedId()]]]; + const updatedReactions = addReaction(reactions.slice(), '👍', userId); + + expect(updatedReactions).toEqual([...reactions, ['👍', [userId]]]); + }); + + it('adds a already existing reaction to a reaction list', () => { + const userId = generateQualifiedId(); + const firstReactorId = generateQualifiedId(); + const reactions: ReactionMap = [['👍', [firstReactorId]]]; + const updatedReactions = addReaction(reactions.slice(), '👍', userId); + + expect(updatedReactions).toEqual([['👍', [firstReactorId, userId]]]); + }); + + it('removes a user reaction', () => { + const userId = generateQualifiedId(); + const reactions: ReactionMap = [['👍', [userId]]]; + const updatedReactions = addReaction(reactions.slice(), '', userId); + + expect(updatedReactions).toEqual([]); + }); + + it('leaves other user reactions if a single reaction is removed', () => { + const userId = generateQualifiedId(); + const reactions: ReactionMap = [ + ['👍', [userId]], + ['👎', [userId]], + ]; + const updatedReactions = addReaction(reactions.slice(), '👍', userId); + + expect(updatedReactions).toEqual([['👍', [userId]]]); + }); + + it('keeps the order at which the reactions were received', () => { + const userId = generateQualifiedId(); + const reactorId = generateQualifiedId(); + const reactions: ReactionMap = [ + ['👍', [userId]], + ['👎', [userId]], + ]; + const updatedReactions = addReaction(reactions.slice(), '👍,❤️', reactorId); + + expect(updatedReactions).toEqual([ + ['👍', [userId, reactorId]], + ['👎', [userId]], + ['❤️', [reactorId]], + ]); + }); + }); +}); diff --git a/src/script/util/ReactionUtil.ts b/src/script/util/ReactionUtil.ts index a5fb20e910e..573af21a326 100644 --- a/src/script/util/ReactionUtil.ts +++ b/src/script/util/ReactionUtil.ts @@ -17,42 +17,60 @@ * */ -export interface Reactions { - [key: string]: string; -} +import type {QualifiedId} from '@wireapp/api-client/lib/user'; -type ReactionsGroupedByUser = Map; +import {matchQualifiedIds} from './QualifiedId'; -export function groupByReactionUsers(reactions: Reactions): ReactionsGroupedByUser { - const reactionsGroupedByUser = new Map(); +import {ReactionMap, UserReactionMap} from '../storage'; - for (const user in reactions) { - const userReactions = reactions[user] && reactions[user]?.split(','); +function isReactionMap(reactions: UserReactionMap | ReactionMap): reactions is ReactionMap { + return Array.isArray(reactions); +} - for (const reaction of userReactions) { - const users = reactionsGroupedByUser.get(reaction) || []; - users.push(user); - reactionsGroupedByUser.set(reaction, users); - } +/** + * Will convert the legacy user reaction map to the new reaction map format. + * The new map format will allow keeping track of the order the reactions arrived in. + */ +export function userReactionMapToReactionMap(userReactions: UserReactionMap | ReactionMap): ReactionMap { + if (isReactionMap(userReactions)) { + return userReactions; } + return Object.entries(userReactions).reduce((acc, [userId, reactions]) => { + reactions.split(',').forEach(reaction => { + const existingReaction = acc.find(([r]) => r === reaction); + const qualifiedId = {id: userId, domain: ''}; + if (existingReaction) { + existingReaction[1].push(qualifiedId); + } else { + acc.push([reaction, [qualifiedId]]); + } + }); + return acc; + }, []); +} + +export function addReaction(reactions: ReactionMap, reactionsStr: string, userId: QualifiedId) { + const userReactions = reactionsStr.split(','); - return reactionsGroupedByUser; + // First step is to remove all of this user's reactions + const filteredReactions = reactions.map(([reaction, users]) => { + return [reaction, users.filter(user => !matchQualifiedIds(user, userId))]; + }); + + userReactions + .filter(([reaction]) => !!reaction) + .forEach(reaction => { + const existingEntry = filteredReactions.find(([r]) => r === reaction); + if (existingEntry) { + existingEntry[1].push(userId); + } else { + filteredReactions.push([reaction, [userId]]); + } + }); + return filteredReactions.filter(([, users]) => users.length > 0); } // Maps to the static server emojis url export function getEmojiUrl(unicode: string) { return `/image/emojis/img-apple-64/${unicode}.png`; } - -/** - * - * @param reactionsList This is an array of tuples, each tuple consists of two elements a - * string representing an emoji and an array of strings representing users' reactions for that emoji. - * @returns tuples are sorted in descending order based on the length of the user - * reactions array for each emoji. - */ -export function sortReactionsByUserCount(reactionsList: [string, string[]][]) { - return reactionsList.sort( - ([, reactionAUserList], [, reactionBUserList]) => reactionBUserList.length - reactionAUserList.length, - ); -} diff --git a/src/script/util/ValidationUtil.ts b/src/script/util/ValidationUtil.ts index 55e94d6e37d..de50e2733b4 100644 --- a/src/script/util/ValidationUtil.ts +++ b/src/script/util/ValidationUtil.ts @@ -62,16 +62,6 @@ export const isBearerToken = (token: string): boolean => /^[a-zA-Z0-9\-._~+/]+[= export const isUUID = (string: string): boolean => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(string); -export const isBase64 = (string: string): boolean => { - try { - // Will raise a DOM exception if base64 string is invalid - window.atob(string); - } catch (error) { - return false; - } - return true; -}; - export const isValidApiPath = (path: string): boolean => { const [urlPath] = path.split('?'); if (!/^\/[a-zA-Z0-9\-_/,]+$/.test(urlPath)) { diff --git a/src/script/util/focusUtil.ts b/src/script/util/focusUtil.ts index 4080a434cdb..4748e794dc5 100644 --- a/src/script/util/focusUtil.ts +++ b/src/script/util/focusUtil.ts @@ -58,6 +58,6 @@ export const setElementsTabIndex = (elements: NodeListOf | [], isFo * @param element an element * @param isFocusable current message focus state */ -export const setElementTabIndex = (element: Element, isFocusable: boolean) => { +const setElementTabIndex = (element: Element, isFocusable: boolean) => { element.setAttribute('tabindex', isFocusable ? '0' : '-1'); }; diff --git a/src/script/view_model/ActionsViewModel.ts b/src/script/view_model/ActionsViewModel.ts index 99112babed1..1e099c83b42 100644 --- a/src/script/view_model/ActionsViewModel.ts +++ b/src/script/view_model/ActionsViewModel.ts @@ -29,6 +29,8 @@ import {PrimaryModal, removeCurrentModal, usePrimaryModalState} from 'Components import {t} from 'Util/LocalizerUtil'; import {isBackendError} from 'Util/TypePredicateUtil'; +import type {MainViewModel} from './MainViewModel'; + import type {ClientEntity} from '../client'; import type {ConnectionRepository} from '../connection/ConnectionRepository'; import type {ConversationRepository} from '../conversation/ConversationRepository'; @@ -50,6 +52,7 @@ export class ActionsViewModel { private readonly integrationRepository: IntegrationRepository, private readonly messageRepository: MessageRepository, private readonly userState = container.resolve(UserState), + private readonly mainViewModel: MainViewModel, ) {} readonly acceptConnectionRequest = (userEntity: User): Promise => { @@ -64,6 +67,14 @@ export class ActionsViewModel { return this.conversationRepository.archiveConversation(conversationEntity); }; + readonly unarchiveConversation = (conversationEntity: Conversation): void => { + if (!conversationEntity) { + return; + } + + return this.mainViewModel.list.clickToUnarchive(conversationEntity); + }; + /** * @param userEntity User to block * @param hideConversation Hide current conversation diff --git a/src/script/view_model/CallingViewModel.mocks.ts b/src/script/view_model/CallingViewModel.mocks.ts index 1028625cc89..ba7ece74784 100644 --- a/src/script/view_model/CallingViewModel.mocks.ts +++ b/src/script/view_model/CallingViewModel.mocks.ts @@ -39,7 +39,9 @@ export const mockCallingRepository = { onCallParticipantChangedCallback: jest.fn(), onCallClosed: jest.fn(), leaveCall: jest.fn(), + rejectCall: jest.fn(), setEpochInfo: jest.fn(), + supportsConferenceCalling: true, } as unknown as CallingRepository; export const callState = new CallState(); @@ -51,9 +53,8 @@ export function buildCall(conversationId: QualifiedId, convType = CONV_TYPE.ONEO } as any); } -const mockCore = container.resolve(Core); - export function buildCallingViewModel() { + const mockCore = container.resolve(Core); const callingViewModel = new CallingViewModel( mockCallingRepository, {} as any, @@ -70,82 +71,5 @@ export function buildCallingViewModel() { mockCore, ); - return callingViewModel; + return [callingViewModel, {core: mockCore}] as const; } - -export const prepareMLSConferenceMocks = (parentGroupId: string, subGroupId: string) => { - const mockGetClientIdsResponses = { - [parentGroupId]: [ - {userId: 'userId1', clientId: 'clientId1', domain: 'example.com'}, - {userId: 'userId1', clientId: 'clientId1A', domain: 'example.com'}, - {userId: 'userId2', clientId: 'clientId2', domain: 'example.com'}, - {userId: 'userId2', clientId: 'clientId2A', domain: 'example.com'}, - {userId: 'userId3', clientId: 'clientId3', domain: 'example.com'}, - ], - [subGroupId]: [ - {userId: 'userId1', clientId: 'clientId1', domain: 'example.com'}, - {userId: 'userId1', clientId: 'clientId1A', domain: 'example.com'}, - {userId: 'userId2', clientId: 'clientId2', domain: 'example.com'}, - ], - }; - - const expectedMemberListResult = [ - { - userid: 'userId1@example.com', - clientid: 'clientId1', - in_subconv: true, - }, - { - userid: 'userId1@example.com', - clientid: 'clientId1A', - in_subconv: true, - }, - { - userid: 'userId2@example.com', - clientid: 'clientId2', - in_subconv: true, - }, - { - userid: 'userId2@example.com', - clientid: 'clientId2A', - in_subconv: false, - }, - { - userid: 'userId3@example.com', - clientid: 'clientId3', - in_subconv: false, - }, - ]; - - const mockSecretKey = 'secretKey'; - const mockEpochNumber = 1; - - jest - .spyOn(mockCore.service!.mls!, 'joinConferenceSubconversation') - .mockResolvedValue({epoch: mockEpochNumber, groupId: subGroupId}); - - jest - .spyOn(mockCore.service!.mls!, 'getGroupIdFromConversationId') - .mockImplementation((_conversationId, subconversationId) => - subconversationId ? Promise.resolve(subGroupId) : Promise.resolve(parentGroupId), - ); - - jest - .spyOn(mockCore.service!.mls!, 'getClientIds') - .mockImplementation(groupId => - Promise.resolve(mockGetClientIdsResponses[groupId as keyof typeof mockGetClientIdsResponses]), - ); - - jest.spyOn(mockCore.service!.mls!, 'getEpoch').mockImplementation(() => Promise.resolve(mockEpochNumber)); - - jest.spyOn(mockCore.service!.mls!, 'exportSecretKey').mockResolvedValue(mockSecretKey); - - let callClosedCallback: (conversationId: QualifiedId, callType: CONV_TYPE) => void; - - jest.spyOn(mockCallingRepository, 'onCallClosed').mockImplementation(callback => (callClosedCallback = callback)); - jest - .spyOn(mockCallingRepository, 'leaveCall') - .mockImplementation(conversationId => callClosedCallback(conversationId, CONV_TYPE.CONFERENCE_MLS)); - - return {expectedMemberListResult, mockSecretKey, mockEpochNumber}; -}; diff --git a/src/script/view_model/CallingViewModel.test.ts b/src/script/view_model/CallingViewModel.test.ts index 3c3b858e43f..b48b4616806 100644 --- a/src/script/view_model/CallingViewModel.test.ts +++ b/src/script/view_model/CallingViewModel.test.ts @@ -17,25 +17,25 @@ * */ -import {waitFor} from '@testing-library/react'; import {ConversationProtocol} from '@wireapp/api-client/lib/conversation'; +import {QualifiedId} from '@wireapp/api-client/lib/user'; import {CALL_TYPE, CONV_TYPE, STATE} from '@wireapp/avs'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {createUuid} from 'Util/uuid'; -import { - buildCall, - buildCallingViewModel, - callState, - mockCallingRepository, - prepareMLSConferenceMocks, -} from './CallingViewModel.mocks'; +import {buildCall, buildCallingViewModel, callState, mockCallingRepository} from './CallingViewModel.mocks'; import {LEAVE_CALL_REASON} from '../calling/enum/LeaveCallReason'; import {Conversation} from '../entity/Conversation'; +const createMLSConversation = (conversationId: QualifiedId, groupId: string) => { + const mlsConversation = new Conversation(conversationId.id, conversationId.domain, ConversationProtocol.MLS); + mlsConversation.groupId = groupId; + return mlsConversation; +}; + describe('CallingViewModel', () => { afterEach(() => { callState.calls.removeAll(); @@ -44,7 +44,7 @@ describe('CallingViewModel', () => { describe('answerCall', () => { it('answers a call directly if no call is ongoing', async () => { - const callingViewModel = buildCallingViewModel(); + const [callingViewModel] = buildCallingViewModel(); const call = buildCall({id: 'conversation1', domain: ''}); await callingViewModel.callActions.answer(call); expect(mockCallingRepository.answerCall).toHaveBeenCalledWith(call); @@ -52,7 +52,7 @@ describe('CallingViewModel', () => { it('lets the user leave previous call before answering a new one', async () => { jest.useFakeTimers(); - const callingViewModel = buildCallingViewModel(); + const [callingViewModel] = buildCallingViewModel(); const joinedCall = buildCall({id: 'conversation1', domain: ''}); joinedCall.state(STATE.MEDIA_ESTAB); callState.calls.push(joinedCall); @@ -73,7 +73,7 @@ describe('CallingViewModel', () => { describe('startCall', () => { it('starts a call directly if no call is ongoing', async () => { - const callingViewModel = buildCallingViewModel(); + const [callingViewModel] = buildCallingViewModel(); const conversation = new Conversation(createUuid()); await callingViewModel.callActions.startAudio(conversation); expect(mockCallingRepository.startCall).toHaveBeenCalledWith(conversation, CALL_TYPE.NORMAL); @@ -81,7 +81,7 @@ describe('CallingViewModel', () => { it('lets the user leave previous call before starting a new one', async () => { jest.useFakeTimers(); - const callingViewModel = buildCallingViewModel(); + const [callingViewModel] = buildCallingViewModel(); const joinedCall = buildCall({id: 'conversation1', domain: ''}); joinedCall.state(STATE.MEDIA_ESTAB); callState.calls.push(joinedCall); @@ -105,17 +105,12 @@ describe('CallingViewModel', () => { jest.useRealTimers(); }); - it('updates epoch info after initiating a call', async () => { - const mockParentGroupId = 'mockParentGroupId1'; - const mockSubGroupId = 'mockSubGroupId1'; - const {expectedMemberListResult, mockEpochNumber, mockSecretKey} = prepareMLSConferenceMocks( - mockParentGroupId, - mockSubGroupId, - ); - - const callingViewModel = buildCallingViewModel(); + it('subscribes to epoch updates after initiating a call', async () => { + const [callingViewModel, {core}] = buildCallingViewModel(); const conversationId = {domain: 'example.com', id: 'conversation1'}; - const mlsConversation = new Conversation(conversationId.id, conversationId.domain, ConversationProtocol.MLS); + + const groupId = 'groupId'; + const mlsConversation = createMLSConversation(conversationId, groupId); const mockedCall = buildCall(conversationId, CONV_TYPE.CONFERENCE_MLS); jest.spyOn(mockCallingRepository, 'startCall').mockResolvedValueOnce(mockedCall); @@ -124,107 +119,34 @@ describe('CallingViewModel', () => { expect(mockCallingRepository.startCall).toHaveBeenCalledWith(mlsConversation, CALL_TYPE.NORMAL); - expect(mockCallingRepository.setEpochInfo).toHaveBeenCalledWith( + expect(core.service?.subconversation.subscribeToEpochUpdates).toHaveBeenCalledWith( conversationId, - { - epoch: mockEpochNumber, - secretKey: mockSecretKey, - }, - expectedMemberListResult, + groupId, + expect.any(Function), + expect.any(Function), ); }); - it('updates epoch info after answering a call', async () => { - const mockParentGroupId = 'mockParentGroupId2'; - const mockSubGroupId = 'mockSubGroupId2'; - const {expectedMemberListResult, mockEpochNumber, mockSecretKey} = prepareMLSConferenceMocks( - mockParentGroupId, - mockSubGroupId, - ); - - const callingViewModel = buildCallingViewModel(); + it('subscribes to epoch updates after answering a call', async () => { + const [callingViewModel, {core}] = buildCallingViewModel(); const conversationId = {domain: 'example.com', id: 'conversation2'}; - const call = buildCall(conversationId, CONV_TYPE.CONFERENCE_MLS); - - await callingViewModel.callActions.answer(call); - - expect(mockCallingRepository.answerCall).toHaveBeenCalledWith(call); - - expect(mockCallingRepository.setEpochInfo).toHaveBeenCalledWith( - conversationId, - { - epoch: mockEpochNumber, - secretKey: mockSecretKey, - }, - expectedMemberListResult, - ); - }); + const groupId = 'groupId'; + const mlsConversation = createMLSConversation(conversationId, groupId); - it('updates epoch info after mls service has emmited "newEpoch" event', async () => { - const mockParentGroupId = 'mockParentGroupId3'; - const mockSubGroupId = 'mockSubGroupId3'; - const {expectedMemberListResult, mockEpochNumber, mockSecretKey} = prepareMLSConferenceMocks( - mockParentGroupId, - mockSubGroupId, - ); + callingViewModel['conversationState'].conversations.push(mlsConversation); - const callingViewModel = buildCallingViewModel(); - const conversationId = {domain: 'example.com', id: 'conversation3'}; const call = buildCall(conversationId, CONV_TYPE.CONFERENCE_MLS); await callingViewModel.callActions.answer(call); - expect(mockCallingRepository.answerCall).toHaveBeenCalledWith(call); - - //at this point we start to listen to the mls service events - expect(mockCallingRepository.setEpochInfo).toHaveBeenCalledWith( - conversationId, - { - epoch: mockEpochNumber, - secretKey: mockSecretKey, - }, - expectedMemberListResult, - ); - - const newEpochNumber = 2; - callingViewModel.mlsService.emit('newEpoch', { - epoch: newEpochNumber, - groupId: mockSubGroupId, - }); - - await waitFor(() => { - expect(mockCallingRepository.setEpochInfo).toHaveBeenCalledTimes(2); - expect(mockCallingRepository.setEpochInfo).toHaveBeenCalledWith( - conversationId, - { - epoch: newEpochNumber, - secretKey: mockSecretKey, - }, - expectedMemberListResult, - ); - }); - // once we leave the call, we stop listening to the mls service events - await waitFor(() => { - callingViewModel.callingRepository.leaveCall(conversationId, LEAVE_CALL_REASON.MANUAL_LEAVE_BY_UI_CLICK); - }); - - const anotherEpochNumber = 3; - callingViewModel.mlsService.emit('newEpoch', { - epoch: anotherEpochNumber, - groupId: mockSubGroupId, - }); + expect(mockCallingRepository.answerCall).toHaveBeenCalledWith(call); - // Wait for all the callback queue tasks to be executed so we know that the function was not called. - // Without this, test will always succeed (even without unsubscribing to epoch changes) because the function was not called YET. - await new Promise(r => setTimeout(r, 0)); - expect(mockCallingRepository.setEpochInfo).not.toHaveBeenCalledWith( + expect(core.service?.subconversation.subscribeToEpochUpdates).toHaveBeenCalledWith( conversationId, - { - epoch: anotherEpochNumber, - secretKey: mockSecretKey, - }, - expectedMemberListResult, + groupId, + expect.any(Function), + expect.any(Function), ); }); }); diff --git a/src/script/view_model/CallingViewModel.ts b/src/script/view_model/CallingViewModel.ts index a76bf1293af..978b80aeb21 100644 --- a/src/script/view_model/CallingViewModel.ts +++ b/src/script/view_model/CallingViewModel.ts @@ -17,7 +17,6 @@ * */ -import {SUBCONVERSATION_ID} from '@wireapp/api-client/lib/conversation/Subconversation'; import {QualifiedId} from '@wireapp/api-client/lib/user'; import {constructFullyQualifiedClientId} from '@wireapp/core/lib/util/fullyQualifiedClientIdUtils'; import {TaskScheduler} from '@wireapp/core/lib/util/TaskScheduler'; @@ -41,9 +40,9 @@ import {CallingRepository, QualifiedWcallMember} from '../calling/CallingReposit import {callingSubscriptions} from '../calling/callingSubscriptionsHandler'; import {CallState} from '../calling/CallState'; import {LEAVE_CALL_REASON} from '../calling/enum/LeaveCallReason'; -import {getSubconversationEpochInfo, subscribeToEpochUpdates} from '../calling/mlsConference'; import {PrimaryModal} from '../components/Modals/PrimaryModal'; import {Config} from '../Config'; +import {isMLSConversation} from '../conversation/ConversationSelectors'; import {ConversationState} from '../conversation/ConversationState'; import type {Conversation} from '../entity/Conversation'; import type {User} from '../entity/User'; @@ -102,7 +101,7 @@ export class CallingViewModel { readonly permissionRepository: PermissionRepository, readonly teamRepository: TeamRepository, readonly propertiesRepository: PropertiesRepository, - private readonly selfUser: ko.Subscribable, + private readonly selfUser: ko.Observable, readonly multitasking: Multitasking, private readonly conversationState = container.resolve(ConversationState), readonly callState = container.resolve(CallState), @@ -160,13 +159,12 @@ export class CallingViewModel { return; } - if (conversation.isUsingMLSProtocol) { - const unsubscribe = await subscribeToEpochUpdates( - {mlsService: this.mlsService, conversationState: this.conversationState}, + if (isMLSConversation(conversation)) { + const unsubscribe = await this.subconversationService.subscribeToEpochUpdates( conversation.qualifiedId, - ({epoch, keyLength, secretKey, members}) => { - this.callingRepository.setEpochInfo(conversation.qualifiedId, {epoch, secretKey}, members); - }, + conversation.groupId, + (groupId: string) => this.conversationState.findConversationByGroupId(groupId)?.qualifiedId, + data => this.callingRepository.setEpochInfo(conversation.qualifiedId, data), ); callingSubscriptions.addCall(call.conversationId, unsubscribe); @@ -175,12 +173,17 @@ export class CallingViewModel { }; const joinOngoingMlsConference = async (call: Call) => { - const unsubscribe = await subscribeToEpochUpdates( - {mlsService: this.mlsService, conversationState: this.conversationState}, + const conversation = this.getConversationById(call.conversationId); + + if (!conversation || !isMLSConversation(conversation)) { + return; + } + + const unsubscribe = await this.subconversationService.subscribeToEpochUpdates( call.conversationId, - ({epoch, keyLength, secretKey, members}) => { - this.callingRepository.setEpochInfo(call.conversationId, {epoch, secretKey}, members); - }, + conversation.groupId, + (groupId: string) => this.conversationState.findConversationByGroupId(groupId)?.qualifiedId, + data => this.callingRepository.setEpochInfo(call.conversationId, data), ); callingSubscriptions.addCall(call.conversationId, unsubscribe); @@ -213,31 +216,21 @@ export class CallingViewModel { const updateEpochInfo = async (conversationId: QualifiedId, shouldAdvanceEpoch = false) => { const conversation = this.getConversationById(conversationId); - if (!conversation?.isUsingMLSProtocol) { + if (!conversation || !isMLSConversation(conversation)) { return; } - const subconversationGroupId = await this.mlsService.getGroupIdFromConversationId( + const subconversationEpochInfo = await this.subconversationService.getSubconversationEpochInfo( conversationId, - SUBCONVERSATION_ID.CONFERENCE, + conversation.groupId, + shouldAdvanceEpoch, ); - if (!subconversationGroupId) { - return; - } - - //we don't want to react to avs callbacks when conversation was not yet established - const doesMLSGroupExist = await this.mlsService.conversationExists(subconversationGroupId); - if (!doesMLSGroupExist) { + if (!subconversationEpochInfo) { return; } - const {epoch, secretKey, members} = await getSubconversationEpochInfo( - {mlsService: this.mlsService}, - conversationId, - shouldAdvanceEpoch, - ); - this.callingRepository.setEpochInfo(conversationId, {epoch, secretKey}, members); + this.callingRepository.setEpochInfo(conversationId, subconversationEpochInfo); }; const closeCall = async (conversationId: QualifiedId, conversationType: CONV_TYPE) => { @@ -246,7 +239,7 @@ export class CallingViewModel { return; } - await this.mlsService.leaveConferenceSubconversation(conversationId); + await this.subconversationService.leaveConferenceSubconversation(conversationId); callingSubscriptions.removeCall(conversationId); }; @@ -257,47 +250,9 @@ export class CallingViewModel { } }); - const removeStaleClient = async ( - conversationId: QualifiedId, - memberToRemove: QualifiedWcallMember, - ): Promise => { - const subconversationGroupId = await this.mlsService.getGroupIdFromConversationId( - conversationId, - SUBCONVERSATION_ID.CONFERENCE, - ); - - if (!subconversationGroupId) { - return; - } - - const doesMLSGroupExist = await this.mlsService.conversationExists(subconversationGroupId); - if (!doesMLSGroupExist) { - return; - } - - const { - userId: {id: userId, domain}, - clientid, - } = memberToRemove; - const clientToRemoveQualifiedId = constructFullyQualifiedClientId(userId, clientid, domain); - - const subconversationMembers = await this.mlsService.getClientIds(subconversationGroupId); - - const isSubconversationMember = subconversationMembers.some( - ({userId, clientId, domain}) => - constructFullyQualifiedClientId(userId, clientId, domain) === clientToRemoveQualifiedId, - ); - - if (!isSubconversationMember) { - return; - } - - return void this.mlsService.removeClientsFromConversation(subconversationGroupId, [clientToRemoveQualifiedId]); - }; - const handleCallParticipantChange = (conversationId: QualifiedId, members: QualifiedWcallMember[]) => { const conversation = this.getConversationById(conversationId); - if (!conversation?.isUsingMLSProtocol) { + if (conversation && isMLSConversation(conversation)) { return; } @@ -326,7 +281,11 @@ export class CallingViewModel { firingDate, key, // if timer expires = client is stale -> remove client from the subconversation - task: () => removeStaleClient(conversationId, member), + task: () => + this.subconversationService.removeClientFromConferenceSubconversation(conversationId, { + user: {id: member.userId.id, domain: member.userId.domain}, + clientId: member.clientid, + }), }); } }; @@ -366,7 +325,7 @@ export class CallingViewModel { this.callActions = { answer: async (call: Call) => { - if (call.conversationType === CONV_TYPE.CONFERENCE && !this.callingRepository.supportsConferenceCalling) { + if (call.isConference && !this.callingRepository.supportsConferenceCalling) { PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, { primaryAction: { action: () => { @@ -455,13 +414,13 @@ export class CallingViewModel { }; } - get mlsService() { - const mlsService = this.core.service?.mls; - if (!mlsService) { - throw new Error('mls service was not initialised'); + get subconversationService() { + const subconversationService = this.core.service?.subconversation; + if (!subconversationService) { + throw new Error('SubconversationService was not initialised'); } - return mlsService; + return subconversationService; } /** @@ -519,7 +478,7 @@ export class CallingViewModel { } private showRestrictedConferenceCallingModal() { - if (this.selfUser().inTeam()) { + if (this.teamState.isInTeam(this.selfUser())) { if (this.selfUser().teamRole() === ROLE.OWNER) { const replaceEnterprise = replaceLink( Config.getConfig().URL.PRICING, diff --git a/src/script/view_model/ListViewModel.ts b/src/script/view_model/ListViewModel.ts index 0697aea9dfe..6db10f91f15 100644 --- a/src/script/view_model/ListViewModel.ts +++ b/src/script/view_model/ListViewModel.ts @@ -21,7 +21,6 @@ import {amplify} from 'amplify'; import ko from 'knockout'; import {container} from 'tsyringe'; -import {CONV_TYPE} from '@wireapp/avs'; import {Runtime} from '@wireapp/commons'; import {WebAppEvents} from '@wireapp/webapp-events'; @@ -132,6 +131,9 @@ export class ListViewModel { } private readonly _initSubscriptions = () => { + amplify.subscribe(WebAppEvents.CONVERSATION.SHOW, (conversation?: Conversation) => { + this.openConversations(conversation?.archivedState()); + }); amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_ACCOUNT, this.openPreferencesAccount); amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_DEVICES, this.openPreferencesDevices); amplify.subscribe(WebAppEvents.PREFERENCES.SHOW_AV, this.openPreferencesAudioVideo); @@ -151,7 +153,7 @@ export class ListViewModel { return; } - if (call.conversationType === CONV_TYPE.CONFERENCE && !this.callingRepository.supportsConferenceCalling) { + if (call.isConference && !this.callingRepository.supportsConferenceCalling) { PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, { text: { message: `${t('modalConferenceCallNotSupportedMessage')} ${t('modalConferenceCallNotSupportedJoinMessage')}`, diff --git a/src/script/view_model/MainViewModel.ts b/src/script/view_model/MainViewModel.ts index db2eee30724..0c0293534a2 100644 --- a/src/script/view_model/MainViewModel.ts +++ b/src/script/view_model/MainViewModel.ts @@ -113,6 +113,8 @@ export class MainViewModel { repositories.conversation, repositories.integration, repositories.message, + userState, + this, ); this.calling = new CallingViewModel( diff --git a/test/helper/EventGenerator.ts b/test/helper/EventGenerator.ts index b27bfab3f30..496c0bd46a8 100644 --- a/test/helper/EventGenerator.ts +++ b/test/helper/EventGenerator.ts @@ -18,7 +18,13 @@ */ import {AssetTransferState} from 'src/script/assets/AssetTransferState'; -import {AssetAddEvent, DeleteEvent, EventBuilder, MessageAddEvent} from 'src/script/conversation/EventBuilder'; +import { + AssetAddEvent, + DeleteEvent, + EventBuilder, + MessageAddEvent, + ReactionEvent, +} from 'src/script/conversation/EventBuilder'; import {Conversation} from 'src/script/entity/Conversation'; import {CONVERSATION} from 'src/script/event/Client'; import {createUuid} from 'Util/uuid'; @@ -48,6 +54,20 @@ export function createMessageAddEvent({ }; } +export function createReactionEvent(targetMessageId: string, reaction: string = '👍'): ReactionEvent { + return { + conversation: createUuid(), + data: { + message_id: targetMessageId, + reaction, + }, + from: createUuid(), + id: createUuid(), + time: new Date().toISOString(), + type: CONVERSATION.REACTION, + }; +} + export function createDeleteEvent(deleteMessageId: string, conversationId: string = createUuid()): DeleteEvent { return { conversation: conversationId, diff --git a/test/helper/TestFactory.js b/test/helper/TestFactory.js index 41407011a88..65276f56889 100644 --- a/test/helper/TestFactory.js +++ b/test/helper/TestFactory.js @@ -52,7 +52,6 @@ import {PermissionRepository} from 'src/script/permission/PermissionRepository'; import {PropertiesRepository} from 'src/script/properties/PropertiesRepository'; import {PropertiesService} from 'src/script/properties/PropertiesService'; import {SearchRepository} from 'src/script/search/SearchRepository'; -import {SearchService} from 'src/script/search/SearchService'; import {SelfService} from 'src/script/self/SelfService'; import {Core} from 'src/script/service/CoreSingleton'; import {createStorageEngine, DatabaseTypes} from 'src/script/service/StoreEngineProvider'; @@ -196,8 +195,7 @@ export class TestFactory { */ async exposeSearchActors() { await this.exposeUserActors(); - this.search_service = new SearchService(); - this.search_repository = new SearchRepository(this.search_service, this.user_repository); + this.search_repository = new SearchRepository(this.user_repository); return this.search_repository; } @@ -272,7 +270,6 @@ export class TestFactory { this.user_repository, this.assetRepository, this.user_repository['userState'], - this.team_repository['teamState'], clientState, ); const core = container.resolve(Core); diff --git a/test/helper/UserGenerator.ts b/test/helper/UserGenerator.ts index 32a44806e95..996617d4817 100644 --- a/test/helper/UserGenerator.ts +++ b/test/helper/UserGenerator.ts @@ -27,6 +27,13 @@ import type {User} from '../../src/script/entity/User'; import {serverTimeHandler} from '../../src/script/time/serverTimeHandler'; import {UserMapper} from '../../src/script/user/UserMapper'; +export function generateQualifiedId(): QualifiedId { + return { + id: createUuid(), + domain: 'test.wire.link', + }; +} + export function generateAPIUser( id: QualifiedId = {id: createUuid(), domain: 'test.wire.link'}, overwites?: Partial, @@ -48,7 +55,7 @@ export function generateAPIUser( handle: faker.internet.userName(), id: id.id, // replace special chars to avoid escaping problems with querying the DOM - name: faker.person.fullName().replace(/[^a-zA-Z ]/, ''), + name: faker.person.fullName().replace(/[^a-zA-Z ]/g, ''), qualified_id: id, ...overwites, }; diff --git a/test/unit_tests/event/EventServiceCommon.js b/test/unit_tests/event/EventServiceCommon.js index 0c60d834c29..2ba48ff55d9 100644 --- a/test/unit_tests/event/EventServiceCommon.js +++ b/test/unit_tests/event/EventServiceCommon.js @@ -372,7 +372,7 @@ const testEventServiceClass = (testedServiceName, className) => { it('fails if changes do not contain version property', () => { const updates = {reactions: ['user-id']}; return testFactory[testedServiceName] - .updateEventSequentially(12, updates) + .updateEventSequentially({primary_key: 12, ...updates}) .then(fail) .catch(error => { expect(error.type).toBe(ConversationError.TYPE.WRONG_CHANGE); @@ -385,7 +385,7 @@ const testEventServiceClass = (testedServiceName, className) => { spyOn(testFactory.storage_service, 'load').and.returnValue(Promise.resolve({version: 2})); return testFactory[testedServiceName] - .updateEventSequentially(12, updates) + .updateEventSequentially({primary_key: 12, ...updates}) .then(fail) .catch(error => { expect(error.type).toBe(StorageError.TYPE.NON_SEQUENTIAL_UPDATE); @@ -399,7 +399,7 @@ const testEventServiceClass = (testedServiceName, className) => { spyOn(testFactory.storage_service, 'update').and.returnValue(Promise.resolve('ok')); return testFactory[testedServiceName] - .updateEventSequentially(12, updates) + .updateEventSequentially({primary_key: 12, ...updates}) .then(fail) .catch(error => { expect(error.type).toBe(StorageError.TYPE.NOT_FOUND); @@ -413,7 +413,7 @@ const testEventServiceClass = (testedServiceName, className) => { spyOn(testFactory.storage_service, 'update').and.returnValue(Promise.resolve('ok')); spyOn(testFactory.storage_service.db, 'transaction').and.callThrough(); - return testFactory[testedServiceName].updateEventSequentially(12, updates).then(() => { + return testFactory[testedServiceName].updateEventSequentially({primary_key: 12, ...updates}).then(() => { expect(testFactory.storage_service.update).toHaveBeenCalledWith(eventStoreName, 12, updates); expect(testFactory.storage_service.db.transaction).toHaveBeenCalled(); }); @@ -548,25 +548,6 @@ const testEventServiceClass = (testedServiceName, className) => { afterEach(() => { testFactory.storage_service.clearStores(); }); - - it('deletes message with the given key', () => { - return testFactory[testedServiceName] - .deleteEventByKey(primary_keys[1]) - .then(() => testFactory[testedServiceName].loadPrecedingEvents(conversationId)) - .then(events => { - expect(events.length).toBe(2); - events.forEach(event => expect(event.primary_key).not.toBe(primary_keys[1])); - }); - }); - - it('does not delete the event if key is wrong', () => { - return testFactory[testedServiceName] - .deleteEventByKey('wrongKey') - .then(() => testFactory[testedServiceName].loadPrecedingEvents(conversationId)) - .then(events => { - expect(events.length).toBe(3); - }); - }); }); describe('updateEvent', () => { diff --git a/test/unit_tests/search/SearchRepositorySpec.js b/test/unit_tests/search/SearchRepositorySpec.js deleted file mode 100644 index 9a6afd0f375..00000000000 --- a/test/unit_tests/search/SearchRepositorySpec.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import {User} from 'src/script/entity/User'; - -import {TestFactory} from '../../helper/TestFactory'; - -describe('SearchRepository', () => { - const testFactory = new TestFactory(); - - beforeAll(() => { - return testFactory.exposeSearchActors(); - }); - - describe('searchUserInSet', () => { - const sabine = generateUser('jesuissabine', 'Sabine Duchemin'); - const janina = generateUser('yosoyjanina', 'Janina Felix'); - const felixa = generateUser('iamfelix', 'Felix Abo'); - const felix = generateUser('iamfelix', 'Felix Oulala'); - const felicien = generateUser('ichbinfelicien', 'Felicien Delatour'); - const lastguy = generateUser('lastfelicien', 'lastsabine lastjanina'); - const jeanpierre = generateUser('jean-pierre', 'Jean-Pierre Sansbijou'); - const pierre = generateUser('pierrot', 'Pierre Monsouci'); - const noMatch1 = generateUser(undefined, 'yyy yyy'); - const noMatch2 = generateUser('xxx', undefined); - const users = [lastguy, noMatch1, felix, felicien, sabine, janina, noMatch2, felixa, jeanpierre, pierre]; - - const tests = [ - {expected: users, term: '', testCase: 'returns the whole user list if no term is given'}, - {expected: [jeanpierre, janina, sabine, lastguy], term: 'j', testCase: 'matches multiple results'}, - { - expected: [janina, lastguy], - term: 'ja', - testCase: 'puts matches that start with the pattern on top of the list', - }, - { - expected: [felicien, felixa, felix, janina, lastguy], - term: 'fel', - testCase: 'sorts by name, handle, inside match and alphabetically', - }, - { - expected: [felixa, felix, janina], - term: 'felix', - testCase: 'sorts by firstname and lastname', - }, - { - expected: [felicien, lastguy], - term: 'felici', - testCase: 'sorts by name and inside match', - }, - { - expected: [sabine, jeanpierre, lastguy, pierre, janina], - term: 's', - testCase: 'sorts by name, handle and inside match', - }, - { - expected: [sabine, jeanpierre, lastguy], - term: 'sa', - testCase: 'puts matches that start with the pattern on top of the list', - }, - { - expected: [sabine, lastguy], - term: 'sabine', - testCase: 'puts matches that start with the pattern on top of the list', - }, - { - expected: [sabine, lastguy], - term: 'sabine', - testCase: 'puts matches that start with the pattern on top of the list', - }, - {expected: [felicien, lastguy], term: 'ic', testCase: 'matches inside the properties'}, - { - expected: [jeanpierre], - term: 'jean-pierre', - testCase: 'finds compound names', - }, - { - expected: [pierre, jeanpierre], - term: 'pierre', - testCase: 'matches compound names and prioritize matches from start', - }, - ]; - - tests.forEach(({expected, term, testCase}) => { - it(`${testCase} term: ${term}`, () => { - const suggestions = testFactory.search_repository.searchUserInSet(term, users); - - expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); - }); - }); - - it('does not replace numbers with emojis', () => { - const felix10 = generateUser('simple10', 'Felix10'); - const unsortedUsers = [felix10]; - const suggestions = testFactory.search_repository.searchUserInSet('😋', unsortedUsers); - - expect(suggestions.map(serializeUser)).toEqual([]); - }); - - it('prioritize exact matches with special characters', () => { - const smilyFelix = generateUser('smily', '😋Felix'); - const atFelix = generateUser('at', '@Felix'); - const simplyFelix = generateUser('simple', 'Felix'); - - const unsortedUsers = [atFelix, smilyFelix, simplyFelix]; - - let suggestions = testFactory.search_repository.searchUserInSet('felix', unsortedUsers); - let expected = [simplyFelix, smilyFelix, atFelix]; - - expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); - suggestions = testFactory.search_repository.searchUserInSet('😋', unsortedUsers); - expected = [smilyFelix]; - - expect(suggestions.map(serializeUser)).toEqual(expected.map(serializeUser)); - }); - - it('handles sorting matching results', () => { - const first = generateUser('xxx', '_surname'); - const second = generateUser('xxx', 'surname _lastname'); - const third = generateUser('_xxx', 'surname lastname'); - const fourth = generateUser('xxx', 'sur_name lastname'); - const fifth = generateUser('xxx', 'surname last_name'); - const sixth = generateUser('x_xx', 'surname lastname'); - - const unsortedUsers = [sixth, fifth, third, second, first, fourth]; - const expectedUsers = [first, second, third, fourth, fifth, sixth]; - - const suggestions = testFactory.search_repository.searchUserInSet('_', unsortedUsers); - - expect(suggestions.map(serializeUser)).toEqual(expectedUsers.map(serializeUser)); - }); - }); -}); - -function generateUser(handle, name) { - const user = new User(); - user.username(handle); - user.name(name); - return user; -} -function serializeUser(userEntity) { - return {name: userEntity.name(), username: userEntity.username()}; -} diff --git a/test/unit_tests/util/ValidationUtilSpec.js b/test/unit_tests/util/ValidationUtilSpec.js index e99100b0245..951913853de 100644 --- a/test/unit_tests/util/ValidationUtilSpec.js +++ b/test/unit_tests/util/ValidationUtilSpec.js @@ -21,7 +21,6 @@ import {createUuid} from 'Util/uuid'; import { isBearerToken, isUUID, - isBase64, isValidApiPath, isTweetUrl, legacyAsset, @@ -101,22 +100,6 @@ describe('ValidationUtil', () => { }); }); - describe('"isBase64"', () => { - it('detects a correct Base64-encoded string', () => { - const encoded = 'SGVsbG8gV29ybGQh'; - const actual = isBase64(encoded); - - expect(actual).toBe(true); - }); - - it('detects an incorrect Base64-encoded string', () => { - const encoded = 'SGVsbG8gV29ybGQh=='; - const actual = isBase64(encoded); - - expect(actual).toBe(false); - }); - }); - describe('"isBearerToken"', () => { it('detects a correct Bearer Token', () => { const token = 'iJCRCjc8oROO-dkrkqCXOade997oa8Jhbz6awMUQPBQo80VenWqp_oNvfY6AnU5BxEsdDPOBfBP-uz_b0gAKBQ=='; diff --git a/webpack.config.common.js b/webpack.config.common.js index bdf15610475..d557c5e2a6e 100644 --- a/webpack.config.common.js +++ b/webpack.config.common.js @@ -146,7 +146,7 @@ module.exports = { new CopyPlugin({ patterns: [ { - context: 'node_modules/@wireapp/core-crypto/platforms/web/assets', + context: 'node_modules/@wireapp/core-crypto/platforms/web', from: '*.wasm', to: `${dist}/min/core-crypto.wasm`, }, diff --git a/yarn.lock b/yarn.lock index 77abeed1cbd..0edc833b844 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2024,15 +2024,15 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-cascade-layers@npm:^4.0.0": - version: 4.0.0 - resolution: "@csstools/postcss-cascade-layers@npm:4.0.0" +"@csstools/postcss-cascade-layers@npm:^4.0.1": + version: 4.0.1 + resolution: "@csstools/postcss-cascade-layers@npm:4.0.1" dependencies: "@csstools/selector-specificity": ^3.0.0 postcss-selector-parser: ^6.0.13 peerDependencies: postcss: ^8.4 - checksum: 3bc9369e83a7ac1c017fdaac249de4d2fb9a7c016175352302fe82a2bfd5a0b1cfd352801573bff714b96398495c41593d59a5d77962811c4039ce9f97f300de + checksum: 71f30dec7a123cadfc749246acfa60ebbb9dff0064834fb51afd08bf3928c5488bae564f826581889de8ed9235a20c1cecde0ee7d148eefc6ce4f5f1ab9a570f languageName: node linkType: hard @@ -2170,6 +2170,24 @@ __metadata: languageName: node linkType: hard +"@csstools/postcss-logical-overflow@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-logical-overflow@npm:1.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 6111f6741753b7d20a00d3f224976c353e85c25ec333cad6ce12cb403186cd8f37caa9e11565233e86487e45ce96173255b5add66e35663bdeeff3d9a4a77eaf + languageName: node + linkType: hard + +"@csstools/postcss-logical-overscroll-behavior@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-logical-overscroll-behavior@npm:1.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 32f9e96af8e5a0d40610203e4077accadd3b5baa01e874103f4748bc84ca8e241be23460327e29637e424468616fdda62bee0d0b7608fcb8f79a0bf53522f938 + languageName: node + linkType: hard + "@csstools/postcss-logical-resize@npm:^2.0.0": version: 2.0.0 resolution: "@csstools/postcss-logical-resize@npm:2.0.0" @@ -2347,48 +2365,48 @@ __metadata: languageName: node linkType: hard -"@datadog/browser-core@npm:4.50.1": - version: 4.50.1 - resolution: "@datadog/browser-core@npm:4.50.1" - checksum: d3581d68b6164e9cd07f7514d79708782209dad9a5746eccdfce3d8f9249c4f496d0ad342f7b96168a89dfdf83551ff80acd6132719832a1decdaf64b4fa0005 +"@datadog/browser-core@npm:5.1.0": + version: 5.1.0 + resolution: "@datadog/browser-core@npm:5.1.0" + checksum: 7c8768dd3c7d620563103c6fa692f3d010ba5674b6826d937f439a694f4ae36b28adb2f6842b6005f8d34f1e9774afad4077300f2dd2c18843c222321b1b0f93 languageName: node linkType: hard -"@datadog/browser-logs@npm:^4.50.1": - version: 4.50.1 - resolution: "@datadog/browser-logs@npm:4.50.1" +"@datadog/browser-logs@npm:^5.1.0": + version: 5.1.0 + resolution: "@datadog/browser-logs@npm:5.1.0" dependencies: - "@datadog/browser-core": 4.50.1 + "@datadog/browser-core": 5.1.0 peerDependencies: - "@datadog/browser-rum": 4.50.1 + "@datadog/browser-rum": 5.1.0 peerDependenciesMeta: "@datadog/browser-rum": optional: true - checksum: f43f67f328a9063b63836f38a2f5e30bc11d22316c363c23ad794693f99ecee8d9a22f91c863fabe8f28150a7398f1429d1f39586d2042e1a2b8297503bcf114 + checksum: 0dd22e68fbd2810cc48e206641681e84bba18229d8909cfc77bbde2563f00d1ad6388d6c252c7d4c954e8cdde030a16138eaab0ede890df007bbea1e4f3c238a languageName: node linkType: hard -"@datadog/browser-rum-core@npm:4.50.1": - version: 4.50.1 - resolution: "@datadog/browser-rum-core@npm:4.50.1" +"@datadog/browser-rum-core@npm:5.1.0": + version: 5.1.0 + resolution: "@datadog/browser-rum-core@npm:5.1.0" dependencies: - "@datadog/browser-core": 4.50.1 - checksum: 3f73524686c030cfcd5ff40bb5e5b4cbcd158239755f6e56713a3e89a3a1e4f7d9315f796f30ea98468c6509231720b0278385a06dc601b9d3bb2e2c9a14b896 + "@datadog/browser-core": 5.1.0 + checksum: 2a33c3523ed2463f78ddc24af9a6e28479e5a771d97c7d085435a3168da4a0b449ad12c0856d975dd2594a3d3a088167ad2bb8c30c1c69435d54635cbe27d4d2 languageName: node linkType: hard -"@datadog/browser-rum@npm:^4.50.1": - version: 4.50.1 - resolution: "@datadog/browser-rum@npm:4.50.1" +"@datadog/browser-rum@npm:^5.1.0": + version: 5.1.0 + resolution: "@datadog/browser-rum@npm:5.1.0" dependencies: - "@datadog/browser-core": 4.50.1 - "@datadog/browser-rum-core": 4.50.1 + "@datadog/browser-core": 5.1.0 + "@datadog/browser-rum-core": 5.1.0 peerDependencies: - "@datadog/browser-logs": 4.50.1 + "@datadog/browser-logs": 5.1.0 peerDependenciesMeta: "@datadog/browser-logs": optional: true - checksum: 202867d39d1e8bfaa64a9a9679d1806052af181951f9091520356e5a22d7c237708d0781adf439e721eff56c3b208480a252cb0e6fbd7bfa820bfa5812a9f880 + checksum: 62e1087bd18fccbd6ba1177b820020c3140d391a09f31341bd19ea4eb4488c34d7fb76e9ed8b1e118f0ba70d8e2fa2e82ad28ca6dd9ab3c8233342c96c558378 languageName: node linkType: hard @@ -2571,10 +2589,20 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.51.0": - version: 8.51.0 - resolution: "@eslint/js@npm:8.51.0" - checksum: 0228bf1e1e0414843e56d9ff362a2a72d579c078f93174666f29315690e9e30a8633ad72c923297f7fd7182381b5a476805ff04dac8debe638953eb1ded3ac73 +"@eslint/eslintrc@npm:^2.1.3": + version: 2.1.3 + resolution: "@eslint/eslintrc@npm:2.1.3" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.6.0 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: 5c6c3878192fe0ddffa9aff08b4e2f3bcc8f1c10d6449b7295a5f58b662019896deabfc19890455ffd7e60a5bd28d25d0eaefb2f78b2d230aae3879af92b89e5 languageName: node linkType: hard @@ -2585,10 +2613,17 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:8.1.0": - version: 8.1.0 - resolution: "@faker-js/faker@npm:8.1.0" - checksum: 76036cbad2f0735fe2a2834bb3e16233e7c1aa4998cf90dbd097631465f3fcd4e7022c901f80b6de1c25b47154880f06916609a81dacb039a25f9cb000a3ab4e +"@eslint/js@npm:8.53.0": + version: 8.53.0 + resolution: "@eslint/js@npm:8.53.0" + checksum: e0d5cfb0000aaee237c8e6d6d6e366faa60b1ef7f928ce17778373aa44d3b886368f6d5e1f97f913f0f16801aad016db8b8df78418c9d18825c15590328028af + languageName: node + linkType: hard + +"@faker-js/faker@npm:8.2.0": + version: 8.2.0 + resolution: "@faker-js/faker@npm:8.2.0" + checksum: febc17018acfb841a348591bfe415e815ea981bf7fa0a12670ac2b449479ad7e9e7b130c42878ec30210da964c13a620a3303dc00d63cb13ded00c6fc701e2be languageName: node linkType: hard @@ -2732,17 +2767,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.11": - version: 0.11.11 - resolution: "@humanwhocodes/config-array@npm:0.11.11" - dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 - minimatch: ^3.0.5 - checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19 - languageName: node - linkType: hard - "@humanwhocodes/config-array@npm:^0.11.13": version: 0.11.13 resolution: "@humanwhocodes/config-array@npm:0.11.13" @@ -2761,13 +2785,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 - languageName: node - linkType: hard - "@humanwhocodes/object-schema@npm:^2.0.1": version: 2.0.1 resolution: "@humanwhocodes/object-schema@npm:2.0.1" @@ -4071,10 +4088,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.10.0": - version: 1.10.0 - resolution: "@remix-run/router@npm:1.10.0" - checksum: f8f9fcd5f08465a7e0a05378398ff6df2c5c5ef5766df3490a134d64260b3b16f1bd490bb0c3f5925c2671a0c1d8d1fa01dfbdc7ecc3b2447dc6eafe6b73bcc2 +"@remix-run/router@npm:1.11.0": + version: 1.11.0 + resolution: "@remix-run/router@npm:1.11.0" + checksum: 1966436ab3ab982862195e4871790644ce21e01511aa3f4350436296224e4dec2e6ee35f1f4cb83db69f7aa0e8ad4a0a01928b05359ae654edc8e2aa82bf754b languageName: node linkType: hard @@ -4161,7 +4178,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^10.0.2, @sinonjs/fake-timers@npm:^10.3.0": +"@sinonjs/fake-timers@npm:^10.0.2": version: 10.3.0 resolution: "@sinonjs/fake-timers@npm:10.3.0" dependencies: @@ -4170,6 +4187,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^11.2.2": + version: 11.2.2 + resolution: "@sinonjs/fake-timers@npm:11.2.2" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 68c29b0e1856fdc280df03ddbf57c726420b78e9f943a241b471edc018fb14ff36fdc1daafd6026cba08c3c7f50c976fb7ae11b88ff44cd7f609692ca7d25158 + languageName: node + linkType: hard + "@sinonjs/samsam@npm:^8.0.0": version: 8.0.0 resolution: "@sinonjs/samsam@npm:8.0.0" @@ -4279,15 +4305,6 @@ __metadata: languageName: node linkType: hard -"@types/adm-zip@npm:0.5.2": - version: 0.5.2 - resolution: "@types/adm-zip@npm:0.5.2" - dependencies: - "@types/node": "*" - checksum: c25ad926fdd38100e883fbd34e94541c7893f785feb2d9f3261dee837baaf29c161d8c9b44d436d937e11e6f20218f1496bdf1cc4f16262b319033aef495ecf8 - languageName: node - linkType: hard - "@types/aria-query@npm:^5.0.1": version: 5.0.2 resolution: "@types/aria-query@npm:5.0.2" @@ -4361,12 +4378,12 @@ __metadata: languageName: node linkType: hard -"@types/color@npm:3.0.4": - version: 3.0.4 - resolution: "@types/color@npm:3.0.4" +"@types/color@npm:3.0.5": + version: 3.0.5 + resolution: "@types/color@npm:3.0.5" dependencies: "@types/color-convert": "*" - checksum: 46935626ddeb8ae8877488b1f8f2f71b17a529c113673301a1dd7a64d917cb7a748545ac34ed30c188d66a2cd09c33abc24c87bd5b3a80d520cf1f35aaa2e476 + checksum: 82cce7cb132b5c0898dd69b92a4663e34ad8f19db6a009bef7ef79d4840ddb4d5f7e793e2e64e3b4a60da92dc2ae33d1aa498981d0fcf0562917c9550603a99d languageName: node linkType: hard @@ -4488,13 +4505,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.6": - version: 29.5.6 - resolution: "@types/jest@npm:29.5.6" +"@types/jest@npm:29.5.7": + version: 29.5.7 + resolution: "@types/jest@npm:29.5.7" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: fa13a27bd1c8efd0381a419478769d0d6d3a8e93e1952d7ac3a16274e8440af6f73ed6f96ac1ff00761198badf2ee226b5ab5583a5d87a78d609ea78da5c5a24 + checksum: e28624ccb0ef1255a03fbbb4b5bc3e5cbcdc450d39e0739985ff679b124198f808c38c8c3e67859c6efc0e848196deeb8cfed028e12a821c511dfc1112a2d6e9 languageName: node linkType: hard @@ -4585,20 +4602,13 @@ __metadata: linkType: hard "@types/libsodium-wrappers@npm:*, @types/libsodium-wrappers@npm:^0": - version: 0.7.11 - resolution: "@types/libsodium-wrappers@npm:0.7.11" - checksum: e3c3acdfc178a466a04d81c030ba1b748abc9335b1d66421125eb55b32cbaf6a9076e32a98744fcb84ba2fa2af342203ff29054262dcc465c12c4feddddb64ac + version: 0.7.12 + resolution: "@types/libsodium-wrappers@npm:0.7.12" + checksum: 8f25b4ffe6b60c36f3c59b3dea2e952b8790c9b8375ee5235e6d294c1519a578b7882d773f168005eb0f3fdb4f11e06ba27b30b89d2c3b8be3f985c7eedd0491 languageName: node linkType: hard -"@types/linkify-it@npm:*": - version: 3.0.3 - resolution: "@types/linkify-it@npm:3.0.3" - checksum: a734becc4e7476833b0e6951ec133c006a34809639c722d3e28b7cf88f5f6ccbb433f195788be5e56209b1e9e6e0778879291dd2db401acee3bb585c44dcc329 - languageName: node - linkType: hard - -"@types/linkify-it@npm:3.0.4": +"@types/linkify-it@npm:*, @types/linkify-it@npm:3.0.4": version: 3.0.4 resolution: "@types/linkify-it@npm:3.0.4" checksum: cd873857faf77231811a5ee49aadffdbdd7c6309b92ca004cb28320993858d2e30cad7b343c6db928763ed0f766c6ed140e0f995536e488a1447a527b6f8127f @@ -4614,13 +4624,13 @@ __metadata: languageName: node linkType: hard -"@types/markdown-it@npm:13.0.4": - version: 13.0.4 - resolution: "@types/markdown-it@npm:13.0.4" +"@types/markdown-it@npm:13.0.5": + version: 13.0.5 + resolution: "@types/markdown-it@npm:13.0.5" dependencies: "@types/linkify-it": "*" "@types/mdurl": "*" - checksum: 0af9c349467599f984e8faf548144e5c68ca8926d45e155cbc622897290fadb1fd31fced8194a2a1095406805dd21719d2bc74d8dc0295581d0eed771a4fd58b + checksum: 3c87efe8e24f77dc9aa1962b8f18d37530b63e33ab363653e568816de6d5b6185a65690da729e2757ba370a3499635da6b4fc4d9a99df9d85959d8c64c7fe8e9 languageName: node linkType: hard @@ -4649,11 +4659,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 20.8.6 - resolution: "@types/node@npm:20.8.6" + version: 20.8.9 + resolution: "@types/node@npm:20.8.9" dependencies: - undici-types: ~5.25.1 - checksum: ccfb7ac482c5a96edeb239893c5c099f5257fcc2ed9ae62fefdfbc782b79e16dbc2af9a85b379665237bf759904b44ca2be68e75d239e0297882aad42f61905c + undici-types: ~5.26.4 + checksum: 0c05f3502a9507ff27e91dd6fd574fa6f391b3fafedcfe8e0c8d33351fb22d02c0121f854e5b6b3ecb9a8a468407ddf6e7ac0029fb236d4c7e1361ffc758a01f languageName: node linkType: hard @@ -4671,12 +4681,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.8.7": - version: 20.8.7 - resolution: "@types/node@npm:20.8.7" +"@types/node@npm:^20.8.10": + version: 20.8.10 + resolution: "@types/node@npm:20.8.10" dependencies: - undici-types: ~5.25.1 - checksum: 2173c0c03daefcb60c03a61b1371b28c8fe412e7a40dc6646458b809d14a85fbc7aeb369d957d57f0aaaafd99964e77436f29b3b579232d8f2b20c58abbd1d25 + undici-types: ~5.26.4 + checksum: 7c61190e43e8074a1b571e52ff14c880bc67a0447f2fe5ed0e1a023eb8a23d5f815658edb98890f7578afe0f090433c4a635c7c87311762544e20dd78723e515 languageName: node linkType: hard @@ -4733,7 +4743,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:18.2.14": +"@types/react-dom@npm:18.2.14, @types/react-dom@npm:^18.0.0": version: 18.2.14 resolution: "@types/react-dom@npm:18.2.14" dependencies: @@ -4742,15 +4752,6 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.0.0": - version: 18.2.13 - resolution: "@types/react-dom@npm:18.2.13" - dependencies: - "@types/react": "*" - checksum: 22ba066b141dca5a5a9227fae0afc7c94b470fff8e8a38ade72649da57a8ea04d0cb2ba3e22005e7d8e772d49bddd28855b1dd98e6defd033bba6afb6edff883 - languageName: node - linkType: hard - "@types/react-redux@npm:7.1.28": version: 7.1.28 resolution: "@types/react-redux@npm:7.1.28" @@ -4763,7 +4764,7 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:4.4.8": +"@types/react-transition-group@npm:4.4.8, @types/react-transition-group@npm:^4.4.0": version: 4.4.8 resolution: "@types/react-transition-group@npm:4.4.8" dependencies: @@ -4772,23 +4773,14 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:^4.4.0": - version: 4.4.7 - resolution: "@types/react-transition-group@npm:4.4.7" - dependencies: - "@types/react": "*" - checksum: 3b91486e7aa777a3787e773efce79a0fa9be4ec9e02d51ccda8c7532c5c5d84fbcefe248dacb4007293d85bf0794ac51603bb9cec360db81cf3657d2b7123fb9 - languageName: node - linkType: hard - -"@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:18.2.28": - version: 18.2.28 - resolution: "@types/react@npm:18.2.28" +"@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:18.2.33": + version: 18.2.33 + resolution: "@types/react@npm:18.2.33" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 81381bedeba83278f4c9febb0b83e0bd3f42a25897a50b9cb36ef53651d34b3d50f87ebf11211ea57ea575131f85d31e93e496ce46478a00b0f9bf7b26b5917a + checksum: 75903c4d53898c69dd23d0b2730eac4676dc5ade15c25c793dec855f0d7c650cb823832bb1dd881efe8895724f15b06d4bf7081ea0b82391aa3059512ad49ccf languageName: node linkType: hard @@ -4831,12 +4823,12 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:10.0.19": - version: 10.0.19 - resolution: "@types/sinon@npm:10.0.19" +"@types/sinon@npm:17.0.0": + version: 17.0.0 + resolution: "@types/sinon@npm:17.0.0" dependencies: "@types/sinonjs__fake-timers": "*" - checksum: 79cab4cfc618a37a11e519795a297aa641b30eb05e2d2c7a9b03f40845b54ef631af80bddbef3e57dcd4be6c67bd78dce5210ea9860617d870f1d365c78468b6 + checksum: 19850ffb787bea148e6429fd0d6207de2f38aa22e6ac5e856f304b4f2eb7d612aa04f315bc715b38a8605a8ff12dc40bd633096bb996472b1dff925a0e25e872 languageName: node linkType: hard @@ -5295,15 +5287,15 @@ __metadata: languageName: node linkType: hard -"@wireapp/api-client@npm:^26.4.0": - version: 26.4.0 - resolution: "@wireapp/api-client@npm:26.4.0" +"@wireapp/api-client@npm:^26.5.1": + version: 26.5.1 + resolution: "@wireapp/api-client@npm:26.5.1" dependencies: - "@wireapp/commons": ^5.2.1 + "@wireapp/commons": ^5.2.2 "@wireapp/priority-queue": ^2.1.4 "@wireapp/protocol-messaging": 1.44.0 axios: 1.5.1 - axios-retry: 3.8.0 + axios-retry: 3.8.1 http-status-codes: 2.3.0 logdown: 3.3.1 pako: 2.1.0 @@ -5312,7 +5304,7 @@ __metadata: tough-cookie: 4.1.3 ws: 8.14.2 zod: 3.22.4 - checksum: 900233b8ec83803ade60e5ec4121ef21103be33e6587a6065bd82713d0b235b010c89db59be58c50711175bd7518584477adcb06f05ce67ef54e722e3fcb5f1b + checksum: 71c27b4215532059e1357086e0a3f400ce3bfb78ac45d04e74aec79fe6ef70b241bab29d5bf970edc9e9e4ca52790c68e02ba9ecdf390bb965a44cdb3c8f4b10 languageName: node linkType: hard @@ -5330,21 +5322,21 @@ __metadata: languageName: node linkType: hard -"@wireapp/commons@npm:5.2.1, @wireapp/commons@npm:^5.2.1": - version: 5.2.1 - resolution: "@wireapp/commons@npm:5.2.1" +"@wireapp/commons@npm:5.2.2, @wireapp/commons@npm:^5.2.2": + version: 5.2.2 + resolution: "@wireapp/commons@npm:5.2.2" dependencies: ansi-regex: 5.0.1 fs-extra: 11.1.0 logdown: 3.3.1 platform: 1.3.6 - checksum: 1510b705a40d45ceaf07b12b5a199d94fe977d3b2faaafc298ff167a65b820471f5863f9f93f27d2003f9f44ee3401423d6e12bb38ecd7808f8b2fc72821d411 + checksum: ae78630f8299eaae9ee136136981dabdcb4c100c43ec1430882fc154ae21e9fdb17999c1b892140ca5547625e7f502ba83e85ecf62312c8525098865032b6928 languageName: node linkType: hard -"@wireapp/copy-config@npm:2.1.9": - version: 2.1.9 - resolution: "@wireapp/copy-config@npm:2.1.9" +"@wireapp/copy-config@npm:2.1.10": + version: 2.1.10 + resolution: "@wireapp/copy-config@npm:2.1.10" dependencies: axios: 1.5.1 copy: 0.3.2 @@ -5354,31 +5346,31 @@ __metadata: logdown: 3.3.1 bin: copy-config: lib/cli.js - checksum: 37ee9d82c6eabc33573ede5c0d9e467c2d6c3dd8f0c2717378ef8c4b84b568613d64e33f4f2903b9d4d612b25ea40f22881e0f44b51cd4f72185f0606294a168 + checksum: 5b7fa44b067c51dc567d29ebcb9e2fa87cd5464fb4bb5fcef93d4fa5596cf729d1549f0c52f2a9241f0e415678af56765925b8329f49c7efe6e93dcc97bdf1b2 languageName: node linkType: hard -"@wireapp/core-crypto@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@wireapp/core-crypto@npm:1.0.0-rc.13" - checksum: 8edbf8ffbda8db2b0a2a9dd93fe5c823fb337bb6f4eef0333d3c96ea8d594739b50511eb8d463e11863205537e505fe78fe8a25fcf6e61c675ad30a335cf1b5d +"@wireapp/core-crypto@npm:1.0.0-rc.16": + version: 1.0.0-rc.16 + resolution: "@wireapp/core-crypto@npm:1.0.0-rc.16" + checksum: 95061f0a0ee69205492ffb254a60c81be604b6a7d76e5f3e512129c2a5669adc39114e343f50002ff77db7302e1cfd39ac8dd684a87d28555b95c1e4fda7a4f3 languageName: node linkType: hard -"@wireapp/core@npm:42.17.0": - version: 42.17.0 - resolution: "@wireapp/core@npm:42.17.0" +"@wireapp/core@npm:42.19.2": + version: 42.19.2 + resolution: "@wireapp/core@npm:42.19.2" dependencies: - "@wireapp/api-client": ^26.4.0 - "@wireapp/commons": ^5.2.1 - "@wireapp/core-crypto": 1.0.0-rc.13 + "@wireapp/api-client": ^26.5.1 + "@wireapp/commons": ^5.2.2 + "@wireapp/core-crypto": 1.0.0-rc.16 "@wireapp/cryptobox": 12.8.0 - "@wireapp/promise-queue": ^2.2.6 + "@wireapp/promise-queue": ^2.2.7 "@wireapp/protocol-messaging": 1.44.0 "@wireapp/store-engine": 5.1.4 "@wireapp/store-engine-dexie": ^2.1.6 axios: 1.5.1 - bazinga64: ^6.3.1 + bazinga64: ^6.3.2 deepmerge-ts: 5.1.0 hash.js: 1.1.7 http-status-codes: 2.3.0 @@ -5387,7 +5379,7 @@ __metadata: long: ^5.2.0 uuidjs: 4.2.13 zod: 3.22.4 - checksum: 97176143fbab1c36abb7cb0ec6372347b108729cb49d43c8d332c11bba023b3ccbb49806d6edaea1a116f27e3e34ed7c53ade665e3e59fd0552129c2c10cb4eb + checksum: 24f4b254c7571d10928088db86723350bc73f015cf0b56aafa05892f4417ca098f7a0cc56be407a89ae69f3ff89a74949805f603757f3f6c91f665c97c7d5b69 languageName: node linkType: hard @@ -5470,10 +5462,10 @@ __metadata: languageName: node linkType: hard -"@wireapp/promise-queue@npm:^2.2.6": - version: 2.2.6 - resolution: "@wireapp/promise-queue@npm:2.2.6" - checksum: 6de05205a44b62dea38b6a0c00ec8d8f9bd96c98d8e4fa6cf711a9fb5cf5fd39f818432d3c676649e1d5cb638fe9386b24cc098b4514c378d995295ed2f8f3d6 +"@wireapp/promise-queue@npm:^2.2.7": + version: 2.2.7 + resolution: "@wireapp/promise-queue@npm:2.2.7" + checksum: 0e607b00325fa702c9222da4e424e31d46ba456e0b8d6af795062f79e3c774581f2e9da9860a8123a7315c510e7da09850030336e356965a5429721a9bbc7976 languageName: node linkType: hard @@ -5501,11 +5493,11 @@ __metadata: languageName: node linkType: hard -"@wireapp/react-ui-kit@npm:9.9.11": - version: 9.9.11 - resolution: "@wireapp/react-ui-kit@npm:9.9.11" +"@wireapp/react-ui-kit@npm:9.9.12": + version: 9.9.12 + resolution: "@wireapp/react-ui-kit@npm:9.9.12" dependencies: - "@types/color": 3.0.4 + "@types/color": 3.0.5 color: 4.2.3 emotion-normalize: 11.0.1 react-select: 5.7.5 @@ -5518,7 +5510,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 2bb5023b171b9b5da5084b783b8a55221f971b60acd8f7f8770f81578dd2edfa05ed8854c856235f0648231278762ac977305d602a953f31c89e762abb396b85 + checksum: 4129c63b988ccd9471177f3cc2c73d7dff161e0a9f3ebe85e431adf982e5f2a3ddb0cb11f7b66821a39512cddafcb2a99b30b6ada9aedc74bd56e1da0dbf17b5 languageName: node linkType: hard @@ -5708,13 +5700,6 @@ __metadata: languageName: node linkType: hard -"adm-zip@npm:0.5.10": - version: 0.5.10 - resolution: "adm-zip@npm:0.5.10" - checksum: 07ed91cf6423bf5dca4ee63977bc7635e91b8d21829c00829d48dce4c6932e1b19e6cfcbe44f1931c956e68795ae97183fc775913883fa48ce88a1ac11fb2034 - languageName: node - linkType: hard - "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -6244,13 +6229,13 @@ __metadata: languageName: node linkType: hard -"axios-retry@npm:3.8.0": - version: 3.8.0 - resolution: "axios-retry@npm:3.8.0" +"axios-retry@npm:3.8.1": + version: 3.8.1 + resolution: "axios-retry@npm:3.8.1" dependencies: "@babel/runtime": ^7.15.4 is-retry-allowed: ^2.2.0 - checksum: 448d951b971ccd35eaedc0f10ff1129a6bf2b3dfe13ce57749809bd37975332ae0e906ea4e67a41c9c98215bb1bf8a554e6880f1272419c758f91e4d68ca6b55 + checksum: 9233523d34987838504b1ea9d5f90025bf9d1210e10c3851c28d1e97be9f0c2dd401ddc9f0568759c79426533795de9c54bb8429720ace641032c51fef71cb0f languageName: node linkType: hard @@ -6471,10 +6456,10 @@ __metadata: languageName: node linkType: hard -"bazinga64@npm:^6.3.1": - version: 6.3.1 - resolution: "bazinga64@npm:6.3.1" - checksum: f720936b3919f8df3b903675a9557f36b5a959ec458d7536756b805a9a1e3c81d5d6a367adb0245b80e1ef870cd5f94491ac9566738016b5613b5e5e3bccce6e +"bazinga64@npm:^6.3.2": + version: 6.3.2 + resolution: "bazinga64@npm:6.3.2" + checksum: 5865c96c45120da1fc8728ef0c464b2cac85ecb955bd173912d6201ad3b96eaab9c4b1d81659e21a983c0fcaf6d6b91948499f917fc616258a032a0f31a7d16d languageName: node linkType: hard @@ -6976,13 +6961,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.2.1": - version: 1.2.1 - resolution: "clsx@npm:1.2.1" - checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 - languageName: node - linkType: hard - "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -7276,10 +7254,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.33.1": - version: 3.33.1 - resolution: "core-js@npm:3.33.1" - checksum: 3a95003b0e77995203587117f3bde7f4e96adf434b6b78033dbe60347ffe38b2bac31eafab6a4cc641e5766062846b52f336ab4553fc0902c278959af4778e53 +"core-js@npm:3.33.2": + version: 3.33.2 + resolution: "core-js@npm:3.33.2" + checksum: 71de081acbd060ff985afdcdf2552de4a00ab3ac4695c77f3535b72ddf4526920dcd0cb73e72e57c2ae16e384838a6d55790e138f0a19d60afcf851f89d0064d languageName: node linkType: hard @@ -7419,10 +7397,10 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.1.1": - version: 4.1.1 - resolution: "crypto-js@npm:4.1.1" - checksum: b3747c12ee3a7632fab3b3e171ea50f78b182545f0714f6d3e7e2858385f0f4101a15f2517e033802ce9d12ba50a391575ff4638c9de3dd9b2c4bc47768d5425 +"crypto-js@npm:^4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 languageName: node linkType: hard @@ -7688,10 +7666,10 @@ __metadata: languageName: node linkType: hard -"cssdb@npm:^7.8.0": - version: 7.8.0 - resolution: "cssdb@npm:7.8.0" - checksum: 4071a60df6edaeba1d7df63432836c59023b41bdf3c0a2b4f434cb46a4a6e1b6a5717d0003eb264503187d183827c4b24209f4ad3009b76b96df65654444207d +"cssdb@npm:^7.9.0": + version: 7.9.0 + resolution: "cssdb@npm:7.9.0" + checksum: 83c2e3192336345bfcfb824f94f46afb5e0cd8b9a9755690bc0eecf004de57a1e031c31437be74bf957f348c4808cc5c8e378f4fb910ab3fd150ac69f30ae38a languageName: node linkType: hard @@ -8318,14 +8296,12 @@ __metadata: languageName: node linkType: hard -"emoji-picker-react@npm:4.5.3": - version: 4.5.3 - resolution: "emoji-picker-react@npm:4.5.3" - dependencies: - clsx: ^1.2.1 +"emoji-picker-react@npm:4.5.14": + version: 4.5.14 + resolution: "emoji-picker-react@npm:4.5.14" peerDependencies: react: ">=16" - checksum: 79681d0e4f3a733c9fa715559b8e3e6c20810e745844718d7072d909f9e5eb2870ba782df5d23d74e0b3ce6d733bbe045b42bf4b27bf1cfc0f5ecfbc971cfe61 + checksum: c25f47effd79128b597ea5fad57dff93ce95e698a858349cdc1b2fde7c3203a227fb63cc365ee3f3a351a64ae4519e6a775aa22419ebe92cc7d72f65974b361a languageName: node linkType: hard @@ -8979,16 +8955,17 @@ __metadata: linkType: hard "eslint@npm:^8": - version: 8.51.0 - resolution: "eslint@npm:8.51.0" + version: 8.52.0 + resolution: "eslint@npm:8.52.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.51.0 - "@humanwhocodes/config-array": ^0.11.11 + "@eslint/js": 8.52.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 @@ -9021,18 +8998,18 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 214fa5d1fcb67af1b8992ce9584ccd85e1aa7a482f8b8ea5b96edc28fa838a18a3b69456db45fc1ed3ef95f1e9efa9714f737292dc681e572d471d02fda9649c + checksum: fd22d1e9bd7090e31b00cbc7a3b98f3b76020a4c4641f987ae7d0c8f52e1b88c3b268bdfdabac2e1a93513e5d11339b718ff45cbff48a44c35d7e52feba510ed languageName: node linkType: hard -"eslint@npm:^8.52.0": - version: 8.52.0 - resolution: "eslint@npm:8.52.0" +"eslint@npm:^8.53.0": + version: 8.53.0 + resolution: "eslint@npm:8.53.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 - "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.52.0 + "@eslint/eslintrc": ^2.1.3 + "@eslint/js": 8.53.0 "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -9069,7 +9046,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: fd22d1e9bd7090e31b00cbc7a3b98f3b76020a4c4641f987ae7d0c8f52e1b88c3b268bdfdabac2e1a93513e5d11339b718ff45cbff48a44c35d7e52feba510ed + checksum: 2da808655c7aa4b33f8970ba30d96b453c3071cc4d6cd60d367163430677e32ff186b65270816b662d29139283138bff81f28dddeb2e73265495245a316ed02c languageName: node linkType: hard @@ -12511,29 +12488,29 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:15.0.1": - version: 15.0.1 - resolution: "lint-staged@npm:15.0.1" +"lint-staged@npm:15.0.2": + version: 15.0.2 + resolution: "lint-staged@npm:15.0.2" dependencies: chalk: 5.3.0 commander: 11.1.0 debug: 4.3.4 execa: 8.0.1 lilconfig: 2.1.0 - listr2: 7.0.1 + listr2: 7.0.2 micromatch: 4.0.5 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.3.2 + yaml: 2.3.3 bin: lint-staged: bin/lint-staged.js - checksum: 4ef972b8fb246fa047e2f819833e675a816bfe1dd1fa4130d1b75cb6af2a51abc54eb7047a7b82403d9d4f8aefe1e71532a999161d180fb5ec019d360988b7bf + checksum: 437bc006a103eda779584b0beccef03732d1e79fe3c5d66004fee0ba641b2defe81ed8f7b4909fd1b4c59a7b7e2587d811dcc3a2e171f95573976af4294da9fc languageName: node linkType: hard -"listr2@npm:7.0.1": - version: 7.0.1 - resolution: "listr2@npm:7.0.1" +"listr2@npm:7.0.2": + version: 7.0.2 + resolution: "listr2@npm:7.0.2" dependencies: cli-truncate: ^3.1.0 colorette: ^2.0.20 @@ -12541,7 +12518,7 @@ __metadata: log-update: ^5.0.1 rfdc: ^1.3.0 wrap-ansi: ^8.1.0 - checksum: ad6a64b952505c54f18f2c18d7bc4cf92a236b2742bf5bc514994dfadd48d9dacc5abd4f599ba7cd5bd0343da1ad0a0c3f3faa034597e8905b751b75c2a6b2e8 + checksum: 1734c6b9367ceeb09bf372427930a4586b3727097373408f2f840896b9333cc80e53a1a696771a83a7d4d9ada46229843f3052b87f3b0b58c20e9451362c2dd3 languageName: node linkType: hard @@ -13366,16 +13343,16 @@ __metadata: languageName: node linkType: hard -"nise@npm:^5.1.4": - version: 5.1.4 - resolution: "nise@npm:5.1.4" +"nise@npm:^5.1.5": + version: 5.1.5 + resolution: "nise@npm:5.1.5" dependencies: "@sinonjs/commons": ^2.0.0 "@sinonjs/fake-timers": ^10.0.2 "@sinonjs/text-encoding": ^0.7.1 just-extend: ^4.0.2 path-to-regexp: ^1.7.0 - checksum: bc57c10eaec28a6a7ddfb2e1e9b21d5e1fe22710e514f8858ae477cf9c7e9c891475674d5241519193403db43d16c3675f4207bc094a7a27b7e4f56584a78c1b + checksum: c763dc62c5796cafa5c9268e14a5b34db6e6fa2f1dbc57a891fe5d7ea632a87868e22b5bb34965006f984630793ea11368351e94971163228d9e20b2e88edce8 languageName: node linkType: hard @@ -13699,13 +13676,13 @@ __metadata: languageName: node linkType: hard -"oidc-client-ts@npm:^2.2.5": - version: 2.3.0 - resolution: "oidc-client-ts@npm:2.3.0" +"oidc-client-ts@npm:^2.4.0": + version: 2.4.0 + resolution: "oidc-client-ts@npm:2.4.0" dependencies: - crypto-js: ^4.1.1 + crypto-js: ^4.2.0 jwt-decode: ^3.1.2 - checksum: 74e20b8df748f901d67aba176f5f68bd8aa5ff7eed92dc92f34479dddc49e238dc722c757c5ab0f3365d170e3031343f650c789b51baa01062e8975c7580e15d + checksum: 8467db689298221f706d3358961efb0ddc789f6bd7d4765e71ae5fe62067999d2ce6e8e7584b9d991b8caa6f7fb383f75841e1cfa9e05808c34632de374f5e68 languageName: node linkType: hard @@ -14856,11 +14833,11 @@ __metadata: languageName: node linkType: hard -"postcss-preset-env@npm:^9.2.0": - version: 9.2.0 - resolution: "postcss-preset-env@npm:9.2.0" +"postcss-preset-env@npm:^9.3.0": + version: 9.3.0 + resolution: "postcss-preset-env@npm:9.3.0" dependencies: - "@csstools/postcss-cascade-layers": ^4.0.0 + "@csstools/postcss-cascade-layers": ^4.0.1 "@csstools/postcss-color-function": ^3.0.7 "@csstools/postcss-color-mix-function": ^2.0.7 "@csstools/postcss-exponential-functions": ^1.0.1 @@ -14872,6 +14849,8 @@ __metadata: "@csstools/postcss-initial": ^1.0.0 "@csstools/postcss-is-pseudo-class": ^4.0.3 "@csstools/postcss-logical-float-and-clear": ^2.0.0 + "@csstools/postcss-logical-overflow": ^1.0.0 + "@csstools/postcss-logical-overscroll-behavior": ^1.0.0 "@csstools/postcss-logical-resize": ^2.0.0 "@csstools/postcss-logical-viewport-units": ^2.0.3 "@csstools/postcss-media-minmax": ^1.1.0 @@ -14891,7 +14870,7 @@ __metadata: css-blank-pseudo: ^6.0.0 css-has-pseudo: ^6.0.0 css-prefers-color-scheme: ^9.0.0 - cssdb: ^7.8.0 + cssdb: ^7.9.0 postcss-attribute-case-insensitive: ^6.0.2 postcss-clamp: ^4.1.0 postcss-color-functional-notation: ^6.0.2 @@ -14920,7 +14899,7 @@ __metadata: postcss-value-parser: ^4.2.0 peerDependencies: postcss: ^8.4 - checksum: 1423ec0e081379d612462952d04b7f826eb458702e10289f06a33aec14b47afa66a65562aa2209e5103056e0206782f44c13e23d062a3af4dce93b1e6a945558 + checksum: 51838c416eac4a1fb5ed64be61de8697ecb0b9dbd79133d60d76c23c9b5068f766b60d50c992f3ab92079d8d479b352ab631759ee6591442524c30a09209a381 languageName: node linkType: hard @@ -15472,27 +15451,27 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.17.0": - version: 6.17.0 - resolution: "react-router-dom@npm:6.17.0" +"react-router-dom@npm:6.18.0": + version: 6.18.0 + resolution: "react-router-dom@npm:6.18.0" dependencies: - "@remix-run/router": 1.10.0 - react-router: 6.17.0 + "@remix-run/router": 1.11.0 + react-router: 6.18.0 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: e0ba4f4c507681e2ffdecdf2e67edf7ec0e2bf4be35222e29d013afdb03866a5e6ecacc8b452bd55797b9672785d02f81bd6dbf6b05ac93a59e48e774b0060de + checksum: ca5c9a9f748f4ff9677d25762970fc59cb216568aad0ebc668b22398222a940f767680bc9a3e65a92e940d3fe05731eda8a4b352ccbf1054904b3b785a9f5e6f languageName: node linkType: hard -"react-router@npm:6.17.0": - version: 6.17.0 - resolution: "react-router@npm:6.17.0" +"react-router@npm:6.18.0": + version: 6.18.0 + resolution: "react-router@npm:6.18.0" dependencies: - "@remix-run/router": 1.10.0 + "@remix-run/router": 1.11.0 peerDependencies: react: ">=16.8" - checksum: 99c30d94fbb34657e4c8c3ef1aaae33b143167d3869b442e06c83b4006f35200fde810029180e209654bef2f47f0b27a928f77cc2d859a358a2722cc9d494f03 + checksum: 03e9a23c5b75d8813720745e2952bb9e62ec310d238cde4f19e0ce73582701fa5e04cf609ff9ced978e9e6c531b5e333b9aee35371e6c4743afc2829e32e926a languageName: node linkType: hard @@ -16395,17 +16374,17 @@ __metadata: languageName: node linkType: hard -"sinon@npm:16.1.0": - version: 16.1.0 - resolution: "sinon@npm:16.1.0" +"sinon@npm:17.0.1": + version: 17.0.1 + resolution: "sinon@npm:17.0.1" dependencies: "@sinonjs/commons": ^3.0.0 - "@sinonjs/fake-timers": ^10.3.0 + "@sinonjs/fake-timers": ^11.2.2 "@sinonjs/samsam": ^8.0.0 diff: ^5.1.0 - nise: ^5.1.4 + nise: ^5.1.5 supports-color: ^7.2.0 - checksum: b3f910e9b3d28f1241b28ac9d384f45c673b64ecfabf7ca2b94c964036118bd4166a2315f2ec2379a85745a7d4840384514aca416bbf189508a9d7fb55b9d5a8 + checksum: a807c2997d6eabdcaa4409df9fd9816a3e839f96d7e5d76610a33f5e1b60cf37616c6288f0f580262da17ea4ee626c6d1600325bf423e30c5a7f0d9a203e26c0 languageName: node linkType: hard @@ -16458,13 +16437,6 @@ __metadata: languageName: node linkType: hard -"snabbdom@npm:3.5.1": - version: 3.5.1 - resolution: "snabbdom@npm:3.5.1" - checksum: 8f3512c3e1c89deda0db4a185012dc6929a64b46c8ba44e5c7ed51d77c13d389a3605f5914af2c158d9f4a8c23b7face831d9802277fffb345a6fa1f1e330427 - languageName: node - linkType: hard - "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -17790,10 +17762,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~5.25.1": - version: 5.25.3 - resolution: "undici-types@npm:5.25.3" - checksum: ec9d2cc36520cbd9fbe3b3b6c682a87fe5be214699e1f57d1e3d9a2cb5be422e62735f06e0067dc325fd3dd7404c697e4d479f9147dc8a804e049e29f357f2ff +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 languageName: node linkType: hard @@ -18482,23 +18454,22 @@ __metadata: "@babel/preset-env": 7.23.2 "@babel/preset-react": 7.22.15 "@babel/preset-typescript": 7.23.2 - "@datadog/browser-logs": ^4.50.1 - "@datadog/browser-rum": ^4.50.1 + "@datadog/browser-logs": ^5.1.0 + "@datadog/browser-rum": ^5.1.0 "@emotion/eslint-plugin": ^11.11.0 "@emotion/react": 11.11.1 - "@faker-js/faker": 8.1.0 + "@faker-js/faker": 8.2.0 "@formatjs/cli": 6.2.1 "@koush/wrtc": 0.5.3 "@lexical/history": 0.12.2 "@lexical/react": 0.12.2 "@peculiar/x509": 1.9.5 "@testing-library/react": 14.0.0 - "@types/adm-zip": 0.5.2 "@types/dexie-batch": 0.4.6 "@types/eslint": ^8 "@types/fs-extra": 11.0.3 "@types/generate-changelog": 1.8.2 - "@types/jest": 29.5.6 + "@types/jest": 29.5.7 "@types/jquery": ^3 "@types/js-cookie": 3.0.5 "@types/jsdom": 21.1.4 @@ -18506,33 +18477,31 @@ __metadata: "@types/libsodium-wrappers": ^0 "@types/linkify-it": 3.0.4 "@types/loadable__component": ^5 - "@types/markdown-it": 13.0.4 - "@types/node": ^20.8.7 + "@types/markdown-it": 13.0.5 + "@types/node": ^20.8.10 "@types/open-graph": 0.2.4 "@types/platform": 1.3.5 - "@types/react": 18.2.28 + "@types/react": 18.2.33 "@types/react-dom": 18.2.14 "@types/react-redux": 7.1.28 "@types/react-transition-group": 4.4.8 "@types/redux-mock-store": 1.0.5 "@types/seedrandom": ^3 - "@types/sinon": 10.0.19 + "@types/sinon": 17.0.0 "@types/speakingurl": 13.0.5 "@types/underscore": 1.11.12 "@types/webpack-env": 1.18.3 "@wireapp/avs": 9.5.2 - "@wireapp/commons": 5.2.1 - "@wireapp/copy-config": 2.1.9 - "@wireapp/core": 42.17.0 + "@wireapp/commons": 5.2.2 + "@wireapp/copy-config": 2.1.10 + "@wireapp/core": 42.19.2 "@wireapp/eslint-config": 3.0.4 - "@wireapp/lru-cache": 3.8.1 "@wireapp/prettier-config": 0.6.3 - "@wireapp/react-ui-kit": 9.9.11 + "@wireapp/react-ui-kit": 9.9.12 "@wireapp/store-engine": ^5.1.4 "@wireapp/store-engine-dexie": 2.1.6 "@wireapp/store-engine-sqleet": 1.8.9 "@wireapp/webapp-events": 0.18.3 - adm-zip: 0.5.10 amplify: "https://github.com/wireapp/amplify#head=master" archiver: ^6.0.1 autoprefixer: ^10.4.16 @@ -18541,7 +18510,7 @@ __metadata: beautiful-react-hooks: ^5.0.0 classnames: 2.3.2 copy-webpack-plugin: 11.0.0 - core-js: 3.33.1 + core-js: 3.33.2 countly-sdk-web: 23.6.2 cross-env: 7.0.3 cspell: 7.3.8 @@ -18552,8 +18521,8 @@ __metadata: dexie-batch: 0.4.3 dotenv: 16.3.1 dpdm: 3.14.0 - emoji-picker-react: 4.5.3 - eslint: ^8.52.0 + emoji-picker-react: 4.5.14 + eslint: ^8.53.0 eslint-plugin-prettier: ^5.0.1 fake-indexeddb: 4.0.2 generate-changelog: 1.8.0 @@ -18579,12 +18548,12 @@ __metadata: lexical: 0.12.2 libsodium-wrappers: 0.7.13 linkify-it: 4.0.1 - lint-staged: 15.0.1 + lint-staged: 15.0.2 long: 5.2.3 markdown-it: 13.0.2 murmurhash: 2.0.1 node-fetch: 2.7.0 - oidc-client-ts: ^2.2.5 + oidc-client-ts: ^2.4.0 os-browserify: 0.3.0 path-browserify: 1.0.1 platform: 1.3.6 @@ -18592,7 +18561,7 @@ __metadata: postcss-import: ^15.1.0 postcss-less: 6.0.0 postcss-loader: ^7.3.3 - postcss-preset-env: ^9.2.0 + postcss-preset-env: ^9.3.0 postcss-scss: 4.0.9 prettier: ^3.0.3 raf: 3.4.1 @@ -18601,8 +18570,8 @@ __metadata: react-error-boundary: 4.0.11 react-intl: 6.5.1 react-redux: 8.1.3 - react-router: 6.17.0 - react-router-dom: 6.17.0 + react-router: 6.18.0 + react-router-dom: 6.18.0 react-transition-group: 4.4.5 redux: 4.2.1 redux-devtools-extension: 2.13.9 @@ -18611,8 +18580,7 @@ __metadata: redux-thunk: 2.4.2 seedrandom: ^3.0.5 simple-git: 3.20.0 - sinon: 16.1.0 - snabbdom: 3.5.1 + sinon: 17.0.1 speakingurl: 14.0.1 style-loader: ^3.3.3 stylelint: ^15 @@ -18634,7 +18602,7 @@ __metadata: webpack-hot-middleware: 2.25.4 webrtc-adapter: 8.2.3 workbox-webpack-plugin: 7.0.0 - zustand: 4.4.4 + zustand: 4.4.6 languageName: unknown linkType: soft @@ -19035,10 +19003,10 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.3.2": - version: 2.3.2 - resolution: "yaml@npm:2.3.2" - checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146 +"yaml@npm:2.3.3": + version: 2.3.3 + resolution: "yaml@npm:2.3.3" + checksum: cdfd132e7e0259f948929efe8835923df05c013c273c02bb7a2de9b46ac3af53c2778a35b32c7c0f877cc355dc9340ed564018c0242bfbb1278c2a3e53a0e99e languageName: node linkType: hard @@ -19148,9 +19116,9 @@ __metadata: languageName: node linkType: hard -"zustand@npm:4.4.4": - version: 4.4.4 - resolution: "zustand@npm:4.4.4" +"zustand@npm:4.4.6": + version: 4.4.6 + resolution: "zustand@npm:4.4.6" dependencies: use-sync-external-store: 1.2.0 peerDependencies: @@ -19164,6 +19132,6 @@ __metadata: optional: true react: optional: true - checksum: 371fd842dc704ed5983c6d64a77994c9c91867338c742d162ac95c4252b5f98fc38aeb2d5a07f48311babed5ca7dbff2d2258301db0ae143d32897bcf3ae651b + checksum: da7b00cc6dbe5cf5fc2e3fbca745317da4bbaf53bf4a6909bbd3e335242704df9689027f613461aff07eb5f672d5570bc1a2ef99d0ad7bc868920a3b331613d4 languageName: node linkType: hard