diff --git a/Dockerfile b/Dockerfile index eeceb1bcbcf..766e5d79580 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:21-alpine # For some extra dependencies... RUN apk add --no-cache dumb-init git bash diff --git a/package.json b/package.json index db0cfca98f4..c6614763af9 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "@lexical/history": "0.12.2", "@lexical/react": "0.12.2", "@peculiar/x509": "1.9.5", - "@wireapp/avs": "9.4.18", + "@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.10", + "@wireapp/react-ui-kit": "9.9.11", "@wireapp/store-engine-dexie": "2.1.6", "@wireapp/store-engine-sqleet": "1.8.9", "@wireapp/webapp-events": "0.18.3", @@ -18,11 +18,11 @@ "beautiful-react-hooks": "^5.0.0", "classnames": "2.3.2", "copy-webpack-plugin": "11.0.0", - "core-js": "3.33.0", - "countly-sdk-web": "23.6.1", + "core-js": "3.33.1", + "countly-sdk-web": "23.6.2", "date-fns": "2.30.0", "dexie-batch": "0.4.3", - "emoji-picker-react": "4.5.2", + "emoji-picker-react": "4.5.3", "highlight.js": "11.9.0", "http-status-codes": "2.3.0", "jimp": "0.22.10", @@ -42,10 +42,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.11", - "react-intl": "6.4.7", + "react-intl": "6.5.1", "react-redux": "8.1.3", - "react-router": "6.16.0", - "react-router-dom": "6.16.0", + "react-router": "6.17.0", + "react-router-dom": "6.17.0", "react-transition-group": "4.4.5", "redux": "4.2.1", "redux-logdown": "1.0.4", @@ -56,7 +56,7 @@ "underscore": "1.13.6", "uuidjs": "4.2.13", "webrtc-adapter": "8.2.3", - "zustand": "4.4.3" + "zustand": "4.4.4" }, "devDependencies": { "@babel/core": "7.23.2", @@ -67,36 +67,36 @@ "@babel/preset-typescript": "7.23.2", "@emotion/eslint-plugin": "^11.11.0", "@faker-js/faker": "8.1.0", - "@formatjs/cli": "6.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.5", + "@types/dexie-batch": "0.4.6", "@types/eslint": "^8", - "@types/fs-extra": "11.0.2", - "@types/generate-changelog": "1.8.1", - "@types/jest": "29.5.5", + "@types/fs-extra": "11.0.3", + "@types/generate-changelog": "1.8.2", + "@types/jest": "29.5.6", "@types/jquery": "^3", - "@types/js-cookie": "3.0.4", - "@types/jsdom": "21.1.3", - "@types/keyboardjs": "2.5.1", + "@types/js-cookie": "3.0.5", + "@types/jsdom": "21.1.4", + "@types/keyboardjs": "2.5.2", "@types/libsodium-wrappers": "^0", - "@types/linkify-it": "3.0.3", + "@types/linkify-it": "3.0.4", "@types/loadable__component": "^5", - "@types/markdown-it": "13.0.2", - "@types/node": "^20.8.6", - "@types/open-graph": "0.2.3", - "@types/platform": "1.3.4", + "@types/markdown-it": "13.0.4", + "@types/node": "^20.8.7", + "@types/open-graph": "0.2.4", + "@types/platform": "1.3.5", "@types/react": "18.2.28", - "@types/react-dom": "18.2.13", - "@types/react-redux": "7.1.27", - "@types/react-transition-group": "4.4.7", - "@types/redux-mock-store": "1.0.4", + "@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/speakingurl": "13.0.4", - "@types/underscore": "1.11.11", - "@types/webpack-env": "1.18.2", + "@types/speakingurl": "13.0.5", + "@types/underscore": "1.11.12", + "@types/webpack-env": "1.18.3", "@wireapp/copy-config": "2.1.9", "@wireapp/eslint-config": "3.0.4", "@wireapp/prettier-config": "0.6.3", @@ -113,7 +113,7 @@ "dexie": "3.2.4", "dotenv": "16.3.1", "dpdm": "3.14.0", - "eslint": "^8.51.0", + "eslint": "^8.52.0", "eslint-plugin-prettier": "^5.0.1", "fake-indexeddb": "4.0.2", "generate-changelog": "1.8.0", diff --git a/server/package.json b/server/package.json index b8c8a2d2ed9..63ffac1379e 100644 --- a/server/package.json +++ b/server/package.json @@ -23,12 +23,12 @@ }, "devDependencies": { "@types/express": "4.17.19", - "@types/express-sitemap-xml": "3.0.2", - "@types/express-useragent": "1.0.3", - "@types/fs-extra": "11.0.2", + "@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.2", - "@types/jest": "^29.5.5", + "@types/hbs": "4.0.3", + "@types/jest": "^29.5.6", "@types/node": "18.11.18", "jest": "29.7.0", "rimraf": "4.4.1", diff --git a/server/yarn.lock b/server/yarn.lock index 2b1e89c19d8..0d20d50b06e 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -938,21 +938,21 @@ __metadata: languageName: node linkType: hard -"@types/express-sitemap-xml@npm:3.0.2": - version: 3.0.2 - resolution: "@types/express-sitemap-xml@npm:3.0.2" +"@types/express-sitemap-xml@npm:3.0.3": + version: 3.0.3 + resolution: "@types/express-sitemap-xml@npm:3.0.3" dependencies: "@types/express": "*" - checksum: 92118dae76ca4030672be1a85cc5abfc015a54db2b8e8f02e4d3e3ef1ffe47fe9cdaac673fda2d6f21955c2d0da93aa09c019ea1aa3f3ac49df08405ec99d7b2 + checksum: d6d20da77364589fe7d43160227becb89c8920ce411bf06505f9c97473a022e39d504ff67ab896eb8fe449d5f703129938e61593fe1accb554edf4667114b454 languageName: node linkType: hard -"@types/express-useragent@npm:1.0.3": - version: 1.0.3 - resolution: "@types/express-useragent@npm:1.0.3" +"@types/express-useragent@npm:1.0.4": + version: 1.0.4 + resolution: "@types/express-useragent@npm:1.0.4" dependencies: "@types/express": "*" - checksum: 9c980165ed6d3e208ab5653e3a39c679c174c81cf5ceecca233182848796d7f0234c938ac28163e1f1e645302a7a4050cdd3e5a8acc94dd869e78fd3c0572e8a + checksum: 20019a130237d3a1063b9265bd997d45722e282ca63c8c362fe55f53a77f9670698be8db1335300574cc9a78c80b4e9212aaec2aebdb0981a8c338b2dbcb0b7c languageName: node linkType: hard @@ -968,13 +968,13 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:11.0.2": - version: 11.0.2 - resolution: "@types/fs-extra@npm:11.0.2" +"@types/fs-extra@npm:11.0.3": + version: 11.0.3 + resolution: "@types/fs-extra@npm:11.0.3" dependencies: "@types/jsonfile": "*" "@types/node": "*" - checksum: 5b3e30343ee62d2e393e1029355f13f64bab6f3416226e22492483f99da840e2e53ca22cbfa4ac3749f2f83f7086d19c009005c8fa175da01df0fae59c2d73e1 + checksum: f196bc216906e7016a6c9c549dbe204fe7e1e87515c7e961f741309e25f8e2f8c268dba3dbf0ca7f3ddab5911d39888472f8624ac0c11a461f1b2d05377e38fa languageName: node linkType: hard @@ -994,12 +994,12 @@ __metadata: languageName: node linkType: hard -"@types/hbs@npm:4.0.2": - version: 4.0.2 - resolution: "@types/hbs@npm:4.0.2" +"@types/hbs@npm:4.0.3": + version: 4.0.3 + resolution: "@types/hbs@npm:4.0.3" dependencies: handlebars: ^4.1.0 - checksum: 58f24eeab51b13d02e6457cce0069d26d7827b69b21370a62b7acb133eea06456798ca08af7bcd32494bce4777175d7bac2295cfd9d8b004b00cbaa7a50520f1 + checksum: a3e6e70894154d289da2d7af0b893ab677882a52b4322433d14c8af914eb845b75f16bae820c080e4143a68702e123a13877453f11fc08a63f079bce67ecdc1c languageName: node linkType: hard @@ -1035,13 +1035,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.5": - version: 29.5.5 - resolution: "@types/jest@npm:29.5.5" +"@types/jest@npm:^29.5.6": + version: 29.5.6 + resolution: "@types/jest@npm:29.5.6" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 56e55cde9949bcc0ee2fa34ce5b7c32c2bfb20e53424aa4ff3a210859eeaaa3fdf6f42f81a3f655238039cdaaaf108b054b7a8602f394e6c52b903659338d8c6 + checksum: fa13a27bd1c8efd0381a419478769d0d6d3a8e93e1952d7ac3a16274e8440af6f73ed6f96ac1ff00761198badf2ee226b5ab5583a5d87a78d609ea78da5c5a24 languageName: node linkType: hard @@ -5545,12 +5545,12 @@ __metadata: resolution: "wire-web-server@workspace:." dependencies: "@types/express": 4.17.19 - "@types/express-sitemap-xml": 3.0.2 - "@types/express-useragent": 1.0.3 - "@types/fs-extra": 11.0.2 + "@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.2 - "@types/jest": ^29.5.5 + "@types/hbs": 4.0.3 + "@types/jest": ^29.5.6 "@types/node": 18.11.18 "@wireapp/commons": 5.2.1 dotenv: 16.3.1 diff --git a/src/i18n/ar-SA.json b/src/i18n/ar-SA.json index 0d68083f289..bc555c0f4a7 100644 --- a/src/i18n/ar-SA.json +++ b/src/i18n/ar-SA.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "التالي", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "إضافة", "addParticipantsHeader": "أضف أشخاصًا", "addParticipantsHeaderWithCounter": "أضف أشخاصًا ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "وصلت", "conversationMissedMessages": "لم تستخدم هذا الجهاز منذ مدة. ربما لن تظهر بعض الرسائل هنا.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/bn-BD.json b/src/i18n/bn-BD.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/bn-BD.json +++ b/src/i18n/bn-BD.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ca-ES.json b/src/i18n/ca-ES.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/ca-ES.json +++ b/src/i18n/ca-ES.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/cs-CZ.json b/src/i18n/cs-CZ.json index 16f7cfc0dd6..310fe0524d8 100644 --- a/src/i18n/cs-CZ.json +++ b/src/i18n/cs-CZ.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Další", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Přidat", "addParticipantsHeader": "Přidat účastníky", "addParticipantsHeaderWithCounter": "Přidat účastníky ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Doručeno", "conversationMissedMessages": "Toto zařízení jste nějakou dobu nepoužíval(a). Některé zprávy se nemusí zobrazit.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/da-DK.json b/src/i18n/da-DK.json index 312d136f507..8c224ee8ae4 100644 --- a/src/i18n/da-DK.json +++ b/src/i18n/da-DK.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Næste", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Tilføj", "addParticipantsHeader": "Tilføj personer", "addParticipantsHeaderWithCounter": "Tilføj personer ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Leveret", "conversationMissedMessages": "Du har ikke brugt denne enhed i et stykke tid. Nogle meddelelser vises måske ikke her.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/de-DE.json b/src/i18n/de-DE.json index 676967e2593..e1fd7fba54b 100644 --- a/src/i18n/de-DE.json +++ b/src/i18n/de-DE.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Weiter", "accountForm.terms": "Ich akzeptiere die Nutzungsbedingungen", "accountForm.termsAndPrivacyPolicy": "Ich akzeptiere die Datenschutzerklärung und Nutzungsbedingungen", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Wiederholen", + "acme.error.button.secondary": "Abbrechen", + "acme.error.headline": "Ein Fehler ist aufgetreten", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Zertifikat erhalten", + "acme.inProgress.paragraph.alt": "Herunterladen…", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Zertifikat erhalten", + "acme.settingsChanged.button.secondary": "Später erinnern", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team-Einstellungen geändert", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Hinzufügen", "addParticipantsHeader": "Teilnehmer hinzufügen", "addParticipantsHeaderWithCounter": "Teilnehmer hinzufügen ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] hat {{users}} entfernt", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} wurde aus dieser Unterhaltung entfernt, da die gesetzliche Aufbewahrung aktiviert wurde. [link]Mehr erfahren[/link]", "conversationMemberRemovedYou": "[bold]Sie[/bold] haben {{users}} entfernt", + "conversationMemberWereRemoved": "{{users}} wurden aus der Unterhaltung entfernt", "conversationMessageDelivered": "Zugestellt", "conversationMissedMessages": "Sie haben dieses Gerät eine Zeit lang nicht benutzt. Einige Nachrichten werden hier möglicherweise nicht angezeigt.", "conversationModalRestrictedFileSharingDescription": "Diese Datei konnte aufgrund von Einschränkungen bei der Dateifreigabe nicht geteilt werden.", diff --git a/src/i18n/el-GR.json b/src/i18n/el-GR.json index 323552ac623..83b447cc112 100644 --- a/src/i18n/el-GR.json +++ b/src/i18n/el-GR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Επόμενο", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Προσθήκη", "addParticipantsHeader": "Προσθήκη ατόμων", "addParticipantsHeaderWithCounter": "Προσθήκη ατόμων ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Παραδόθηκε", "conversationMissedMessages": "Δεν έχετε χρησιμοποιήσει αυτή την συσκευή για ένα χρονικό διάστημα. Ορισμένα μηνύματα ενδέχεται να μην εμφανίζονται εδώ.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 1dec0c2a9fd..aee2c1f716c 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -136,25 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", - "acme.settingsChanged.headline.main": "Team settings changed", - "acme.settingsChanged.headline.alt": "End-to-end identify certificate", - "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", - "acme.settingsChanged.button.primary": "Get Certificate", - "acme.settingsChanged.button.secondary": "Remind Me Later", - "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", - "acme.inProgress.headline": "Getting Certificate", - "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", - "acme.inProgress.paragraph.alt": "Downloading...", - "acme.inProgress.button.close": "Close window 'Getting Certificate'", - "acme.done.headline": "Certificate Downloaded", - "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", "acme.done.button": "Ok", "acme.done.button.close": "Close window 'Certificate Downloaded'", - "acme.error.headline": "Something went wrong", - "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", "acme.error.button.primary": "Retry", "acme.error.button.secondary": "Cancel", - "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -431,8 +431,8 @@ "conversationMemberLeftYou": "[bold]You[/bold] left", "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", - "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/es-ES.json b/src/i18n/es-ES.json index 29af90762cc..1e519233a6f 100644 --- a/src/i18n/es-ES.json +++ b/src/i18n/es-ES.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Siguiente", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Agregar", "addParticipantsHeader": "Agregar participantes", "addParticipantsHeaderWithCounter": "Añadir participantes ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] ha removido a {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Tú[/bold] has removido a {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Entregado", "conversationMissedMessages": "No has utilizado este dispositivo durante un tiempo. Algunos mensajes no aparecerán aquí.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/et-EE.json b/src/i18n/et-EE.json index 6dd288bc64e..7e692621d01 100644 --- a/src/i18n/et-EE.json +++ b/src/i18n/et-EE.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Järgmine", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Lisa", "addParticipantsHeader": "Lisa osalejaid", "addParticipantsHeaderWithCounter": "Lisa osalejaid ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] eemaldas {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Sina[/bold] eemaldasid {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Kohale toimetatud", "conversationMissedMessages": "Sa ei ole seda seadet mõnda aega kasutanud. Osad sõnumid ei pruugi siia ilmuda.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/fa-IR.json b/src/i18n/fa-IR.json index 257bd9ce89f..fb24a96edc7 100644 --- a/src/i18n/fa-IR.json +++ b/src/i18n/fa-IR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "اضافه کردن", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "رسید", "conversationMissedMessages": "شما از این دستگاه مدتی است که استفاده نکرده‌اید. بعضی از پیام‌ها در اینجا ظاهر نمی‌شود.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/fi-FI.json b/src/i18n/fi-FI.json index 0e4da5a00e6..f7ac271e6b1 100644 --- a/src/i18n/fi-FI.json +++ b/src/i18n/fi-FI.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Seuraava", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Lisää", "addParticipantsHeader": "Lisää osallistujia", "addParticipantsHeaderWithCounter": "Lisää osallistujia ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Toimitettu", "conversationMissedMessages": "Et ole käyttänyt tätä laitetta pitkään aikaan. Jotkut viestit eivät saata näkyä täällä.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/fr-FR.json b/src/i18n/fr-FR.json index 150a5342b52..22db9a64526 100644 --- a/src/i18n/fr-FR.json +++ b/src/i18n/fr-FR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Suivant", "accountForm.terms": "J'accepte les termes d'utilisation", "accountForm.termsAndPrivacyPolicy": "J'accepte les la politique de confidentialité et conditions d'utilisation", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Ajouter", "addParticipantsHeader": "Ajouter des participants", "addParticipantsHeaderWithCounter": "Ajouter des participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] a exclu {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Vous[/bold] avez exclu {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Distribué", "conversationMissedMessages": "Vous n’avez pas utilisé cet appareil depuis un moment. Il est possible que certains messages n’apparaissent pas ici.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ga-IE.json b/src/i18n/ga-IE.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/ga-IE.json +++ b/src/i18n/ga-IE.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/he-IL.json b/src/i18n/he-IL.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/he-IL.json +++ b/src/i18n/he-IL.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/hi-IN.json b/src/i18n/hi-IN.json index 0199dbe94c2..1a73a114438 100644 --- a/src/i18n/hi-IN.json +++ b/src/i18n/hi-IN.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "आपने कुछ समय के लिए इस यंत्र का उपयोग नहीं किया है| कुछ संदेश शायद यहां दिखाई नहीं दे|", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/hr-HR.json b/src/i18n/hr-HR.json index 19050cef3d2..4a566f65c29 100644 --- a/src/i18n/hr-HR.json +++ b/src/i18n/hr-HR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Sljedeće", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Dodaj", "addParticipantsHeader": "Dodaj sudionike", "addParticipantsHeaderWithCounter": "Dodaj ({{number}}) sudionika", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] je uklonio {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Vi[/bold] ste uklonili {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Dostavljeno", "conversationMissedMessages": "Niste upotrebljavali ovaj uređaj neko vrijeme. Neke poruke možda neće biti vidljive na njemu.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/hu-HU.json b/src/i18n/hu-HU.json index 69cb585a746..3a612a4b054 100644 --- a/src/i18n/hu-HU.json +++ b/src/i18n/hu-HU.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Tovább", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Hozzáadás", "addParticipantsHeader": "Partnerek hozzáadása", "addParticipantsHeaderWithCounter": "Partnerek hozzáadása ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] eltávolította: {{users}}\n", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Te[/bold] eltávolítottad {{users}} felhasználót\n", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Kézbesítve", "conversationMissedMessages": "Ezt a készüléket már nem használtad egy ideje, ezért nem biztos, hogy minden üzenet megjelenik itt.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/id-ID.json b/src/i18n/id-ID.json index 8bc8f834744..92bdfb524ee 100644 --- a/src/i18n/id-ID.json +++ b/src/i18n/id-ID.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Tambahkan", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Terkirim", "conversationMissedMessages": "Anda belum pernah menggunakan perangkat ini untuk sementara waktu. Beberapa pesan mungkin tidak muncul di sini.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/is-IS.json b/src/i18n/is-IS.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/is-IS.json +++ b/src/i18n/is-IS.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/it-IT.json b/src/i18n/it-IT.json index ebfe826fe6a..b322efb18e8 100644 --- a/src/i18n/it-IT.json +++ b/src/i18n/it-IT.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Aggiungi", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Consegnato", "conversationMissedMessages": "È da un po’ di tempo che non utilizzi questo dispositivo. Alcuni messaggi potrebbero non apparire qui.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ja-JP.json b/src/i18n/ja-JP.json index 709ad92799e..39b6c175c97 100644 --- a/src/i18n/ja-JP.json +++ b/src/i18n/ja-JP.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "次へ", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "追加する", "addParticipantsHeader": "人を追加します", "addParticipantsHeaderWithCounter": "({{number}}) を追加します。", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] が {{users}} を削除しました", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]あなた[/bold] が {{users}} を削除しました", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "配信済み", "conversationMissedMessages": "しばらくの間、このデバイスでWireを利用していなかったため、いくつかのメッセージがここに表示されない可能性があります。", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/lt-LT.json b/src/i18n/lt-LT.json index 7e1afc91b31..de6f6ffbb5d 100644 --- a/src/i18n/lt-LT.json +++ b/src/i18n/lt-LT.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Kitas", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Pridėti", "addParticipantsHeader": "Pridėti žmonių", "addParticipantsHeaderWithCounter": "Pridėti žmonių ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] pašalino {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Jūs[/bold] pašalinote {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Pristatyta", "conversationMissedMessages": "Jūs kurį laiką nenaudojote šio įrenginio. Kai kurios žinutės čia gali neatsirasti.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/lv-LV.json b/src/i18n/lv-LV.json index a792141e572..fe4cf67fb2b 100644 --- a/src/i18n/lv-LV.json +++ b/src/i18n/lv-LV.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Pievienot", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Piegādāts", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ms-MY.json b/src/i18n/ms-MY.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/ms-MY.json +++ b/src/i18n/ms-MY.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/nl-NL.json b/src/i18n/nl-NL.json index b4beb606986..a50f9fca030 100644 --- a/src/i18n/nl-NL.json +++ b/src/i18n/nl-NL.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Volgende", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Toevoegen", "addParticipantsHeader": "Personen toevoegen", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Afgeleverd", "conversationMissedMessages": "Je hebt dit apparaat een tijdje niet gebruikt. Sommige berichten worden hier niet getoond.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/no-NO.json b/src/i18n/no-NO.json index bb1db1c018f..7c247a5a81f 100644 --- a/src/i18n/no-NO.json +++ b/src/i18n/no-NO.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Legg til", "addParticipantsHeader": "Legg til deltakere", "addParticipantsHeaderWithCounter": "Legg til deltakere ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Levert", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/pl-PL.json b/src/i18n/pl-PL.json index 39e39682655..0628cb9e637 100644 --- a/src/i18n/pl-PL.json +++ b/src/i18n/pl-PL.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Dalej", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Dodaj", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Dostarczono", "conversationMissedMessages": "Dość długo nie używałeś tego urządzenia. Niektóre wiadomości mogą nie być widoczne.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/pt-BR.json b/src/i18n/pt-BR.json index 88db36b714d..2e4234308fa 100644 --- a/src/i18n/pt-BR.json +++ b/src/i18n/pt-BR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Próximo", "accountForm.terms": "Eu aceito os termos e condições", "accountForm.termsAndPrivacyPolicy": "Eu aceito a política de privacidade e os termos e condições", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Adicionar", "addParticipantsHeader": "Adicionar participantes", "addParticipantsHeaderWithCounter": "Adicionar participantes ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removeu {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} foi removido desta conversa porque a Retenção Legal foi ativada. [link]Saiba mais[/link]", "conversationMemberRemovedYou": "[bold]Você[/bold] removeu {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Entregue", "conversationMissedMessages": "Você não usa este dispositivo há algum tempo. Algumas mensagens podem não aparecer aqui.", "conversationModalRestrictedFileSharingDescription": "Este arquivo não pôde ser compartilhado devido às suas restrições de compartilhamento.", diff --git a/src/i18n/pt-PT.json b/src/i18n/pt-PT.json index 986692d52de..9ab0a5ffd6f 100644 --- a/src/i18n/pt-PT.json +++ b/src/i18n/pt-PT.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Seguinte", "accountForm.terms": "Eu aceito os termos e condições", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Adicionar", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Entregue", "conversationMissedMessages": "Não usou este dispositivo durante algum tempo. Algumas mensagens podem não ser mostradas.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ro-RO.json b/src/i18n/ro-RO.json index f9a5e02586e..cbd9c5810ab 100644 --- a/src/i18n/ro-RO.json +++ b/src/i18n/ro-RO.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Mai departe", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Adaugă", "addParticipantsHeader": "Adaugă persoane", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Livrat", "conversationMissedMessages": "Nu ai folosit acest dispozitiv de ceva timp. Unele mesaje ar putea să nu apară aici.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/ru-RU.json b/src/i18n/ru-RU.json index 83dc5c9fd6d..9687fbfd817 100644 --- a/src/i18n/ru-RU.json +++ b/src/i18n/ru-RU.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Вперед", "accountForm.terms": "Я принимаю условия и положения", "accountForm.termsAndPrivacyPolicy": "Я принимаю политику конфиденциальности и условия и положения", + "acme.done.button": "OK", + "acme.done.button.close": "Закрыть окно 'Сертификат загружен'", + "acme.done.headline": "Сертификат загружен", + "acme.done.paragraph": "Теперь сертификат активен, и ваше устройство верифицировано. Более подробную информацию об этом сертификате можно найти в Настройках Wire в разделе Устройства.", + "acme.error.button.close": "Закрыть окно 'Что-то пошло не так'", + "acme.error.button.primary": "Повторить", + "acme.error.button.secondary": "Отмена", + "acme.error.headline": "Что-то пошло не так", + "acme.error.paragraph": "Сертификат загрузить не удалось

Вы можете повторить попытку получения сертификата сейчас. Или система попытается сделать это автоматически в фоновом режиме в течение следующих 24 часов.", + "acme.inProgress.button.close": "Закрыть окно 'Получение сертификата'", + "acme.inProgress.headline": "Получить сертификат", + "acme.inProgress.paragraph.alt": "Загрузка...", + "acme.inProgress.paragraph.main": "Пожалуйста, перейдите в браузер и войдите в сервис сертификации.

В случае если сайт не открывается, вы можете перейти на него вручную по следующему URL:

{{url}}", + "acme.settingsChanged.button.close": "Закрыть окно 'Сертификат сквозной идентификации'", + "acme.settingsChanged.button.primary": "Получить сертификат", + "acme.settingsChanged.button.secondary": "Напомнить позже", + "acme.settingsChanged.headline.alt": "Сертификат сквозной идентификации", + "acme.settingsChanged.headline.main": "Настройки команды изменены", + "acme.settingsChanged.paragraph": "Для повышения безопасности использования Wire предусмотрена сквозная идентификация. Верификация устройства теперь происходит автоматически с использованием сертификата.

Введите свои учетные данные Wire на внешней веб-странице, на которую мы вас перенаправим. При таком входе вы автоматически получаете сертификат верификации для данного устройства. При этом будет сохраняться история бесед.

Узнайте больше о сквозной идентификации .", "addParticipantsConfirmLabel": "Добавить", "addParticipantsHeader": "Добавить участников", "addParticipantsHeaderWithCounter": "Добавить участников ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] удален(-а) {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} был(-а) удален(-а) из этой беседы, поскольку были активированы юридические ограничения. [link]Подробнее[/link]", "conversationMemberRemovedYou": "[bold]Вы[/bold] удалены {{users}}", + "conversationMemberWereRemoved": "{{users}} были удалены из беседы", "conversationMessageDelivered": "Доставлено", "conversationMissedMessages": "Вы не использовали это устройство продолжительное время. Некоторые сообщения могут не отображаться.", "conversationModalRestrictedFileSharingDescription": "Этим файлом нельзя поделиться из-за ограничений на совместное использование файлов.", diff --git a/src/i18n/si-LK.json b/src/i18n/si-LK.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/si-LK.json +++ b/src/i18n/si-LK.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/sk-SK.json b/src/i18n/sk-SK.json index dfa4818fac2..e55784af6ba 100644 --- a/src/i18n/sk-SK.json +++ b/src/i18n/sk-SK.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Pridať", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Doručená", "conversationMissedMessages": "Na chvíľu ste nepoužili toto zariadenie. Niektoré správy sa nemusia zobraziť.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/sl-SI.json b/src/i18n/sl-SI.json index 31277f88d61..9ff238e8c3f 100644 --- a/src/i18n/sl-SI.json +++ b/src/i18n/sl-SI.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Dodaj", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Dostavljeno", "conversationMissedMessages": "Te naprave nekaj časa niste uporabljali. Nekatera sporočila se morda tukaj ne bodo pojavila.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/sr-SP.json b/src/i18n/sr-SP.json index 4fd78fa94f5..26bfd3833d5 100644 --- a/src/i18n/sr-SP.json +++ b/src/i18n/sr-SP.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Следећe", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Додај", "addParticipantsHeader": "Додајте учеснике", "addParticipantsHeaderWithCounter": "Додајте учеснике ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] уклоњено је {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]ви[/bold] сте уклонили {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "испоручено", "conversationMissedMessages": "Овај уређај нисте користили неко време. Неке поруке се можда неће приказати овде.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/sv-SE.json b/src/i18n/sv-SE.json index 7c4ea6b991b..cc440dda7aa 100644 --- a/src/i18n/sv-SE.json +++ b/src/i18n/sv-SE.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Lägg till", "addParticipantsHeader": "Lägg till människor", "addParticipantsHeaderWithCounter": "Lägg till ({{number}}) människor", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] tog bort {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Du[/bold] tog bort {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Levererat", "conversationMissedMessages": "Du har inte använt denna enhet på ett tag. Några meddelanden kanske inte dyker upp här.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/th-TH.json b/src/i18n/th-TH.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/th-TH.json +++ b/src/i18n/th-TH.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/tr-TR.json b/src/i18n/tr-TR.json index 7b14a375515..900c6fe821c 100644 --- a/src/i18n/tr-TR.json +++ b/src/i18n/tr-TR.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "İleri", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Ekle", "addParticipantsHeader": "Katılımcıları ekle", "addParticipantsHeaderWithCounter": "({{number}}) Katılımcı ekle", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] {{users}} kullanıcısını çıkardı", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Siz[/bold] {{users}} kullanıcısını çıkardınız", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Teslim edildi", "conversationMissedMessages": "Bir süredir bu cihazı kullanmıyorsun. Bazı mesajlar gösterilmeyebilir.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/uk-UA.json b/src/i18n/uk-UA.json index d0606089d77..07fd45cf502 100644 --- a/src/i18n/uk-UA.json +++ b/src/i18n/uk-UA.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Далі", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Додати", "addParticipantsHeader": "Додати учасників", "addParticipantsHeaderWithCounter": "Додати учасників ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] видалив(-ла) {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]Ви[/bold] видалили {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Доставлене", "conversationMissedMessages": "Ви не користувались цим простроєм протягом певного часу. Деякі повідомлення можуть не відображатися тут.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/uz-UZ.json b/src/i18n/uz-UZ.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/uz-UZ.json +++ b/src/i18n/uz-UZ.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/vi-VN.json b/src/i18n/vi-VN.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/vi-VN.json +++ b/src/i18n/vi-VN.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json index 37eb6a7c054..755fe515b73 100644 --- a/src/i18n/zh-CN.json +++ b/src/i18n/zh-CN.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "继续", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "新增", "addParticipantsHeader": "新增好友", "addParticipantsHeaderWithCounter": "添加人员({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] 移除了{{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]你[/bold] 移除了{{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "已送达", "conversationMissedMessages": "您已经有一段时间没有使用此设备, 一些信息可能不会在这里出现。", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/zh-HK.json b/src/i18n/zh-HK.json index 7ede419df6b..2d912e9f5cb 100644 --- a/src/i18n/zh-HK.json +++ b/src/i18n/zh-HK.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "Add", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "Delivered", "conversationMissedMessages": "You haven’t used this device for a while. Some messages may not appear here.", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/i18n/zh-TW.json b/src/i18n/zh-TW.json index 255e774dfca..429e40c0bd2 100644 --- a/src/i18n/zh-TW.json +++ b/src/i18n/zh-TW.json @@ -136,6 +136,25 @@ "accountForm.submitButton": "Next", "accountForm.terms": "I accept the terms and conditions", "accountForm.termsAndPrivacyPolicy": "I accept the privacy policy and terms and conditions", + "acme.done.button": "Ok", + "acme.done.button.close": "Close window 'Certificate Downloaded'", + "acme.done.headline": "Certificate Downloaded", + "acme.done.paragraph": "The certificate is active now and your device is verified. You can find more details about this certificate in your Wire Settings under Devices.", + "acme.error.button.close": "Close window 'Something went wrong'", + "acme.error.button.primary": "Retry", + "acme.error.button.secondary": "Cancel", + "acme.error.headline": "Something went wrong", + "acme.error.paragraph": "Certiticate could not no downloaded

You can retry to get the certificate now. Or the system will try it automatically in the background during the next 24 hours.", + "acme.inProgress.button.close": "Close window 'Getting Certificate'", + "acme.inProgress.headline": "Getting Certificate", + "acme.inProgress.paragraph.alt": "Downloading...", + "acme.inProgress.paragraph.main": "Please navigate to your web browser and log in to the certiticate service.

In case the website does not open. you can also navigate there manually by following this URL:

{{url}}", + "acme.settingsChanged.button.close": "Close window 'End-to-end identify certificate'", + "acme.settingsChanged.button.primary": "Get Certificate", + "acme.settingsChanged.button.secondary": "Remind Me Later", + "acme.settingsChanged.headline.alt": "End-to-end identify certificate", + "acme.settingsChanged.headline.main": "Team settings changed", + "acme.settingsChanged.paragraph": "End-to-end identity is enabled for you to make Wire's usage more secure. The device verification takes as of now place automatically using a certificate.

Enter your Wire credentials on the external webpage we will redirect you to. With this login, you automatically get the verification certificate for this device. You will keep your conversation history.

Learn more about end-to-end identity ", "addParticipantsConfirmLabel": "加入", "addParticipantsHeader": "Add participants", "addParticipantsHeaderWithCounter": "Add participants ({{number}})", @@ -413,6 +432,7 @@ "conversationMemberRemoved": "[bold]{{name}}[/bold] removed {{users}}", "conversationMemberRemovedMissingLegalHoldConsent": "{{user}} was removed from this conversation because legal hold has been activated. [link]Learn more[/link]", "conversationMemberRemovedYou": "[bold]You[/bold] removed {{users}}", + "conversationMemberWereRemoved": "{{users}} were removed from the conversation", "conversationMessageDelivered": "已傳送", "conversationMissedMessages": "您已經有一段時間沒有使用此設備了,可能有部分訊息不會顯示出來。", "conversationModalRestrictedFileSharingDescription": "This file could not be shared due to your file sharing restrictions.", diff --git a/src/script/calling/CallingRepository.ts b/src/script/calling/CallingRepository.ts index ab6ed043ba1..289f732273c 100644 --- a/src/script/calling/CallingRepository.ts +++ b/src/script/calling/CallingRepository.ts @@ -143,6 +143,7 @@ export class CallingRepository { private wCall?: Wcall; private wUser: number = 0; private nextMuteState: MuteState = MuteState.SELF_MUTED; + private isConferenceCallingSupported = false; /** * Keeps track of the size of the avs log once the webapp is initiated. This allows detecting meaningless avs logs (logs that have a length equal to the length when the webapp was initiated) */ @@ -255,6 +256,7 @@ export class CallingRepository { wCall.setUserMediaHandler(this.getCallMediaStream); wCall.setAudioStreamHandler(this.updateCallAudioStreams); wCall.setVideoStreamHandler(this.updateParticipantVideoStream); + this.isConferenceCallingSupported = wCall.isConferenceCallingSupported(); setInterval(() => wCall.poll(), 500); return wCall; } @@ -499,7 +501,7 @@ export class CallingRepository { * @see https://www.chromestatus.com/feature/6321945865879552 */ get supportsConferenceCalling(): boolean { - return Runtime.isSupportingConferenceCalling(); + return this.isConferenceCallingSupported; } /** diff --git a/src/script/components/Conversation/Conversation.tsx b/src/script/components/Conversation/Conversation.tsx index 83955946d54..00d3f37424a 100644 --- a/src/script/components/Conversation/Conversation.tsx +++ b/src/script/components/Conversation/Conversation.tsx @@ -537,27 +537,32 @@ export const Conversation = ({ setMsgElementsFocusable={setMsgElementsFocusable} /> - {showReadOnlyConversationMessage ? ( - - ) : ( - setMsgElementsFocusable(false)} - uploadDroppedFiles={uploadDroppedFiles} - uploadImages={uploadImages} - uploadFiles={uploadFiles} - /> - )} + {isConversationLoaded && + (showReadOnlyConversationMessage ? ( + + ) : ( + setMsgElementsFocusable(false)} + uploadDroppedFiles={uploadDroppedFiles} + uploadImages={uploadImages} + uploadFiles={uploadFiles} + /> + ))}
diff --git a/src/script/conversation/ConversationCellState.ts b/src/script/conversation/ConversationCellState.ts index 5059f037def..28d241e53ea 100644 --- a/src/script/conversation/ConversationCellState.ts +++ b/src/script/conversation/ConversationCellState.ts @@ -248,14 +248,9 @@ const _getStateGroupActivity = { return ''; }, icon: (conversationEntity: Conversation): ConversationStatusIcon | void => { - const lastMessageEntity = conversationEntity.getNewestMessage(); - const isMemberRemoval = lastMessageEntity.isMember() && (lastMessageEntity as MemberMessage).isMemberRemoval(); - - if (isMemberRemoval) { - return conversationEntity.showNotificationsEverything() - ? ConversationStatusIcon.UNREAD_MESSAGES - : ConversationStatusIcon.MUTED; - } + return conversationEntity.showNotificationsEverything() + ? ConversationStatusIcon.UNREAD_MESSAGES + : ConversationStatusIcon.MUTED; }, match: (conversationEntity: Conversation) => { const lastMessageEntity = conversationEntity.getNewestMessage(); @@ -307,7 +302,7 @@ const _getStateRemoved = { return ''; }, - icon: () => ConversationStatusIcon.NONE, + icon: () => ConversationStatusIcon.UNREAD_MESSAGES, match: (conversationEntity: Conversation) => conversationEntity.removed_from_conversation(), }; diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts index fc8a89dbe59..859020e3f6f 100644 --- a/src/script/conversation/ConversationRepository.ts +++ b/src/script/conversation/ConversationRepository.ts @@ -163,7 +163,6 @@ export enum CONVERSATION_READONLY_STATE { export class ConversationRepository { private isBlockingNotificationHandling: boolean; - private readonly conversationsWithNewEvents: Map; private readonly ephemeralHandler: ConversationEphemeralHandler; public readonly conversationLabelRepository: ConversationLabelRepository; public readonly conversationRoleRepository: ConversationRoleRepository; @@ -299,7 +298,6 @@ export class ConversationRepository { } this.isBlockingNotificationHandling = true; - this.conversationsWithNewEvents = new Map(); this.teamState.isTeam.subscribe(() => this.mapGuestStatusSelf()); @@ -1979,9 +1977,6 @@ export class ConversationRepository { const isFetchingFromStream = handlingState !== NOTIFICATION_HANDLING_STATE.WEB_SOCKET; if (this.isBlockingNotificationHandling !== isFetchingFromStream) { - if (!isFetchingFromStream) { - this.checkChangedConversations(); - } this.isBlockingNotificationHandling = isFetchingFromStream; this.logger.info(`Block handling of conversation events: ${this.isBlockingNotificationHandling}`); } @@ -2481,16 +2476,6 @@ export class ConversationRepository { this.onMemberUpdate(conversationEntity, response); } - private checkChangedConversations() { - this.conversationsWithNewEvents.forEach(conversationEntity => { - if (conversationEntity.shouldUnarchive()) { - this.unarchiveConversation(conversationEntity, false, ConversationRepository.eventFromStreamMessage); - } - }); - - this.conversationsWithNewEvents.clear(); - } - /** * Clears conversation content from view and the database. * @@ -2685,19 +2670,10 @@ export class ConversationRepository { const onEventPromise = isConversationCreate ? Promise.resolve(null) : this.getConversationById(conversationId, true); - let previouslyArchived = false; return onEventPromise .then((conversationEntity: Conversation) => { if (conversationEntity) { - // Check if conversation was archived - previouslyArchived = conversationEntity.is_archived(); - const isPastMemberStatus = conversationEntity.status() === ConversationStatus.PAST_MEMBER; - const isMemberJoinType = type === CONVERSATION_EVENT.MEMBER_JOIN; - - if (previouslyArchived && isPastMemberStatus && isMemberJoinType) { - this.unarchiveConversation(conversationEntity, false, ConversationRepository.eventFromStreamMessage); - } const isBackendTimestamp = eventSource !== EventSource.INJECTED; const eventsToSkip: (CLIENT_CONVERSATION_EVENT | CONVERSATION_EVENT)[] = [ @@ -2723,7 +2699,7 @@ export class ConversationRepository { ) .then((entityObject = {} as EntityObject) => { if (type !== CONVERSATION_EVENT.MEMBER_JOIN && type !== CONVERSATION_EVENT.MEMBER_LEAVE) { - this.handleConversationNotification(entityObject as EntityObject, eventSource, previouslyArchived); + this.handleConversationNotification(entityObject as EntityObject, eventSource); } }) .catch((error: BaseError) => { @@ -2961,14 +2937,9 @@ export class ConversationRepository { * * @param entityObject Object containing the conversation and the message that are targeted by the event * @param eventSource Source of event - * @param previouslyArchived `true` if the previous state of the conversation was archived * @returns Resolves when the conversation was updated */ - private async handleConversationNotification( - entityObject: EntityObject, - eventSource: EventSource, - previouslyArchived: boolean, - ) { + private async handleConversationNotification(entityObject: EntityObject, eventSource: EventSource) { const {conversationEntity, messageEntity} = entityObject; if (conversationEntity) { @@ -2990,18 +2961,6 @@ export class ConversationRepository { conversationEntity.cleared_timestamp(0); } } - - // Check if event needs to be un-archived - if (previouslyArchived) { - // Add to check for un-archiving at the end of stream handling - if (eventFromStream) { - return this.conversationsWithNewEvents.set(conversationEntity.id, conversationEntity); - } - - if (eventFromWebSocket && conversationEntity.shouldUnarchive()) { - return this.unarchiveConversation(conversationEntity, false, 'event from WebSocket'); - } - } } } diff --git a/src/script/conversation/EventBuilder.ts b/src/script/conversation/EventBuilder.ts index cec2dbe6688..09524d1ec02 100644 --- a/src/script/conversation/EventBuilder.ts +++ b/src/script/conversation/EventBuilder.ts @@ -57,7 +57,6 @@ export interface BaseEvent { export interface ConversationEvent extends BaseEvent { data: Data; - id: string; type: Type; } diff --git a/src/script/entity/Conversation.test.ts b/src/script/entity/Conversation.test.ts index f92d259da63..83635207caf 100644 --- a/src/script/entity/Conversation.test.ts +++ b/src/script/entity/Conversation.test.ts @@ -21,22 +21,17 @@ import {ConnectionStatus} from '@wireapp/api-client/lib/connection/'; import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation/'; -import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/'; import {ClientEntity} from 'src/script/client/ClientEntity'; import {ConnectionMapper} from 'src/script/connection/ConnectionMapper'; import {ConversationMapper} from 'src/script/conversation/ConversationMapper'; import {NOTIFICATION_STATE} from 'src/script/conversation/NotificationSetting'; import 'src/script/localization/Localizer'; -import {CALL_MESSAGE_TYPE} from 'src/script/message/CallMessageType'; -import {MentionEntity} from 'src/script/message/MentionEntity'; import {StatusType} from 'src/script/message/StatusType'; import {createUuid} from 'Util/uuid'; import {Conversation} from './Conversation'; -import {CallMessage} from './message/CallMessage'; import {ContentMessage} from './message/ContentMessage'; -import {MemberMessage} from './message/MemberMessage'; import {Message} from './message/Message'; import {PingMessage} from './message/PingMessage'; import {Text} from './message/Text'; @@ -897,158 +892,6 @@ describe('Conversation', () => { }); }); - describe('shouldUnarchive', () => { - let timestamp: number = undefined; - let contentMessage: ContentMessage = undefined; - let mutedTimestampMessage: PingMessage = undefined; - let outdatedMessage: PingMessage = undefined; - let pingMessage: PingMessage = undefined; - let selfMentionMessage: ContentMessage = undefined; - const conversationEntity = new Conversation(createUuid()); - - const selfUserEntity = new User(createUuid(), null); - selfUserEntity.isMe = true; - selfUserEntity.inTeam(true); - conversationEntity.selfUser(selfUserEntity); - - beforeEach(() => { - timestamp = Date.now(); - conversationEntity.archivedTimestamp(timestamp); - conversationEntity.archivedState(true); - - mutedTimestampMessage = new PingMessage(); - mutedTimestampMessage.timestamp(timestamp); - - outdatedMessage = new PingMessage(); - outdatedMessage.timestamp(timestamp - 100); - - contentMessage = new ContentMessage(); - contentMessage.assets([new Text('id', 'Hello there')]); - contentMessage.timestamp(timestamp + 100); - - pingMessage = new PingMessage(); - pingMessage.timestamp(timestamp + 200); - - selfMentionMessage = new ContentMessage(); - const mentionEntity = new MentionEntity(0, 7, selfUserEntity.id, selfUserEntity.domain); - const textAsset = new Text('id', '@Gregor, Hello there'); - textAsset.mentions.push(mentionEntity); - selfMentionMessage.assets([textAsset]); - selfMentionMessage.timestamp(timestamp + 300); - }); - - afterEach(() => conversationEntity.messages_unordered.removeAll()); - - it('returns false if conversation is not archived', () => { - conversationEntity.archivedState(false); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(outdatedMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(mutedTimestampMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(contentMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(pingMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(selfMentionMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - }); - - it('returns false if conversation is in no notification state', () => { - conversationEntity.mutedState(NOTIFICATION_STATE.NOTHING); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(outdatedMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(mutedTimestampMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(contentMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(pingMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(selfMentionMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - }); - - it('returns expected value if conversation is in only mentions notifications state', () => { - conversationEntity.mutedState(NOTIFICATION_STATE.MENTIONS_AND_REPLIES); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(outdatedMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(mutedTimestampMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(contentMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(pingMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(selfMentionMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(true); - }); - - it('returns expected value if conversation is in everything notifications state', () => { - conversationEntity.mutedState(NOTIFICATION_STATE.EVERYTHING); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(outdatedMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(mutedTimestampMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - conversationEntity.messages_unordered.push(contentMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(true); - conversationEntity.messages_unordered.removeAll(); - - const memberLeaveMessage = new MemberMessage(); - memberLeaveMessage.type = CONVERSATION_EVENT.MEMBER_LEAVE; - memberLeaveMessage.timestamp(timestamp + 100); - conversationEntity.messages_unordered.push(memberLeaveMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - - const callMessage = new CallMessage(CALL_MESSAGE_TYPE.ACTIVATED); - callMessage.timestamp(timestamp + 200); - conversationEntity.messages_unordered.push(callMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(true); - conversationEntity.messages_unordered.removeAll(); - conversationEntity.messages_unordered.push(memberLeaveMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - const memberJoinMessage = new MemberMessage(); - memberJoinMessage.type = CONVERSATION_EVENT.MEMBER_JOIN; - memberJoinMessage.timestamp(timestamp + 200); - conversationEntity.messages_unordered.push(memberJoinMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(false); - const selfJoinMessage = new MemberMessage(); - selfJoinMessage.type = CONVERSATION_EVENT.MEMBER_JOIN; - selfJoinMessage.userIds.push({domain: selfUserEntity.domain, id: selfUserEntity.id}); - selfJoinMessage.timestamp(timestamp + 200); - conversationEntity.messages_unordered.push(selfJoinMessage); - - expect(conversationEntity.shouldUnarchive()).toBe(true); - }); - }); - describe('_incrementTimeOnly', () => { it('should update only to newer timestamps', () => { //@ts-ignore diff --git a/src/script/entity/Conversation.ts b/src/script/entity/Conversation.ts index 75361c8f4ab..849dd2f2765 100644 --- a/src/script/entity/Conversation.ts +++ b/src/script/entity/Conversation.ts @@ -42,7 +42,6 @@ import {truncate} from 'Util/StringUtil'; import {CallMessage} from './message/CallMessage'; import type {ContentMessage} from './message/ContentMessage'; -import type {MemberMessage} from './message/MemberMessage'; import type {Message} from './message/Message'; import {PingMessage} from './message/PingMessage'; import type {User} from './User'; @@ -831,38 +830,6 @@ export class Conversation { this.messages_unordered.removeAll(); } - shouldUnarchive(): boolean { - if (!this.archivedState() || this.showNotificationsNothing()) { - return false; - } - - const isNewerMessage = (messageEntity: Message) => messageEntity.timestamp() > this.archivedTimestamp(); - - const {allEvents, allMessages, selfMentions, selfReplies} = this.unreadState(); - if (this.showNotificationsMentionsAndReplies()) { - const mentionsAndReplies = selfMentions.concat(selfReplies); - return mentionsAndReplies.some(isNewerMessage); - } - - const hasNewMessage = allMessages.some(isNewerMessage); - if (hasNewMessage) { - return true; - } - - return allEvents.some(messageEntity => { - if (!isNewerMessage(messageEntity)) { - return false; - } - - const isCallActivation = messageEntity.isCall() && messageEntity.isActivation(); - const isMemberJoin = messageEntity.isMember() && (messageEntity as MemberMessage).isMemberJoin(); - const wasSelfUserAdded = - isMemberJoin && (messageEntity as MemberMessage).isUserAffected(this.selfUser().qualifiedId); - - return isCallActivation || wasSelfUserAdded; - }); - } - /** * Checks for message duplicates. * diff --git a/src/script/event/EventRepository.test.ts b/src/script/event/EventRepository.test.ts index 49d8d4aea8e..c352f3dea77 100644 --- a/src/script/event/EventRepository.test.ts +++ b/src/script/event/EventRepository.test.ts @@ -19,23 +19,14 @@ import {BackendEvent, CONVERSATION_EVENT, USER_EVENT} from '@wireapp/api-client/lib/event/'; -import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging'; - -import {AssetTransferState} from 'src/script/assets/AssetTransferState'; -import {EventError} from 'src/script/error/EventError'; import {ClientEvent} from 'src/script/event/Client'; import {EventRepository} from 'src/script/event/EventRepository'; import {NOTIFICATION_HANDLING_STATE} from 'src/script/event/NotificationHandlingState'; -import {createUuid} from 'Util/uuid'; -import {EventService} from './EventService'; import {EventSource} from './EventSource'; import {TestFactory} from '../../../test/helper/TestFactory'; import {ClientConversationEvent} from '../conversation/EventBuilder'; -import {User} from '../entity/User'; -import {StatusType} from '../message/StatusType'; -import {EventRecord} from '../storage'; const testFactory = new TestFactory(); @@ -46,64 +37,9 @@ describe('EventRepository', () => { return testFactory.exposeEventActors(); }); - describe('getCommonMessageUpdates', () => { - /** @see https://wearezeta.atlassian.net/browse/SQCORE-732 */ - it('does not overwrite the seen status if a message gets edited', () => { - const originalEvent = { - category: 16, - conversation: 'a7f1187e-9396-44c9-8242-db9d3051dc89', - data: { - content: 'Original Text Which Has Been Seen By Someone Else', - expects_read_confirmation: true, - legal_hold_status: 1, - mentions: [], - previews: [], - }, - from: '24de8432-03ba-439f-88f8-95bdc68b7bdd', - from_client_id: '79618bbe93e6821c', - id: 'c6269e58-fa82-4f6e-8264-263e09154871', - primary_key: '17', - read_receipts: [ - { - time: '2021-06-10T19:47:19.570Z', - userId: 'b661e27f-24c6-4c52-a425-87a7b7f3df61', - }, - ], - status: StatusType.SEEN, - time: '2021-06-10T19:47:16.071Z', - type: ClientEvent.CONVERSATION.MESSAGE_ADD, - } as any; - - const editedEvent = { - conversation: 'a7f1187e-9396-44c9-8242-db9d3051dc89', - data: { - content: 'Edited Text Which Replaces The Original Text', - expects_read_confirmation: true, - mentions: [], - previews: [], - replacing_message_id: 'c6269e58-fa82-4f6e-8264-263e09154871', - }, - from: '24de8432-03ba-439f-88f8-95bdc68b7bdd', - from_client_id: '79618bbe93e6821c', - id: 'caff044b-cb9c-47c6-833a-d4b76c678bcd', - status: StatusType.SENT, - time: '2021-06-10T19:47:23.706Z', - type: ClientEvent.CONVERSATION.MESSAGE_ADD, - } as any; - - const updatedEvent = EventRepository['getCommonMessageUpdates'](originalEvent, editedEvent); - expect(updatedEvent.data.content).toBe('Edited Text Which Replaces The Original Text'); - expect(updatedEvent.status).toBe(StatusType.SEEN); - expect(Object.keys(updatedEvent.read_receipts).length).toBe(1); - }); - }); - describe('handleEvent', () => { beforeEach(() => { testFactory.event_repository!.notificationHandlingState(NOTIFICATION_HANDLING_STATE.WEB_SOCKET); - jest - .spyOn(testFactory.event_service!, 'saveEvent') - .mockReturnValue(Promise.resolve({data: 'dummy content'} as EventRecord)); spyOn(testFactory.event_repository!, 'distributeEvent'); }); @@ -114,7 +50,6 @@ describe('EventRepository', () => { EventSource.NOTIFICATION_STREAM, ) .then(() => { - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); @@ -126,7 +61,6 @@ describe('EventRepository', () => { EventSource.NOTIFICATION_STREAM, ) .then(() => { - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); @@ -138,7 +72,6 @@ describe('EventRepository', () => { EventSource.NOTIFICATION_STREAM, ) .then(() => { - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); @@ -154,7 +87,6 @@ describe('EventRepository', () => { } as BackendEvent; return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => { - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); @@ -170,7 +102,6 @@ describe('EventRepository', () => { } as BackendEvent; return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => { - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); @@ -186,18 +117,13 @@ describe('EventRepository', () => { } as BackendEvent; return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => { - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); it('accepts "conversation.voice-channel-deactivate" (missed call) events', async () => { - const eventServiceSpy = { - loadEvent: jest.fn().mockResolvedValue(undefined), - saveEvent: jest.fn().mockResolvedValue({data: 'dummy content'}), - } as unknown as EventService; const fakeProp: any = undefined; - const eventRepo = new EventRepository(eventServiceSpy, fakeProp, fakeProp, fakeProp); + const eventRepo = new EventRepository({} as any, fakeProp, fakeProp, fakeProp); eventRepo.notificationHandlingState(NOTIFICATION_HANDLING_STATE.WEB_SOCKET); jest.spyOn(eventRepo, 'distributeEvent').mockImplementation(() => {}); @@ -212,7 +138,6 @@ describe('EventRepository', () => { await eventRepo['handleEvent']({event}, EventSource.NOTIFICATION_STREAM); - expect(eventServiceSpy.saveEvent).toHaveBeenCalled(); expect(eventRepo['distributeEvent']).toHaveBeenCalled(); }); @@ -228,383 +153,8 @@ describe('EventRepository', () => { } as ClientConversationEvent; return testFactory.event_repository.injectEvent(event).then(() => { - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled(); }); }); }); - - describe('processEvent', () => { - let event: any; - let previously_stored_event: any; - - beforeEach(() => { - event = { - conversation: createUuid(), - data: { - content: 'Lorem Ipsum', - previews: [], - }, - from: createUuid(), - id: createUuid(), - time: new Date().toISOString(), - type: ClientEvent.CONVERSATION.MESSAGE_ADD, - }; - - jest - .spyOn(testFactory.event_service!, 'saveEvent') - .mockImplementation(saved_event => Promise.resolve(saved_event as any)); - }); - - it('saves an event with a previously not used ID', () => { - jest.spyOn(testFactory.event_service!, 'loadEvent').mockClear(); - - return testFactory.event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM).then(() => { - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); - }); - }); - - it('ignores an event with an ID previously used by another user', () => { - previously_stored_event = JSON.parse(JSON.stringify(event)); - previously_stored_event.from = createUuid(); - jest - .spyOn(testFactory.event_service!, 'loadEvent') - .mockImplementation(() => Promise.resolve(previously_stored_event)); - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('ignores a non-"text message" with an ID previously used by the same user', () => { - event.type = ClientEvent.CALL.E_CALL; - previously_stored_event = JSON.parse(JSON.stringify(event)); - jest - .spyOn(testFactory.event_service!, 'loadEvent') - .mockImplementation(() => Promise.resolve(previously_stored_event)); - - return testFactory - .event_repository!['handleEventSaving'](event) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('ignores a plain text message with an ID previously used by the same user for a non-"text message"', () => { - previously_stored_event = JSON.parse(JSON.stringify(event)); - previously_stored_event.type = ClientEvent.CALL.E_CALL; - jest - .spyOn(testFactory.event_service!, 'loadEvent') - .mockImplementation(() => Promise.resolve(previously_stored_event)); - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('ignores a plain text message with an ID previously used by the same user', () => { - previously_stored_event = JSON.parse(JSON.stringify(event)); - jest - .spyOn(testFactory.event_service!, 'loadEvent') - .mockImplementation(() => Promise.resolve(previously_stored_event)); - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('ignores a text message with link preview with an ID previously used by the same user for a text message with link preview', () => { - event.data.previews.push(1); - previously_stored_event = JSON.parse(JSON.stringify(event)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event as any); - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('ignores a text message with link preview with an ID previously used by the same user for a text message different content', () => { - previously_stored_event = JSON.parse(JSON.stringify(event)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event as any); - - event.data.previews.push(1); - event.data.content = 'Ipsum loren'; - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then(() => fail('Method should have thrown an error')) - .catch(error => { - expect(error).toEqual(jasmine.any(EventError)); - expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('saves a text message with link preview with an ID previously used by the same user for a plain text message', () => { - previously_stored_event = JSON.parse(JSON.stringify(event)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event); - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation(() => Promise.resolve(previously_stored_event)); - - const initial_time = event.time; - const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString(); - event.data.previews.push(1); - event.time = changed_time; - - return testFactory - .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM) - .then((saved_event: any) => { - expect(saved_event.time).toEqual(initial_time); - expect(saved_event.time).not.toEqual(changed_time); - expect(saved_event.primary_key).toEqual(previously_stored_event.primary_key); - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - }); - }); - - it('ignores edit message with missing associated original message', () => { - const linkPreviewEvent = JSON.parse(JSON.stringify(event)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue({} as any); - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation(() => Promise.resolve({} as EventRecord)); - - linkPreviewEvent.data.replacing_message_id = 'initial_message_id'; - - return testFactory - .event_repository!['handleEventSaving'](linkPreviewEvent) - .then(() => fail('Should have thrown an error')) - .catch(() => { - expect(testFactory.event_service!.replaceEvent).not.toHaveBeenCalled(); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - }); - }); - - it('updates edited messages when link preview arrives', () => { - const replacingId = 'old-replaced-message-id'; - const storedEvent = { - ...event, - data: {...event.data, replacing_message_id: replacingId}, - } as EventRecord; - const linkPreviewEvent = {...event}; - jest - .spyOn(testFactory.event_service!, 'loadEvent') - .mockImplementation((conversationId: string, messageId: string) => { - return messageId === replacingId ? Promise.resolve(undefined) : Promise.resolve(storedEvent as any); - }); - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation((ev: EventRecord) => Promise.resolve(ev)); - - linkPreviewEvent.data.replacing_message_id = replacingId; - linkPreviewEvent.data.previews = ['preview']; - - return testFactory.event_repository!['handleEventSaving'](linkPreviewEvent).then((updatedEvent: any) => { - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - expect(updatedEvent?.data.previews[0]).toEqual('preview'); - }); - }); - - it('updates edited messages', () => { - const originalMessage = JSON.parse(JSON.stringify(event)); - originalMessage.reactions = ['user-id']; - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(originalMessage as any); - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation((updates: EventRecord) => Promise.resolve(updates)); - - const initial_time = event.time; - const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString(); - originalMessage.primary_key = 12; - event.id = createUuid(); - event.data.content = 'new content'; - event.data.replacing_message_id = originalMessage.id; - event.time = changed_time; - - return testFactory.event_repository!['handleEventSaving'](event).then((updatedEvent: any) => { - expect(updatedEvent.time).toEqual(initial_time); - expect(updatedEvent.time).not.toEqual(changed_time); - expect(updatedEvent.data.content).toEqual('new content'); - expect(updatedEvent.primary_key).toEqual(originalMessage.primary_key); - expect(Object.keys(updatedEvent.reactions).length).toEqual(0); - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - }); - }); - - it('updates link preview when edited', () => { - const replacingId = 'replaced-message-id'; - const storedEvent = { - ...event, - data: {...event.data, previews: ['preview']}, - } as any; - const editEvent = {...event} as any; - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(storedEvent as any); - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation((ev: EventRecord) => Promise.resolve(ev)); - - editEvent.data.replacing_message_id = replacingId; - - return testFactory.event_repository!['handleEventSaving'](editEvent).then((updatedEvent: any) => { - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled(); - expect(updatedEvent?.data.previews.length).toEqual(0); - }); - }); - - it('saves a conversation.asset-add event', () => { - const assetAddEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD}; - - jest.spyOn(testFactory.event_service!, 'loadEvent').mockClear(); - - return testFactory - .event_repository!['processEvent'](assetAddEvent, EventSource.NOTIFICATION_STREAM) - .then(updatedEvent => { - expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(testFactory.event_service!.saveEvent).toHaveBeenCalled(); - }); - }); - - it('deletes cancelled conversation.asset-add event', async () => { - const fromIds = [ - // cancel from an other user - createUuid(), - // cancel from the self user - testFactory.user_repository['userState'].self().id, - ]; - - const loadEventSpy = jest.spyOn(testFactory.event_service!, 'loadEvent'); - const deleteEventSpy = jest.spyOn(testFactory.event_service!, 'deleteEvent'); - for (const fromId of fromIds) { - const assetAddEvent = {...event, from: fromId, type: ClientEvent.CONVERSATION.ASSET_ADD}; - const assetCancelEvent = { - ...assetAddEvent, - data: {reason: ProtobufAsset.NotUploaded.CANCELLED, status: AssetTransferState.UPLOAD_FAILED}, - time: '2017-09-06T09:43:36.528Z', - }; - - loadEventSpy.mockResolvedValue(assetAddEvent as any); - deleteEventSpy.mockImplementation(() => Promise.resolve(1)); - - const savedEvent = await testFactory.event_repository!['processEvent']( - assetCancelEvent, - EventSource.NOTIFICATION_STREAM, - ); - expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(testFactory.event_service!.deleteEvent).toHaveBeenCalled(); - } - }); - - it('deletes other user failed upload for conversation.asset-add event', () => { - const assetAddEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD}; - const assetUploadFailedEvent = { - ...assetAddEvent, - data: {reason: ProtobufAsset.NotUploaded.FAILED, status: AssetTransferState.UPLOAD_FAILED}, - time: '2017-09-06T09:43:36.528Z', - }; - - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(assetAddEvent as any); - jest.spyOn(testFactory.event_service!, 'deleteEvent').mockImplementation(() => Promise.resolve(1)); - - return testFactory - .event_repository!['processEvent'](assetUploadFailedEvent, EventSource.NOTIFICATION_STREAM) - .then(savedEvent => { - expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(testFactory.event_service!.deleteEvent).toHaveBeenCalled(); - }); - }); - - it('updates self failed upload for conversation.asset-add event', async () => { - const assetAddEvent: EventRecord = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD}; - const assetUploadFailedEvent = { - ...assetAddEvent, - data: {reason: ProtobufAsset.NotUploaded.FAILED, status: AssetTransferState.UPLOAD_FAILED}, - time: '2017-09-06T09:43:36.528Z', - } as any; - - jest - .spyOn(testFactory.event_repository!['userState'], 'self') - .mockReturnValue(new User(assetAddEvent.from) as any); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(assetAddEvent as any); - jest - .spyOn(testFactory.event_service!, 'updateEventAsUploadFailed') - .mockImplementation(() => Promise.resolve(assetUploadFailedEvent)); - - const savedEvent = await testFactory.event_repository!['processEvent']( - assetUploadFailedEvent, - EventSource.NOTIFICATION_STREAM, - ); - expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(testFactory.event_service!.updateEventAsUploadFailed).toHaveBeenCalled(); - }); - - it('handles conversation.asset-add state update event', () => { - const initialAssetEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD}; - - const updateStatusEvent = { - ...initialAssetEvent, - data: {status: AssetTransferState.UPLOADED}, - time: '2017-09-06T09:43:36.528Z', - }; - - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation(eventToUpdate => Promise.resolve(eventToUpdate)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(initialAssetEvent as any); - - return testFactory - .event_repository!['processEvent'](updateStatusEvent, EventSource.NOTIFICATION_STREAM) - .then((updatedEvent: any) => { - expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(updatedEvent.data.status).toEqual(updateStatusEvent.data.status); - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - }); - }); - - it('updates video when preview is received', () => { - const initialAssetEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD}; - - const AssetPreviewEvent = { - ...initialAssetEvent, - data: {status: AssetTransferState.UPLOADED}, - time: '2017-09-06T09:43:36.528Z', - }; - - jest - .spyOn(testFactory.event_service!, 'replaceEvent') - .mockImplementation(eventToUpdate => Promise.resolve(eventToUpdate)); - jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(initialAssetEvent as any); - - return testFactory - .event_repository!['processEvent'](AssetPreviewEvent, EventSource.NOTIFICATION_STREAM) - .then((updatedEvent: EventRecord) => { - expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); - expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled(); - }); - }); - }); }); diff --git a/src/script/event/EventRepository.ts b/src/script/event/EventRepository.ts index 9e348ddf844..175c18e804b 100644 --- a/src/script/event/EventRepository.ts +++ b/src/script/event/EventRepository.ts @@ -29,32 +29,26 @@ import ko from 'knockout'; import {container} from 'tsyringe'; import {Account, ConnectionState, ProcessedEventPayload} from '@wireapp/core'; -import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging'; import {WebAppEvents} from '@wireapp/webapp-events'; import {getLogger, Logger} from 'Util/Logger'; import {queue} from 'Util/PromiseQueue'; import {TIME_IN_MILLIS} from 'Util/TimeUtil'; -import {ClientEvent, CONVERSATION} from './Client'; +import {ClientEvent} from './Client'; import {EventMiddleware, EventProcessor, IncomingEvent} from './EventProcessor'; import type {EventService} from './EventService'; import {EventSource} from './EventSource'; import {EVENT_TYPE} from './EventType'; -import {EventTypeHandling} from './EventTypeHandling'; import {EventValidation} from './EventValidation'; import {validateEvent} from './EventValidator'; import {NOTIFICATION_HANDLING_STATE} from './NotificationHandlingState'; import type {NotificationService} from './NotificationService'; -import {AssetTransferState} from '../assets/AssetTransferState'; -import {AssetAddEvent, ClientConversationEvent, EventBuilder, MessageAddEvent} from '../conversation/EventBuilder'; +import {ClientConversationEvent, EventBuilder} from '../conversation/EventBuilder'; import {CryptographyMapper} from '../cryptography/CryptographyMapper'; import {CryptographyError} from '../error/CryptographyError'; import {EventError} from '../error/EventError'; -import {categoryFromEvent} from '../message/MessageCategorization'; -import {isEventRecordFailed, isEventRecordWithFederationError} from '../message/StatusType'; -import type {EventRecord, StoredEvent} from '../storage'; import type {ServerTimeHandler} from '../time/serverTimeHandler'; import {EventName} from '../tracking/EventName'; import {UserState} from '../user/UserState'; @@ -62,14 +56,11 @@ import {Warnings} from '../view_model/WarningsContainer'; export class EventRepository { logger: Logger; - notificationHandlingState: ko.Observable; - previousHandlingState: NOTIFICATION_HANDLING_STATE | undefined; - notificationsHandled: number; - notificationsTotal: number; - lastEventDate: ko.Observable; - eventProcessMiddlewares: EventMiddleware[] = []; + notificationHandlingState = ko.observable(NOTIFICATION_HANDLING_STATE.STREAM); + private readonly lastEventDate: ko.Observable = ko.observable(); + private eventProcessMiddlewares: EventMiddleware[] = []; /** event processors are classes that are able to react and process an incoming event */ - eventProcessors: EventProcessor[] = []; + private eventProcessors: EventProcessor[] = []; static get CONFIG() { return { @@ -108,14 +99,9 @@ export class EventRepository { ) { this.logger = getLogger('EventRepository'); - this.notificationHandlingState = ko.observable(NOTIFICATION_HANDLING_STATE.STREAM); this.notificationHandlingState.subscribe(handling_state => { amplify.publish(WebAppEvents.EVENT.NOTIFICATION_HANDLING_STATE, handling_state); }); - this.notificationsHandled = 0; - this.notificationsTotal = 0; - - this.lastEventDate = ko.observable(); } /** @@ -430,12 +416,6 @@ export class EventRepository { event = await eventProcessMiddleware.processEvent(event); } - const shouldSaveEvent = EventTypeHandling.STORE.includes(event.type as CONVERSATION_EVENT); - if (shouldSaveEvent) { - const savedEvent = await this.handleEventSaving(event); - event = savedEvent ?? event; - } - return this.handleEventDistribution(event, source); } @@ -463,211 +443,4 @@ export class EventRepository { } return this.distributeEvent(event, source); } - - /** - * Handle a mapped event, check for malicious ID use and save it. - * - * @param event Backend event extracted from notification stream - * @returns Resolves with the saved event - */ - private async handleEventSaving(event: IncomingEvent) { - const conversationId = 'conversation' in event && event.conversation; - - // first check if a message that should be replaced exists in DB - if (event.type === ClientEvent.CONVERSATION.MESSAGE_ADD) { - const mappedData = event.data; - const eventToReplace = mappedData.replacing_message_id - ? await this.eventService.loadEvent(conversationId, mappedData.replacing_message_id) - : undefined; - - const hasLinkPreview = mappedData.previews && mappedData.previews.length; - const isReplacementWithoutOriginal = !eventToReplace && mappedData.replacing_message_id; - if (isReplacementWithoutOriginal && !hasLinkPreview) { - // the only valid case of a replacement with no original message is when an edited message gets a link preview - this.throwValidationError(event, 'Edit event without original event'); - } - - if (eventToReplace?.type === CONVERSATION.MESSAGE_ADD) { - return this.handleEventReplacement(eventToReplace, event); - } - } - - // check for duplicates (same id) - const storedEvent = 'id' in event ? await this.eventService.loadEvent(conversationId, event.id) : undefined; - - return storedEvent - ? this.handleDuplicatedEvent(storedEvent, event) - : this.eventService.saveEvent(event as EventRecord); - } - - private handleEventReplacement(originalEvent: StoredEvent, newEvent: MessageAddEvent) { - if (originalEvent.from !== newEvent.from) { - const logMessage = `ID previously used by user '${newEvent.from}'`; - const errorMessage = 'ID reused by other user'; - this.throwValidationError(newEvent, errorMessage, logMessage); - } - const newData = newEvent.data; - const primaryKeyUpdate = {primary_key: originalEvent.primary_key}; - const isLinkPreviewEdit = newData?.previews && !!newData?.previews.length; - - const commonUpdates = EventRepository.getCommonMessageUpdates(originalEvent, newEvent); - - const specificUpdates = isLinkPreviewEdit - ? this.getUpdatesForMessage(originalEvent, newEvent) - : EventRepository.getUpdatesForEditMessage(originalEvent, newEvent); - - const updates = {...specificUpdates, ...commonUpdates}; - - const identifiedUpdates = {...primaryKeyUpdate, ...updates}; - return this.eventService.replaceEvent(identifiedUpdates); - } - - private handleDuplicatedEvent(originalEvent: EventRecord, newEvent: IncomingEvent) { - switch (newEvent.type) { - case ClientEvent.CONVERSATION.ASSET_ADD: - return this.handleAssetUpdate(originalEvent, newEvent); - - case ClientEvent.CONVERSATION.MESSAGE_ADD: - return this.handleMessageUpdate(originalEvent, newEvent); - - default: - this.throwValidationError(newEvent, `Forbidden type '${newEvent.type}' for duplicate events`); - } - } - - private async handleAssetUpdate(originalEvent: EventRecord, newEvent: AssetAddEvent) { - if (originalEvent.type !== ClientEvent.CONVERSATION.ASSET_ADD) { - this.throwValidationError(newEvent, 'Trying to update a non-asset message as an asset message'); - } - const newEventData = newEvent.data; - // the preview status is not sent by the client so we fake a 'preview' status in order to cleanly handle it in the switch statement - const ASSET_PREVIEW = 'preview'; - // similarly, no status is sent by the client when we retry sending a failed message - const RETRY_EVENT = 'retry'; - const isPreviewEvent = !newEventData.status && !!newEventData.preview_key; - const isRetryEvent = !!newEventData.content_length; - const handledEvent = isRetryEvent ? RETRY_EVENT : newEventData.status; - const previewStatus = isPreviewEvent ? ASSET_PREVIEW : handledEvent; - - const updateEvent = () => { - const updatedData = {...originalEvent.data, ...newEventData}; - const updatedEvent = {...originalEvent, data: updatedData}; - return this.eventService.replaceEvent(updatedEvent); - }; - - switch (previewStatus) { - case ASSET_PREVIEW: - case RETRY_EVENT: - case AssetTransferState.UPLOADED: { - return updateEvent(); - } - - case AssetTransferState.UPLOAD_FAILED: { - // case of both failed or canceled upload - const fromOther = newEvent.from !== this.userState.self().id; - const sameSender = newEvent.from === originalEvent.from; - const selfCancel = !fromOther && newEvent.data.reason === ProtobufAsset.NotUploaded.CANCELLED; - // we want to delete the event in the case of an error from the remote client or a cancel on the user's own client - const shouldDeleteEvent = (fromOther || selfCancel) && sameSender; - if (shouldDeleteEvent) { - await this.eventService.deleteEvent(newEvent.conversation, newEvent.id); - return newEvent; - } - return this.eventService.updateEventAsUploadFailed(originalEvent.primary_key, newEvent.data.reason); - } - - default: { - this.throwValidationError(newEvent, `Unhandled asset status update '${newEvent.data.status}'`); - } - } - } - - private handleMessageUpdate(originalEvent: EventRecord, newEvent: MessageAddEvent) { - const newEventData = newEvent.data; - const originalData = originalEvent.data; - - if (originalEvent.from !== newEvent.from) { - const logMessage = `ID previously used by user '${newEvent.from}'`; - const errorMessage = 'ID reused by other user'; - return this.throwValidationError(newEvent, errorMessage, logMessage); - } - - const containsLinkPreview = newEventData.previews && !!newEventData.previews.length; - const isRetryAttempt = isEventRecordFailed(originalEvent) || isEventRecordWithFederationError(originalEvent); - - if (!containsLinkPreview && !isRetryAttempt) { - const errorMessage = - 'Message duplication event invalid: original message did not fail to send and does not contain link preview'; - return this.throwValidationError(newEvent, errorMessage); - } - - const textContentMatches = newEventData.content === (originalData as any).content; - if (!textContentMatches) { - const errorMessage = 'ID of link preview reused'; - const logMessage = 'Text content for message duplication not matching'; - return this.throwValidationError(newEvent, errorMessage, logMessage); - } - - const bothAreMessageAddType = newEvent.type === originalEvent.type; - if (!bothAreMessageAddType) { - return this.throwValidationError(newEvent, 'ID reused by same user'); - } - - const updates = this.getUpdatesForMessage(originalEvent, newEvent); - const identifiedUpdates = {primary_key: originalEvent.primary_key, ...updates}; - return this.eventService.replaceEvent(identifiedUpdates); - } - - private static getCommonMessageUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) { - return { - ...newEvent, - data: {...newEvent.data, expects_read_confirmation: originalEvent.data.expects_read_confirmation}, - edited_time: newEvent.time, - read_receipts: !newEvent.read_receipts ? originalEvent.read_receipts : newEvent.read_receipts, - status: !newEvent.status || newEvent.status < originalEvent.status ? originalEvent.status : newEvent.status, - time: originalEvent.time, - version: 1, - }; - } - - private static getUpdatesForEditMessage(originalEvent: EventRecord, newEvent: MessageAddEvent): MessageAddEvent { - // Remove reactions, so that likes (hearts) don't stay when a message's text gets edited - return {...newEvent, reactions: {}}; - } - - private getUpdatesForMessage(originalEvent: EventRecord, newEvent: MessageAddEvent) { - const newData = newEvent.data; - const originalData = originalEvent.data; - const updatingLinkPreview = !!(originalData as any).previews.length; - if (updatingLinkPreview) { - this.throwValidationError(newEvent, 'ID of link preview reused'); - } - - const textContentMatches = !newData.previews?.length || newData.content === (originalData as any).content; - if (!textContentMatches) { - const logMessage = 'Text content for message duplication not matching'; - const errorMessage = 'ID of duplicated message reused'; - this.throwValidationError(newEvent, errorMessage, logMessage); - } - - return { - ...newEvent, - category: categoryFromEvent(newEvent), - ephemeral_expires: originalEvent.ephemeral_expires, - ephemeral_started: originalEvent.ephemeral_started, - ephemeral_time: originalEvent.ephemeral_time, - server_time: newEvent.time, - time: originalEvent.time, - version: originalEvent.version, - }; - } - - private throwValidationError(event: IncomingEvent, errorMessage: string, logMessage?: string): never { - const conversation = 'conversation' in event && event.conversation; - const from = 'from' in event && event.from; - - const baseLogMessage = `Ignored '${event.type}' in '${conversation}' from '${from}''`; - this.logger.warn(`${baseLogMessage} ${logMessage || errorMessage}`); - throw new EventError(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${errorMessage}`); - } } diff --git a/src/script/event/EventService.ts b/src/script/event/EventService.ts index 2e4b124669e..43fa6e0a4bb 100644 --- a/src/script/event/EventService.ts +++ b/src/script/event/EventService.ts @@ -39,7 +39,7 @@ import {StorageSchemata} from '../storage/StorageSchemata'; export type Includes = {includeFrom: boolean; includeTo: boolean}; type DexieCollection = Dexie.Collection; export type DBEvents = DexieCollection | EventRecord[]; -type IdentifiedUpdatePayload = Partial & Pick; +export type IdentifiedUpdatePayload = Partial & Pick; export const eventTimeToDate = (time: string) => new Date(time) || new Date(parseInt(time, 10)); diff --git a/src/script/event/EventTypeHandling.ts b/src/script/event/EventTypeHandling.ts index 70c4b8d378a..89c8ec374c2 100644 --- a/src/script/event/EventTypeHandling.ts +++ b/src/script/event/EventTypeHandling.ts @@ -17,10 +17,16 @@ * */ -import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/'; +import {CONVERSATION_EVENT, ConversationEvent} from '@wireapp/api-client/lib/event'; import {ClientEvent} from './Client'; +import {ClientConversationEvent} from '../conversation/EventBuilder'; + +export function eventShouldBeStored(event: {type: any}): event is ClientConversationEvent | ConversationEvent { + return EventTypeHandling.STORE.includes(event.type); +} + export const EventTypeHandling = { CONFIRM: [ ClientEvent.CONVERSATION.ASSET_ADD, diff --git a/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts new file mode 100644 index 00000000000..60ffc4fb123 --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts @@ -0,0 +1,353 @@ +/* + * 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 {Asset as ProtobufAsset} from '@wireapp/protocol-messaging'; + +import {AssetTransferState} from 'src/script/assets/AssetTransferState'; +import {User} from 'src/script/entity/User'; +import {EventError} from 'src/script/error/EventError'; +import {createAssetAddEvent, createMessageAddEvent, toSavedEvent} from 'test/helper/EventGenerator'; +import {createUuid} from 'Util/uuid'; + +import {EventStorageMiddleware} from './EventStorageMiddleware'; + +import {ClientEvent} from '../../Client'; +import {EventService} from '../../EventService'; + +function buildEventStorageMiddleware() { + const eventService = { + saveEvent: jest.fn(event => event), + loadEvent: jest.fn(), + replaceEvent: jest.fn(event => event), + deleteEvent: jest.fn(), + } as unknown as jest.Mocked; + + const selfUser = new User(createUuid()); + return [new EventStorageMiddleware(eventService, selfUser), {eventService, selfUser}] as const; +} + +describe('EventStorageMiddleware', () => { + describe('processEvent', () => { + it('ignores unhandled event', async () => { + const event = {type: 'other'} as any; + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + + await eventStorageMiddleware.processEvent(event); + expect(eventService.saveEvent).not.toHaveBeenCalledWith(event); + }); + + it('saves an event with a new ID', async () => { + const event = createMessageAddEvent(); + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + + await eventStorageMiddleware.processEvent(event); + expect(eventService.saveEvent).toHaveBeenCalledWith(event); + }); + + it('fails for an event with an ID previously used by another user', async () => { + const event = createMessageAddEvent(); + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const eventWithSameId = {...event, from: createUuid()}; + eventService.loadEvent.mockResolvedValue({primary_key: '', category: 1, ...eventWithSameId}); + + await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual( + new EventError( + EventError.TYPE.VALIDATION_FAILED, + 'Event validation failed: ID previously used by another user', + ), + ); + }); + + it('fails for a non-"text message" with an ID previously used by the same user', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + eventService.loadEvent.mockResolvedValue(toSavedEvent({...event, type: ClientEvent.CALL.E_CALL} as any)); + + await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual( + new EventError( + EventError.TYPE.VALIDATION_FAILED, + 'Event validation failed: ID already used for a different type of message', + ), + ); + }); + + it('fails for a plain text message with an ID previously used by the same user', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + eventService.loadEvent.mockResolvedValue(toSavedEvent(event)); + + await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual( + new EventError( + EventError.TYPE.VALIDATION_FAILED, + 'Event validation failed: ID already used for a successfully sent message', + ), + ); + }); + + it('fails for a text message with link preview with an ID previously used by the same user for a text message with link preview', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + const storedEvent = {...event, data: {...event.data, previews: ['1']}}; + eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent)); + + await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual( + new EventError( + EventError.TYPE.VALIDATION_FAILED, + 'Event validation failed: ID already used for a successfully sent message', + ), + ); + }); + + it('ignores a text message with link preview with an ID previously used by the same user for a text message different content', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + const storedEvent = {...event, data: {...event.data, previews: [] as any[]}}; + eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent)); + + const newEvent = {...event, data: {...event.data, content: 'different content', previews: ['1']}}; + + await expect(eventStorageMiddleware.processEvent(newEvent)).rejects.toEqual( + new EventError( + EventError.TYPE.VALIDATION_FAILED, + 'Event validation failed: Link preview with different text content', + ), + ); + }); + + it('saves a text message with link preview with an ID previously used by the same user for a plain text message', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + const storedEvent = JSON.parse(JSON.stringify(event)); + eventService.loadEvent.mockResolvedValue(storedEvent); + eventService.replaceEvent.mockResolvedValue(storedEvent); + + const initial_time = event.time; + const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString(); + event.data.previews?.push('1'); + event.time = changed_time; + + const savedEvent = (await eventStorageMiddleware.processEvent(event)) as any; + expect(savedEvent.time).toEqual(initial_time); + expect(savedEvent.time).not.toEqual(changed_time); + expect(eventService.replaceEvent).toHaveBeenCalled(); + }); + + it('saves a link preview even if the original message is not found', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent({dataOverrides: {previews: ['1']}}); + + await eventStorageMiddleware.processEvent(event); + expect(eventService.replaceEvent).not.toHaveBeenCalled(); + expect(eventService.saveEvent).toHaveBeenCalled(); + }); + + it('ignores edit message with missing associated original message', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const linkPreviewEvent = createMessageAddEvent(); + eventService.loadEvent.mockResolvedValue(undefined); + + linkPreviewEvent.data.replacing_message_id = 'missing'; + + await expect(eventStorageMiddleware.processEvent(linkPreviewEvent)).rejects.toEqual( + new EventError(EventError.TYPE.VALIDATION_FAILED, 'Event validation failed: Edit event without original event'), + ); + }); + + it('updates edited messages when link preview arrives', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const replacingId = 'old-replaced-message-id'; + const linkPreviewEvent = createMessageAddEvent(); + const storedEvent = JSON.parse( + JSON.stringify({ + ...linkPreviewEvent, + data: {...linkPreviewEvent.data, replacing_message_id: replacingId}, + }), + ); + eventService.loadEvent.mockResolvedValue(storedEvent); + + linkPreviewEvent.data.replacing_message_id = replacingId; + linkPreviewEvent.data.previews = ['preview']; + + const updatedEvent = (await eventStorageMiddleware.processEvent(linkPreviewEvent)) as any; + expect(eventService.replaceEvent).toHaveBeenCalled(); + expect(eventService.saveEvent).not.toHaveBeenCalled(); + expect(updatedEvent.data.previews[0]).toEqual('preview'); + }); + + it('updates edited messages', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + const originalEvent = JSON.parse(JSON.stringify(event)); + originalEvent.reactions = ['user-id']; + eventService.loadEvent.mockResolvedValue(originalEvent); + + const initial_time = event.time; + const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString(); + originalEvent.primary_key = 12; + event.id = createUuid(); + event.data.content = 'new content'; + event.data.replacing_message_id = originalEvent.id; + event.time = changed_time; + + const updatedEvent = (await eventStorageMiddleware.processEvent(event)) as any; + expect(updatedEvent.time).toEqual(initial_time); + expect(updatedEvent.time).not.toEqual(changed_time); + expect(updatedEvent.data.content).toEqual('new content'); + expect(updatedEvent.primary_key).toEqual(originalEvent.primary_key); + expect(Object.keys(updatedEvent.reactions).length).toEqual(0); + expect(eventService.replaceEvent).toHaveBeenCalled(); + }); + + it('updates link preview when edited', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createMessageAddEvent(); + + const replacingId = 'replaced-message-id'; + const storedEvent = { + ...event, + data: {...event.data, previews: ['preview']}, + }; + const editEvent = {...event}; + eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent)); + + editEvent.data.replacing_message_id = replacingId; + + const updatedEvent = (await eventStorageMiddleware.processEvent(editEvent)) as any; + expect(eventService.replaceEvent).toHaveBeenCalled(); + expect(eventService.saveEvent).not.toHaveBeenCalled(); + expect(updatedEvent.data.previews.length).toEqual(0); + }); + + it('saves a conversation.asset-add event', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const event = createAssetAddEvent(); + + const updatedEvent = await eventStorageMiddleware.processEvent(event); + expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(eventService.saveEvent).toHaveBeenCalled(); + }); + + it('deletes cancelled conversation.asset-add event', async () => { + const [eventStorageMiddleware, {eventService, selfUser}] = buildEventStorageMiddleware(); + const fromIds = [ + // cancel from an other user + createUuid(), + // cancel from the self user + selfUser.id, + ]; + + for (const fromId of fromIds) { + const assetAddEvent = createAssetAddEvent({from: fromId}); + const assetCancelEvent = { + ...assetAddEvent, + data: { + ...assetAddEvent.data, + reason: ProtobufAsset.NotUploaded.CANCELLED, + status: AssetTransferState.UPLOAD_FAILED, + }, + time: '2017-09-06T09:43:36.528Z', + }; + + eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent)); + eventService.deleteEvent.mockResolvedValue(1); + + const savedEvent = await eventStorageMiddleware.processEvent(assetCancelEvent); + expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(eventService.deleteEvent).toHaveBeenCalled(); + } + }); + + it('deletes other user failed upload for conversation.asset-add event', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const assetAddEvent = createAssetAddEvent(); + const assetUploadFailedEvent = { + ...assetAddEvent, + data: { + ...assetAddEvent.data, + reason: ProtobufAsset.NotUploaded.FAILED, + status: AssetTransferState.UPLOAD_FAILED, + }, + time: '2017-09-06T09:43:36.528Z', + }; + + eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent)); + eventService.deleteEvent.mockResolvedValue(1); + + const savedEvent = await eventStorageMiddleware.processEvent(assetUploadFailedEvent); + expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(eventService.deleteEvent).toHaveBeenCalled(); + }); + + it('updates self failed upload for conversation.asset-add event', async () => { + const [eventStorageMiddleware, {eventService, selfUser}] = buildEventStorageMiddleware(); + + const assetAddEvent = createAssetAddEvent({from: selfUser.id}); + const assetUploadFailedEvent = { + ...assetAddEvent, + data: { + ...assetAddEvent.data, + reason: ProtobufAsset.NotUploaded.FAILED, + status: AssetTransferState.UPLOAD_FAILED, + }, + time: '2017-09-06T09:43:36.528Z', + } as any; + + eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent)); + + const savedEvent = await eventStorageMiddleware.processEvent(assetUploadFailedEvent); + expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(eventService.replaceEvent).toHaveBeenCalled(); + }); + + it('handles conversation.asset-add state update event', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const initialAssetEvent = createAssetAddEvent({type: ClientEvent.CONVERSATION.ASSET_ADD}); + + const updateStatusEvent = { + ...initialAssetEvent, + data: {...initialAssetEvent.data, status: AssetTransferState.UPLOADED}, + time: '2017-09-06T09:43:36.528Z', + }; + + eventService.loadEvent.mockResolvedValue(toSavedEvent(initialAssetEvent)); + + const updatedEvent = (await eventStorageMiddleware.processEvent(updateStatusEvent)) as any; + expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(updatedEvent.data.status).toEqual(updateStatusEvent.data.status); + expect(eventService.replaceEvent).toHaveBeenCalled(); + }); + + it('updates video when preview is received', async () => { + const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware(); + const initialAssetEvent = createAssetAddEvent(); + + const AssetPreviewEvent = { + ...initialAssetEvent, + data: {...initialAssetEvent.data, status: AssetTransferState.UPLOADED}, + time: '2017-09-06T09:43:36.528Z', + }; + + eventService.loadEvent.mockResolvedValue(toSavedEvent(initialAssetEvent)); + + const updatedEvent = await eventStorageMiddleware.processEvent(AssetPreviewEvent); + expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD); + expect(eventService.replaceEvent).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts new file mode 100644 index 00000000000..66aa216cb49 --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts @@ -0,0 +1,111 @@ +/* + * 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 {User} from 'src/script/entity/User'; + +import {handleLinkPreviewEvent, handleEditEvent, handleAssetEvent} from './eventHandlers'; +import {EventValidationError} from './eventHandlers/EventValidationError'; +import {HandledEvents, DBOperation} from './types'; + +import {isEventRecordFailed, isEventRecordWithFederationError} from '../../../message/StatusType'; +import type {EventRecord} from '../../../storage'; +import {CONVERSATION} from '../../Client'; +import {EventMiddleware, IncomingEvent} from '../../EventProcessor'; +import {EventService} from '../../EventService'; +import {eventShouldBeStored} from '../../EventTypeHandling'; + +export class EventStorageMiddleware implements EventMiddleware { + constructor( + private readonly eventService: EventService, + private readonly selfUser: User, + ) {} + + async processEvent(event: IncomingEvent) { + const shouldSaveEvent = eventShouldBeStored(event); + if (!shouldSaveEvent) { + return event; + } + const eventId = 'id' in event && event.id; + /* We try to load a potential duplicate of the event (same ID, same conversation in the DB). There are multiple valid cases for duplicates: + * - The event is a retry of a previously failed event + * - The event is a link preview of a text message previously sent + * - The event is an asset upload success of a metadata asset message + */ + const duplicateEvent = eventId ? await this.eventService.loadEvent(event.conversation, eventId) : undefined; + + // We first validate that the event is valid + this.validateEvent(event, duplicateEvent); + // Then ask the different handlers which DB operations to perform + const operation = await this.getDbOperation(event, duplicateEvent); + // And finally execute the operation + return operation ? this.execDBOperation(operation, event.conversation) : event; + } + + private async getDbOperation(event: HandledEvents, duplicateEvent?: HandledEvents): Promise { + const handlers = [handleEditEvent, handleLinkPreviewEvent, handleAssetEvent]; + for (const handler of handlers) { + const operation = await handler(event, { + duplicateEvent, + selfUserId: this.selfUser.id, + findEvent: eventId => this.eventService.loadEvent(event.conversation, eventId), + }); + if (operation) { + return operation; + } + } + return {type: 'insert', event}; + } + + private validateEvent(event: HandledEvents, duplicateEvent?: EventRecord) { + if (!duplicateEvent) { + return; + } + if (duplicateEvent.from !== event.from) { + throw new EventValidationError('ID previously used by another user'); + } + + if (event.type !== duplicateEvent.type) { + throw new EventValidationError('ID already used for a different type of message'); + } + + if (event.type === CONVERSATION.MESSAGE_ADD && duplicateEvent.type === CONVERSATION.MESSAGE_ADD) { + const isValidUpdate = !!event.data.previews?.length || event.data.replacing_message_id; + const isRetryAttempt = isEventRecordFailed(duplicateEvent) || isEventRecordWithFederationError(duplicateEvent); + + if (!isValidUpdate && !isRetryAttempt) { + throw new EventValidationError('ID already used for a successfully sent message'); + } + } + } + + private async execDBOperation(operation: DBOperation, conversationId: string) { + switch (operation.type) { + case 'insert': + return this.eventService.saveEvent(operation.event); + + case 'update': + await this.eventService.replaceEvent(operation.updates); + break; + + case 'delete': + await this.eventService.deleteEvent(conversationId, operation.id); + } + return operation.event; + } +} diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts new file mode 100644 index 00000000000..68432ec025c --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts @@ -0,0 +1,26 @@ +/* + * 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 {EventError} from 'src/script/error/EventError'; + +export class EventValidationError extends EventError { + constructor(message: string) { + super(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${message}`); + } +} diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts new file mode 100644 index 00000000000..f18489edd13 --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts @@ -0,0 +1,108 @@ +/* + * 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 {Asset as ProtobufAsset} from '@wireapp/protocol-messaging'; + +import {AssetTransferState} from 'src/script/assets/AssetTransferState'; +import {AssetAddEvent} from 'src/script/conversation/EventBuilder'; +import {StoredEvent} from 'src/script/storage'; + +import {EventValidationError} from './EventValidationError'; + +import {CONVERSATION, ClientEvent} from '../../../Client'; +import {DBOperation, EventHandler, HandledEvents} from '../types'; + +function validateAssetEvent(originalEvent: HandledEvents | undefined): originalEvent is StoredEvent { + if (!originalEvent) { + return false; + } + + if (originalEvent.type !== ClientEvent.CONVERSATION.ASSET_ADD) { + throw new EventValidationError('Trying to update a non-asset message as an asset message'); + } + + return true; +} + +function computeEventUpdates( + originalEvent: StoredEvent, + newEvent: AssetAddEvent, + selfUserId: string, +): DBOperation { + const newEventData = newEvent.data; + // the preview status is not sent by the client so we fake a 'preview' status in order to cleanly handle it in the switch statement + const ASSET_PREVIEW = 'preview'; + // similarly, no status is sent by the client when we retry sending a failed message + const RETRY_EVENT = 'retry'; + const isPreviewEvent = !newEventData.status && !!newEventData.preview_key; + const isRetryEvent = !!newEventData.content_length; + const handledEvent = isRetryEvent ? RETRY_EVENT : newEventData.status; + const previewStatus = isPreviewEvent ? ASSET_PREVIEW : handledEvent; + + const updateEventData = (newData: Partial) => { + return { + ...originalEvent, + data: {...originalEvent.data, ...newData}, + }; + }; + + switch (previewStatus) { + case ASSET_PREVIEW: + case RETRY_EVENT: + case AssetTransferState.UPLOADED: { + return {type: 'update', event: newEvent, updates: updateEventData(newEventData)}; + } + + case AssetTransferState.UPLOAD_FAILED: { + // case of both failed or canceled upload + const fromOther = newEvent.from !== selfUserId; + const sameSender = newEvent.from === originalEvent.from; + const selfCancel = !fromOther && newEvent.data.reason === ProtobufAsset.NotUploaded.CANCELLED; + // we want to delete the event in the case of an error from the remote client or a cancel on the user's own client + const shouldDeleteEvent = (fromOther || selfCancel) && sameSender; + if (shouldDeleteEvent) { + return {type: 'delete', event: newEvent, id: newEvent.id}; + } + + const updatedEvent = updateEventData({ + status: AssetTransferState.UPLOAD_FAILED, + reason: newEvent.data.reason ?? ProtobufAsset.NotUploaded.FAILED, + }); + return { + type: 'update', + event: updatedEvent, + updates: updatedEvent, + }; + } + + default: { + throw new EventValidationError(`Unhandled asset status update '${newEvent.data.status}'`); + } + } +} + +export const handleAssetEvent: EventHandler = async (event, {duplicateEvent, selfUserId}) => { + if (event.type !== CONVERSATION.ASSET_ADD) { + return undefined; + } + if (validateAssetEvent(duplicateEvent)) { + return computeEventUpdates(duplicateEvent, event, selfUserId); + } + return undefined; +}; diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts new file mode 100644 index 00000000000..80f5f88065f --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts @@ -0,0 +1,83 @@ +/* + * 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 {MessageAddEvent} from 'src/script/conversation/EventBuilder'; +import {EventError} from 'src/script/error/EventError'; +import {StoredEvent} from 'src/script/storage'; + +import {getCommonMessageUpdates} from './getCommonMessageUpdates'; + +import {CONVERSATION, ClientEvent} from '../../../Client'; +import {EventHandler, HandledEvents} from '../types'; + +function throwValidationError(message: string): never { + throw new EventError(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${message}`); +} + +function validateEditEvent( + originalEvent: HandledEvents | undefined, + editEvent: MessageAddEvent, +): originalEvent is StoredEvent { + if (!originalEvent) { + throwValidationError('Edit event without original event'); + } + + if (originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD) { + throwValidationError('Edit event for non-text message'); + } + + if (originalEvent.from !== editEvent.from) { + throwValidationError('ID reused by other user'); + } + + return true; +} + +function getUpdatesForEditMessage( + originalEvent: StoredEvent, + newEvent: MessageAddEvent, +): MessageAddEvent { + // Remove reactions, so that likes (hearts) don't stay when a message's text gets edited + const commonUpdates = getCommonMessageUpdates(originalEvent, newEvent); + + return {...newEvent, ...commonUpdates, reactions: {}}; +} + +function computeEventUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) { + const primaryKeyUpdate = {primary_key: originalEvent.primary_key}; + const updates = getUpdatesForEditMessage(originalEvent, newEvent); + + return {...primaryKeyUpdate, ...updates}; +} + +export const handleEditEvent: EventHandler = async (event, {findEvent}) => { + if (event.type !== CONVERSATION.MESSAGE_ADD) { + return undefined; + } + const editedEventId = event.data.replacing_message_id; + if (!editedEventId) { + return undefined; + } + const originalEvent = await findEvent(editedEventId); + if (validateEditEvent(originalEvent, event)) { + const updatedEvent = computeEventUpdates(originalEvent, event); + return {type: 'update', event: updatedEvent, updates: updatedEvent}; + } + return undefined; +}; diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts new file mode 100644 index 00000000000..ffe78482f6c --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts @@ -0,0 +1,48 @@ +/* + * 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 {StatusType} from 'src/script/message/StatusType'; +import {createMessageAddEvent, toSavedEvent} from 'test/helper/EventGenerator'; + +import {getCommonMessageUpdates} from './getCommonMessageUpdates'; + +describe('getCommonMessageUpdates', () => { + /** @see https://wearezeta.atlassian.net/browse/SQCORE-732 */ + it('does not overwrite the seen status if a message gets edited', () => { + const originalEvent = toSavedEvent(createMessageAddEvent({overrides: {status: StatusType.SEEN}})); + + const editedEvent = createMessageAddEvent({ + text: 'Edited Text Which Replaces The Original Text', + overrides: { + read_receipts: [ + { + time: '2021-06-10T19:47:19.570Z', + userId: 'b661e27f-24c6-4c52-a425-87a7b7f3df61', + }, + ], + }, + dataOverrides: {replacing_message_id: originalEvent.id}, + }); + + const updatedEvent = getCommonMessageUpdates(originalEvent, editedEvent) as any; + expect(updatedEvent.data.content).toBe('Edited Text Which Replaces The Original Text'); + expect(updatedEvent.status).toBe(StatusType.SEEN); + expect(Object.keys(updatedEvent.read_receipts).length).toBe(1); + }); +}); diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts new file mode 100644 index 00000000000..776a890cefc --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts @@ -0,0 +1,32 @@ +/* + * 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 {MessageAddEvent} from 'src/script/conversation/EventBuilder'; +import {StoredEvent} from 'src/script/storage'; + +export function getCommonMessageUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) { + return { + ...newEvent, + data: {...newEvent.data, expects_read_confirmation: originalEvent.data.expects_read_confirmation}, + edited_time: newEvent.time, + read_receipts: !newEvent.read_receipts ? originalEvent.read_receipts : newEvent.read_receipts, + status: !newEvent.status || newEvent.status < originalEvent.status ? originalEvent.status : newEvent.status, + time: originalEvent.time, + }; +} diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts new file mode 100644 index 00000000000..469bf765b44 --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts @@ -0,0 +1,22 @@ +/* + * 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/. + * + */ + +export * from './editedEventHandler'; +export * from './linkPreviewEventHandler'; +export * from './assetEventHandler'; diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts new file mode 100644 index 00000000000..040aa14e07a --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts @@ -0,0 +1,88 @@ +/* + * 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 {MessageAddEvent} from 'src/script/conversation/EventBuilder'; +import {categoryFromEvent} from 'src/script/message/MessageCategorization'; +import {StoredEvent} from 'src/script/storage'; + +import {EventValidationError} from './EventValidationError'; +import {getCommonMessageUpdates} from './getCommonMessageUpdates'; + +import {CONVERSATION, ClientEvent} from '../../../Client'; +import {EventHandler, HandledEvents} from '../types'; + +function getLinkPreviewUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) { + const commonUpdates = getCommonMessageUpdates(originalEvent, newEvent); + + return { + ...newEvent, + ...commonUpdates, + category: categoryFromEvent(newEvent), + ephemeral_expires: originalEvent.ephemeral_expires, + ephemeral_started: originalEvent.ephemeral_started, + ephemeral_time: originalEvent.ephemeral_time, + server_time: newEvent.time, + version: originalEvent.version, + }; +} + +function validateLinkPreviewEvent( + originalEvent: HandledEvents | undefined, + editEvent: MessageAddEvent, +): originalEvent is StoredEvent { + const {previews, content} = editEvent.data; + if (!previews?.length) { + return false; + } + if (!originalEvent) { + // It is fine to receive a linkPreview message without the original event + return false; + } + if (originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD) { + throw new EventValidationError('Link preview event for non-text message'); + } + + const {previews: originalPreviews, content: originalContent} = originalEvent.data; + if (!!originalPreviews?.length) { + throw new EventValidationError('Link preview already existing on original message'); + } + + if (content !== originalContent) { + throw new EventValidationError('Link preview with different text content'); + } + return true; +} + +function computeEventUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) { + const primaryKeyUpdate = {primary_key: originalEvent.primary_key}; + const updates = getLinkPreviewUpdates(originalEvent, newEvent); + + return {...primaryKeyUpdate, ...updates}; +} + +export const handleLinkPreviewEvent: EventHandler = async (event, {duplicateEvent}) => { + if (event.type !== CONVERSATION.MESSAGE_ADD) { + return undefined; + } + if (validateLinkPreviewEvent(duplicateEvent, event)) { + const updatedEvent = computeEventUpdates(duplicateEvent, event); + return {type: 'update', event: updatedEvent, updates: updatedEvent}; + } + return undefined; +}; diff --git a/src/script/event/preprocessor/EventStorageMiddleware/index.ts b/src/script/event/preprocessor/EventStorageMiddleware/index.ts new file mode 100644 index 00000000000..0154fc9a98c --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/index.ts @@ -0,0 +1,20 @@ +/* + * 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/. + * + */ + +export * from './EventStorageMiddleware'; diff --git a/src/script/event/preprocessor/EventStorageMiddleware/types.ts b/src/script/event/preprocessor/EventStorageMiddleware/types.ts new file mode 100644 index 00000000000..66a79d03ef1 --- /dev/null +++ b/src/script/event/preprocessor/EventStorageMiddleware/types.ts @@ -0,0 +1,40 @@ +/* + * 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 {ConversationEvent} from '@wireapp/api-client/lib/event'; + +import {EventRecord} from 'src/script/storage'; + +import {ClientConversationEvent} from '../../../conversation/EventBuilder'; +import {IdentifiedUpdatePayload} from '../../EventService'; + +export type HandledEvents = ClientConversationEvent | ConversationEvent; +export type DBOperation = + | {type: 'update'; event: HandledEvents; updates: IdentifiedUpdatePayload} + | {type: 'delete'; event: HandledEvents; id: string} + | {type: 'insert'; event: HandledEvents}; + +export type EventHandler = ( + event: HandledEvents, + optionals: { + selfUserId: string; + duplicateEvent: HandledEvents | undefined; + findEvent: (eventId: string) => Promise; + }, +) => Promise; diff --git a/src/script/event/preprocessor/QuotedMessageMiddleware.ts b/src/script/event/preprocessor/QuotedMessageMiddleware.ts index 8c1652b5d8b..73f43f39573 100644 --- a/src/script/event/preprocessor/QuotedMessageMiddleware.ts +++ b/src/script/event/preprocessor/QuotedMessageMiddleware.ts @@ -64,9 +64,9 @@ export class QuotedMessageMiddleware implements EventMiddleware { const originalMessageId = event.data.message_id; const {replies} = await this.findRepliesToMessage(event.conversation, originalMessageId); this.logger.info(`Invalidating '${replies.length}' replies to deleted message '${originalMessageId}'`); - replies.forEach(reply => { + replies.forEach(async reply => { reply.data.quote = {error: {type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND}}; - this.eventService.replaceEvent(reply); + await this.eventService.replaceEvent(reply); }); return event; } @@ -78,13 +78,12 @@ export class QuotedMessageMiddleware implements EventMiddleware { } this.logger.info(`Updating '${replies.length}' replies to updated message '${originalMessageId}'`); - replies.forEach(reply => { + replies.forEach(async reply => { const quote = reply.data.quote; if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) { quote.message_id = event.id as string; } - // we want to update the messages quoting the original message later, thus the timeout - setTimeout(() => this.eventService.replaceEvent(reply)); + await this.eventService.replaceEvent(reply); }); const decoratedData = {...event.data, quote: originalEvent.data.quote}; return {...event, data: decoratedData}; diff --git a/src/script/main/app.ts b/src/script/main/app.ts index 966b856490b..c2ee80b73e4 100644 --- a/src/script/main/app.ts +++ b/src/script/main/app.ts @@ -70,6 +70,7 @@ import {EventRepository} from '../event/EventRepository'; import {EventService} from '../event/EventService'; import {EventServiceNoCompound} from '../event/EventServiceNoCompound'; import {NotificationService} from '../event/NotificationService'; +import {EventStorageMiddleware} from '../event/preprocessor/EventStorageMiddleware'; import {QuotedMessageMiddleware} from '../event/preprocessor/QuotedMessageMiddleware'; import {ReceiptsMiddleware} from '../event/preprocessor/ReceiptsMiddleware'; import {ServiceMiddleware} from '../event/preprocessor/ServiceMiddleware'; @@ -380,11 +381,17 @@ export class App { await initializeDataDog(this.config, selfUser.qualifiedId); // Setup all event middleware + const eventStorageMiddleware = new EventStorageMiddleware(this.service.event, selfUser); const serviceMiddleware = new ServiceMiddleware(conversationRepository, userRepository, selfUser); const quotedMessageMiddleware = new QuotedMessageMiddleware(this.service.event); const readReceiptMiddleware = new ReceiptsMiddleware(this.service.event, conversationRepository, selfUser); - eventRepository.setEventProcessMiddlewares([serviceMiddleware, quotedMessageMiddleware, readReceiptMiddleware]); + eventRepository.setEventProcessMiddlewares([ + serviceMiddleware, + readReceiptMiddleware, + eventStorageMiddleware, + quotedMessageMiddleware, + ]); // Setup all the event processors const federationEventProcessor = new FederationEventProcessor(eventRepository, serverTimeHandler, selfUser); eventRepository.setEventProcessors([federationEventProcessor]); @@ -422,15 +429,16 @@ export class App { await conversationRepository.conversationRoleRepository.loadTeamRoles(); + let totalNotifications = 0; await eventRepository.connectWebSocket(this.core, ({done, total}) => { const baseMessage = t('initDecryption'); const extraInfo = this.config.FEATURE.SHOW_LOADING_INFORMATION ? ` ${t('initProgress', {number1: done.toString(), number2: total.toString()})}` : ''; + totalNotifications = total; onProgress(25 + 50 * (done / total), `${baseMessage}${extraInfo}`); }); - const notificationsCount = eventRepository.notificationsTotal; await conversationRepository.init1To1Conversations(connections, conversations); @@ -445,7 +453,7 @@ export class App { } telemetry.timeStep(AppInitTimingsStep.UPDATED_FROM_NOTIFICATIONS); - telemetry.addStatistic(AppInitStatisticsValue.NOTIFICATIONS, notificationsCount, 100); + telemetry.addStatistic(AppInitStatisticsValue.NOTIFICATIONS, totalNotifications, 100); eventTrackerRepository.init(propertiesRepository.properties.settings.privacy.telemetry_sharing); onProgress(97.5, t('initUpdatedFromNotifications', this.config.BRAND_NAME)); diff --git a/src/script/page/LeftSidebar/panels/Archive.tsx b/src/script/page/LeftSidebar/panels/Archive.tsx index 683cc07b84c..dc530fa3904 100644 --- a/src/script/page/LeftSidebar/panels/Archive.tsx +++ b/src/script/page/LeftSidebar/panels/Archive.tsx @@ -56,8 +56,6 @@ const Archive = ({ ]); const onClickConversation = async (conversation: Conversation) => { - await conversationRepository.unarchiveConversation(conversation, true, 'opened conversation from archive'); - onClose(); amplify.publish(WebAppEvents.CONVERSATION.SHOW, conversation, {}); }; @@ -68,6 +66,7 @@ const Archive = ({ const {currentFocus, handleKeyDown, resetConversationFocus} = useConversationFocus(conversations); + const isActiveConversation = (conversation: Conversation) => conversationState.isActiveConversation(conversation); return (

{t('archiveHeader')}

@@ -85,6 +84,7 @@ const Archive = ({ conversation={conversation} onJoinCall={answerCall} showJoinButton={false} + isSelected={isActiveConversation} /> ))} diff --git a/src/script/util/test/mock/WebRTCMock.ts b/src/script/util/test/mock/WebRTCMock.ts index 880fa5cd218..c5338e82a16 100644 --- a/src/script/util/test/mock/WebRTCMock.ts +++ b/src/script/util/test/mock/WebRTCMock.ts @@ -19,14 +19,18 @@ import wrtc from '@koush/wrtc'; -const {RTCAudioSource} = wrtc.nonstandard; +const {RTCAudioSource, RTCRtpSender} = wrtc.nonstandard; declare global { interface Window { MediaStream: typeof wrtc.MediaStream; RTCAudioSource: typeof RTCAudioSource; + RTCRtpSender: typeof RTCRtpSender; } } +const RTCRtpSenderMock: Window['RTCRtpSender'] = { + prototype: {createEncodedVideoStreams: {}, createEncodedStreams: {}, transform: {}}, +}; Object.defineProperty(window, 'MediaStream', { value: wrtc.MediaStream, @@ -37,3 +41,8 @@ Object.defineProperty(window, 'RTCAudioSource', { value: RTCAudioSource, writable: true, }); + +Object.defineProperty(window, 'RTCRtpSender', { + value: RTCRtpSenderMock, + writable: true, +}); diff --git a/src/script/view_model/ActionsViewModel.ts b/src/script/view_model/ActionsViewModel.ts index 1296c1da753..93e08c11564 100644 --- a/src/script/view_model/ActionsViewModel.ts +++ b/src/script/view_model/ActionsViewModel.ts @@ -352,10 +352,6 @@ export class ActionsViewModel { }; private readonly openConversation = async (conversationEntity: Conversation): Promise => { - if (conversationEntity.is_archived()) { - await this.conversationRepository.unarchiveConversation(conversationEntity, true); - } - if (conversationEntity.is_cleared()) { conversationEntity.cleared_timestamp(0); } diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts index 6a052d29ec5..023993473bb 100644 --- a/src/script/view_model/ContentViewModel.ts +++ b/src/script/view_model/ContentViewModel.ts @@ -133,12 +133,12 @@ export class ContentViewModel { } } - changeConversation = (conversationEntity: Conversation, messageEntity?: Message) => { + private changeConversation(conversationEntity: Conversation, messageEntity?: Message): void { this.initialMessage = messageEntity; this.conversationState.activeConversation(conversationEntity); - }; + } - private readonly getConversationToDisplay = async ( + private readonly getConversationEntity = async ( conversation: Conversation | string, domain: string | null = null, ): Promise => { @@ -153,6 +153,76 @@ export class ContentViewModel { return this.conversationRepository.init1to1Conversation(conversationEntity, true); }; + private closeRightSidebar(): void { + const {rightSidebar} = useAppMainState.getState(); + rightSidebar.close(); + } + + private handleMissingConversation(): void { + this.closeRightSidebar(); + return this.switchContent(ContentState.CONNECTION_REQUESTS); + } + + private isConversationOpen(conversationEntity: Conversation, isActiveConversation: boolean): boolean { + const {contentState} = useAppState.getState(); + const isConversationState = contentState === ContentState.CONVERSATION; + return conversationEntity && isActiveConversation && isConversationState; + } + + private switchToNotificationSettingsIfApplicable( + openNotificationSettings: boolean, + conversationEntity: Conversation, + ): void { + if (openNotificationSettings) { + const {rightSidebar} = useAppMainState.getState(); + rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity}); + } + } + + private handleConversationState( + isOpenedConversation: boolean, + openNotificationSettings: boolean, + conversationEntity: Conversation, + ): void { + const {setContentState} = useAppState.getState(); + if (isOpenedConversation) { + this.switchToNotificationSettingsIfApplicable(openNotificationSettings, conversationEntity); + return; + } + setContentState(ContentState.CONVERSATION); + + this.mainViewModel.list.openConversations(conversationEntity.archivedState()); + } + private showAndNavigate(conversationEntity: Conversation, openNotificationSettings: boolean): void { + const {rightSidebar} = useAppMainState.getState(); + this.showContent(ContentState.CONVERSATION); + this.previousConversation = this.conversationState.activeConversation(); + setHistoryParam( + generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}), + history.state, + ); + if (openNotificationSettings) { + rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: this.conversationState.activeConversation() ?? null}); + } + } + + private showConversationNotFoundErrorModal(): void { + PrimaryModal.show( + PrimaryModal.type.ACKNOWLEDGE, + { + text: { + message: t('conversationNotFoundMessage'), + title: t('conversationNotFoundTitle', Config.getConfig().BRAND_NAME), + }, + }, + undefined, + ); + } + + private isConversationNotFoundError(error: any): boolean { + return error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND; + } + /** * Opens the specified conversation. * @@ -173,20 +243,15 @@ export class ContentViewModel { openNotificationSettings = false, } = options; - const {rightSidebar} = useAppMainState.getState(); - const {contentState, setContentState} = useAppState.getState(); - if (!conversation) { - rightSidebar.close(); - return this.switchContent(ContentState.CONNECTION_REQUESTS); + return this.handleMissingConversation(); } try { - const conversationEntity = await this.getConversationToDisplay(conversation, domain); + const conversationEntity = await this.getConversationEntity(conversation, domain); if (!conversationEntity) { - rightSidebar.close(); - + this.closeRightSidebar(); throw new ConversationError( ConversationError.TYPE.CONVERSATION_NOT_FOUND, ConversationError.MESSAGE.CONVERSATION_NOT_FOUND, @@ -196,60 +261,25 @@ export class ContentViewModel { const isActiveConversation = this.conversationState.isActiveConversation(conversationEntity); if (!isActiveConversation) { - rightSidebar.close(); + this.closeRightSidebar(); } - const isConversationState = contentState === ContentState.CONVERSATION; - const isOpenedConversation = conversationEntity && isActiveConversation && isConversationState; - - if (isOpenedConversation) { - if (openNotificationSettings) { - rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity}); - } - return; - } - - setContentState(ContentState.CONVERSATION); - this.mainViewModel.list.openConversations(); + const isOpenedConversation = this.isConversationOpen(conversationEntity, isActiveConversation); + this.handleConversationState(isOpenedConversation, openNotificationSettings, conversationEntity); if (!isActiveConversation) { this.conversationState.activeConversation(conversationEntity); } - const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity; - if (conversationEntity.is_cleared()) { conversationEntity.cleared_timestamp(0); } - - if (conversationEntity.is_archived()) { - await this.conversationRepository.unarchiveConversation(conversationEntity); - } - + const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity; this.changeConversation(conversationEntity, messageEntity); - this.showContent(ContentState.CONVERSATION); - this.previousConversation = this.conversationState.activeConversation(); - setHistoryParam( - generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}), - history.state, - ); - - if (openNotificationSettings) { - rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: this.conversationState.activeConversation() ?? null}); - } - } catch (error) { - const isConversationNotFound = error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND; - if (isConversationNotFound) { - PrimaryModal.show( - PrimaryModal.type.ACKNOWLEDGE, - { - text: { - message: t('conversationNotFoundMessage'), - title: t('conversationNotFoundTitle', Config.getConfig().BRAND_NAME), - }, - }, - undefined, - ); + this.showAndNavigate(conversationEntity, openNotificationSettings); + } catch (error: any) { + if (this.isConversationNotFoundError(error)) { + this.showConversationNotFoundErrorModal(); } else { throw error; } diff --git a/src/script/view_model/ListViewModel.ts b/src/script/view_model/ListViewModel.ts index 038f7641a64..0697aea9dfe 100644 --- a/src/script/view_model/ListViewModel.ts +++ b/src/script/view_model/ListViewModel.ts @@ -132,7 +132,6 @@ export class ListViewModel { } private readonly _initSubscriptions = () => { - amplify.subscribe(WebAppEvents.CONVERSATION.SHOW, this.openConversations); amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_ACCOUNT, this.openPreferencesAccount); amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_DEVICES, this.openPreferencesDevices); amplify.subscribe(WebAppEvents.PREFERENCES.SHOW_AV, this.openPreferencesAudioVideo); @@ -279,8 +278,12 @@ export class ListViewModel { } }; - readonly openConversations = (): void => { - const newState = this.isActivatedAccount() ? ListState.CONVERSATIONS : ListState.TEMPORARY_GUEST; + readonly openConversations = (archive = false): void => { + const newState = this.isActivatedAccount() + ? archive + ? ListState.ARCHIVE + : ListState.CONVERSATIONS + : ListState.TEMPORARY_GUEST; this.switchList(newState, false); }; diff --git a/src/types/NodeModules.d.ts b/src/types/NodeModules.d.ts index c2cc54f71fc..1ca9d3d6621 100644 --- a/src/types/NodeModules.d.ts +++ b/src/types/NodeModules.d.ts @@ -20,6 +20,7 @@ declare module '@koush/wrtc' { export const nonstandard: { RTCAudioSource: any; + RTCRtpSender: {prototype: {createEncodedVideoStreams: any; createEncodedStreams: any; transform: any}}; }; export const MediaStream: any; } diff --git a/test/helper/EventGenerator.ts b/test/helper/EventGenerator.ts index 6593f7cb016..b27bfab3f30 100644 --- a/test/helper/EventGenerator.ts +++ b/test/helper/EventGenerator.ts @@ -87,7 +87,9 @@ export function createAssetAddEvent(overrides: Partial = {}): Ass * @param event * @returns */ -export function toSavedEvent(event: MessageAddEvent | AssetAddEvent) { +export function toSavedEvent( + event: T, +): T & {primary_key: string; category: number} { return { primary_key: createUuid(), category: 1, diff --git a/test/helper/UserGenerator.ts b/test/helper/UserGenerator.ts index 32a44806e95..71441fc9453 100644 --- a/test/helper/UserGenerator.ts +++ b/test/helper/UserGenerator.ts @@ -48,7 +48,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/yarn.lock b/yarn.lock index 5b5f3c9a3df..77abeed1cbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2578,6 +2578,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.52.0": + version: 8.52.0 + resolution: "@eslint/js@npm:8.52.0" + checksum: 490893b8091a66415f4ac98b963d23eb287264ea3bd6af7ec788f0570705cf64fd6ab84b717785980f55e39d08ff5c7fde6d8e4391ccb507169370ce3a6d091a + languageName: node + linkType: hard + "@faker-js/faker@npm:8.1.0": version: 8.1.0 resolution: "@faker-js/faker@npm:8.1.0" @@ -2611,9 +2618,9 @@ __metadata: languageName: node linkType: hard -"@formatjs/cli@npm:6.2.0": - version: 6.2.0 - resolution: "@formatjs/cli@npm:6.2.0" +"@formatjs/cli@npm:6.2.1": + version: 6.2.1 + resolution: "@formatjs/cli@npm:6.2.1" peerDependencies: vue: ^3.3.4 peerDependenciesMeta: @@ -2621,7 +2628,7 @@ __metadata: optional: true bin: formatjs: bin/formatjs - checksum: 4afc9535b32d14622e02264f527276117fec9ead67c8172d7f5fa66199b180dc7de7c1a180e36d95e31a1bd366fe826cbfdf510f5b3e86ae2750f01abbd386f1 + checksum: 60404e080f21eb87972007524a3c79ffb5c76509986247e5351bcb5e3fc3c7f88dbbd7bdb8e2986d26734659bc06ad0a5b0498e4c0928ea3e74da30eeabc060b languageName: node linkType: hard @@ -2644,14 +2651,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/icu-messageformat-parser@npm:2.6.2": - version: 2.6.2 - resolution: "@formatjs/icu-messageformat-parser@npm:2.6.2" +"@formatjs/icu-messageformat-parser@npm:2.7.0": + version: 2.7.0 + resolution: "@formatjs/icu-messageformat-parser@npm:2.7.0" dependencies: "@formatjs/ecma402-abstract": 1.17.2 "@formatjs/icu-skeleton-parser": 1.6.2 tslib: ^2.4.0 - checksum: c339a712497deaa14e84610afb6c09377a5641b18fb0ae3d42999354cf2b557c84d7c7fead0d06d38a04f8e63e8002ae51c820a8a3e98b82805e84e7878e9d90 + checksum: 5c289b0090a3549fdeb5ee4c382666cf085be77c8a10abcee879e12e064126ec803a48d7f564a3cb5baf4529ef5fde3a61b30f9c54481279f0fb2cf2e9be1e7e languageName: node linkType: hard @@ -2665,25 +2672,25 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-displaynames@npm:6.5.2": - version: 6.5.2 - resolution: "@formatjs/intl-displaynames@npm:6.5.2" +"@formatjs/intl-displaynames@npm:6.6.1": + version: 6.6.1 + resolution: "@formatjs/intl-displaynames@npm:6.6.1" dependencies: "@formatjs/ecma402-abstract": 1.17.2 "@formatjs/intl-localematcher": 0.4.2 tslib: ^2.4.0 - checksum: e48718c00fc2392aac49bbc6971f44d6e56f3626aaebae9ecf61fe8787b7e11b5b0be9f7be5d8bcd43ea6fbaa7ea4aa1453b042e076f982fa0d24d7744088ccf + checksum: e70c18f5f6228fbf937434c9168eac4d42c7f115410b42b66785ba43db4604184849d9e55b76ca4ce116359fec645b5e41e06b036dcb0966387c19753970c8e5 languageName: node linkType: hard -"@formatjs/intl-listformat@npm:7.4.2": - version: 7.4.2 - resolution: "@formatjs/intl-listformat@npm:7.4.2" +"@formatjs/intl-listformat@npm:7.5.0": + version: 7.5.0 + resolution: "@formatjs/intl-listformat@npm:7.5.0" dependencies: "@formatjs/ecma402-abstract": 1.17.2 "@formatjs/intl-localematcher": 0.4.2 tslib: ^2.4.0 - checksum: e31c0f8cf91c23c22c725db6fee51f5a54399a04d7dc1834a7a83e2d7069e26b92f7276a10c4f35d4ffa82f482c1fcf1240571f2e04e10ef072f2edcd2f69a09 + checksum: 55de558bc7981a0ad442dada2500f369ad05176e7a997cf6ac502b8d446b2ae339477360219553c05d3fce7526823620866d1ba06d4006a00f4712aead5335ab languageName: node linkType: hard @@ -2696,23 +2703,23 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl@npm:2.9.3": - version: 2.9.3 - resolution: "@formatjs/intl@npm:2.9.3" +"@formatjs/intl@npm:2.9.5": + version: 2.9.5 + resolution: "@formatjs/intl@npm:2.9.5" dependencies: "@formatjs/ecma402-abstract": 1.17.2 "@formatjs/fast-memoize": 2.2.0 - "@formatjs/icu-messageformat-parser": 2.6.2 - "@formatjs/intl-displaynames": 6.5.2 - "@formatjs/intl-listformat": 7.4.2 - intl-messageformat: 10.5.3 + "@formatjs/icu-messageformat-parser": 2.7.0 + "@formatjs/intl-displaynames": 6.6.1 + "@formatjs/intl-listformat": 7.5.0 + intl-messageformat: 10.5.4 tslib: ^2.4.0 peerDependencies: - typescript: ^4.7 || 5 + typescript: 5 peerDependenciesMeta: typescript: optional: true - checksum: 70df0ce7121e612f0415ed4dcc83d3d64499127c6a8996d30eb37c91d16d153721befc74332d5ab064b8b03c5bbd4073fd0b0f11f90a73559d997619e285eaac + checksum: 12f6ac51a6630a967dc2a446eab6bf2e1c046aa2a693c550c2fa594bcb095cecf2e38efcc1e07611261c00ffa58a2372d16460e4716fac073fc1b54d302e2520 languageName: node linkType: hard @@ -2736,6 +2743,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.11.13": + version: 0.11.13 + resolution: "@humanwhocodes/config-array@npm:0.11.13" + dependencies: + "@humanwhocodes/object-schema": ^2.0.1 + debug: ^4.1.1 + minimatch: ^3.0.5 + checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805 + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -2750,6 +2768,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/object-schema@npm:^2.0.1": + version: 2.0.1 + resolution: "@humanwhocodes/object-schema@npm:2.0.1" + checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -4046,10 +4071,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.9.0": - version: 1.9.0 - resolution: "@remix-run/router@npm:1.9.0" - checksum: 0537b0ff29879ac85077cb4c42eaca4a295b9efd71477848984c2f2dfa5741c9b83d3106a7bb72994a51a9adfeeab3b0f5a40f2dee8be3f0750feeeca2a6d513 +"@remix-run/router@npm:1.10.0": + version: 1.10.0 + resolution: "@remix-run/router@npm:1.10.0" + checksum: f8f9fcd5f08465a7e0a05378398ff6df2c5c5ef5766df3490a134d64260b3b16f1bd490bb0c3f5925c2671a0c1d8d1fa01dfbdc7ecc3b2447dc6eafe6b73bcc2 languageName: node linkType: hard @@ -4345,12 +4370,12 @@ __metadata: languageName: node linkType: hard -"@types/dexie-batch@npm:0.4.5": - version: 0.4.5 - resolution: "@types/dexie-batch@npm:0.4.5" +"@types/dexie-batch@npm:0.4.6": + version: 0.4.6 + resolution: "@types/dexie-batch@npm:0.4.6" dependencies: dexie: latest - checksum: 827a71d6609b2f19a521b4cef3bb6b6cae932af18572237aa0599fce4ce1936ba398510e3b6da5b2e783844f096b3dc2b5b8b942c8569aa22e37c4d187fa5594 + checksum: 59ef6b3aa47b73a0c2f2b6badc6e32752b1dd8d7e985e5f9b86cfc16dd70af2846643a371152f385d9eeab3d087d5ea47a316e591a16b266abcc3f4a73dd9ba6 languageName: node linkType: hard @@ -4395,20 +4420,20 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:11.0.2": - version: 11.0.2 - resolution: "@types/fs-extra@npm:11.0.2" +"@types/fs-extra@npm:11.0.3": + version: 11.0.3 + resolution: "@types/fs-extra@npm:11.0.3" dependencies: "@types/jsonfile": "*" "@types/node": "*" - checksum: 5b3e30343ee62d2e393e1029355f13f64bab6f3416226e22492483f99da840e2e53ca22cbfa4ac3749f2f83f7086d19c009005c8fa175da01df0fae59c2d73e1 + checksum: f196bc216906e7016a6c9c549dbe204fe7e1e87515c7e961f741309e25f8e2f8c268dba3dbf0ca7f3ddab5911d39888472f8624ac0c11a461f1b2d05377e38fa languageName: node linkType: hard -"@types/generate-changelog@npm:1.8.1": - version: 1.8.1 - resolution: "@types/generate-changelog@npm:1.8.1" - checksum: 97ec6317fbc001720c02b613b5ea52c0be7b4e9bc4639d27dd44baba2714cc45c96f08767e4fc7bcd042d1925193ad53223dc756f6b46132dab507d107bdf2c0 +"@types/generate-changelog@npm:1.8.2": + version: 1.8.2 + resolution: "@types/generate-changelog@npm:1.8.2" + checksum: d2a063f45004c830b0b9395d9a0845e93f3408a7ec90e3e772eb750aa074b4482eba0f7b22eb68ce0d6bbf63457df818347196eb32d4f54588dc96d292c8b5e1 languageName: node linkType: hard @@ -4463,13 +4488,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.5": - version: 29.5.5 - resolution: "@types/jest@npm:29.5.5" +"@types/jest@npm:29.5.6": + version: 29.5.6 + resolution: "@types/jest@npm:29.5.6" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 56e55cde9949bcc0ee2fa34ce5b7c32c2bfb20e53424aa4ff3a210859eeaaa3fdf6f42f81a3f655238039cdaaaf108b054b7a8602f394e6c52b903659338d8c6 + checksum: fa13a27bd1c8efd0381a419478769d0d6d3a8e93e1952d7ac3a16274e8440af6f73ed6f96ac1ff00761198badf2ee226b5ab5583a5d87a78d609ea78da5c5a24 languageName: node linkType: hard @@ -4491,21 +4516,21 @@ __metadata: languageName: node linkType: hard -"@types/js-cookie@npm:3.0.4": - version: 3.0.4 - resolution: "@types/js-cookie@npm:3.0.4" - checksum: 46ac93974776a256f3cedadf60b45ded4d905a5e69986882d8c17baa351cb2e81a691864a1f19c3ca90eaa2cb3eeb7cb5426416b487a7d54cf5ff278d39d7870 +"@types/js-cookie@npm:3.0.5": + version: 3.0.5 + resolution: "@types/js-cookie@npm:3.0.5" + checksum: 4d91ae26445499fdde283928aac9ad149be3561ef9b4d959f77e44694608accd5939c8c68ba42c50c2cfc007ccd442cc566a41077d7f2766390088fa91b612ce languageName: node linkType: hard -"@types/jsdom@npm:21.1.3": - version: 21.1.3 - resolution: "@types/jsdom@npm:21.1.3" +"@types/jsdom@npm:21.1.4": + version: 21.1.4 + resolution: "@types/jsdom@npm:21.1.4" dependencies: "@types/node": "*" "@types/tough-cookie": "*" parse5: ^7.0.0 - checksum: be8e42eb2d24db8abd3a19a229ce2c2e5d0809351765d9053272604ddf1df04fd16ff193eee9a2d79130952c4f91ee995148deae836a4d43451e64db8a698281 + checksum: 915f619111dadd8d1bb7f12b6736c9d2e486911e1aed086de5fb003e7e40ae1e368da322dc04f2122ef47faf40ca75b9315ae2df3e8011f882dcf84660fb0d68 languageName: node linkType: hard @@ -4543,10 +4568,10 @@ __metadata: languageName: node linkType: hard -"@types/keyboardjs@npm:2.5.1": - version: 2.5.1 - resolution: "@types/keyboardjs@npm:2.5.1" - checksum: 5fc7ab296af084cb23e87eeabc000dcec68a74e890f6acfddda914e918b7256aacd8ca09b730b2b0ac1645ee7a4178814e19e8ca01bc0d511647cb5af2127705 +"@types/keyboardjs@npm:2.5.2": + version: 2.5.2 + resolution: "@types/keyboardjs@npm:2.5.2" + checksum: cdb9590e0ac0fcdc8599acfa3a1bd983c84d72d6c2f8cf2b27bf49f13c48de9fa56f7b68c9ed3e0951bc46fa4fd39ac2a1d76232b8fe5a20b28c57220ca996ae languageName: node linkType: hard @@ -4566,13 +4591,20 @@ __metadata: languageName: node linkType: hard -"@types/linkify-it@npm:*, @types/linkify-it@npm:3.0.3": +"@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": + version: 3.0.4 + resolution: "@types/linkify-it@npm:3.0.4" + checksum: cd873857faf77231811a5ee49aadffdbdd7c6309b92ca004cb28320993858d2e30cad7b343c6db928763ed0f766c6ed140e0f995536e488a1447a527b6f8127f + languageName: node + linkType: hard + "@types/loadable__component@npm:^5": version: 5.13.5 resolution: "@types/loadable__component@npm:5.13.5" @@ -4582,13 +4614,13 @@ __metadata: languageName: node linkType: hard -"@types/markdown-it@npm:13.0.2": - version: 13.0.2 - resolution: "@types/markdown-it@npm:13.0.2" +"@types/markdown-it@npm:13.0.4": + version: 13.0.4 + resolution: "@types/markdown-it@npm:13.0.4" dependencies: "@types/linkify-it": "*" "@types/mdurl": "*" - checksum: fe1f6a12ee8ad2246359376431a30d22c9b603e63e93e3e27d6920840934b9764034679a4d0b01ec54b0693c8d5c42012ec34715cba4f5b0736b8a4b66db4c74 + checksum: 0af9c349467599f984e8faf548144e5c68ca8926d45e155cbc622897290fadb1fd31fced8194a2a1095406805dd21719d2bc74d8dc0295581d0eed771a4fd58b languageName: node linkType: hard @@ -4616,7 +4648,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^20.8.6": +"@types/node@npm:*, @types/node@npm:>=13.7.0": version: 20.8.6 resolution: "@types/node@npm:20.8.6" dependencies: @@ -4639,6 +4671,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.8.7": + version: 20.8.7 + resolution: "@types/node@npm:20.8.7" + dependencies: + undici-types: ~5.25.1 + checksum: 2173c0c03daefcb60c03a61b1371b28c8fe412e7a40dc6646458b809d14a85fbc7aeb369d957d57f0aaaafd99964e77436f29b3b579232d8f2b20c58abbd1d25 + languageName: node + linkType: hard + "@types/node@npm:~14": version: 14.18.63 resolution: "@types/node@npm:14.18.63" @@ -4653,12 +4694,12 @@ __metadata: languageName: node linkType: hard -"@types/open-graph@npm:0.2.3": - version: 0.2.3 - resolution: "@types/open-graph@npm:0.2.3" +"@types/open-graph@npm:0.2.4": + version: 0.2.4 + resolution: "@types/open-graph@npm:0.2.4" dependencies: "@types/cheerio": "*" - checksum: 7d3cf515e10ff7c646c1f9da4c6a865e6b421841e8a2c68568ac21ef9c13210a68c8e7b695f0af77fd1dbc3dbdb3231c407281fbcb49ccda57991b78c11ab928 + checksum: d073053a5566c0615f2aa5455c0d442d5bb6e6442452716f1aec97431ca63c3d5ad1917829285a47bf209de1c26cc57c29c94e7f9d3ed8216e7806129cc7d925 languageName: node linkType: hard @@ -4669,10 +4710,10 @@ __metadata: languageName: node linkType: hard -"@types/platform@npm:1.3.4": - version: 1.3.4 - resolution: "@types/platform@npm:1.3.4" - checksum: dce83952e6af3e488fb2a3c7c7ec226cef4a8606ab63bd40301fe7883ff0c434512210497d6cb79adc4c8e671648ed3e7035438a40f41a5f395c60d2bfb6390d +"@types/platform@npm:1.3.5": + version: 1.3.5 + resolution: "@types/platform@npm:1.3.5" + checksum: acdefc3f58029747d76dc9f985ab1fc3dd62125d53fe807eedf2cf88f5c660e0aa31e4c2737cfe7738cf1972608ea03d06ca368ce1385cd5e5a5d54632d2a18c languageName: node linkType: hard @@ -4692,7 +4733,16 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:18.2.13, @types/react-dom@npm:^18.0.0": +"@types/react-dom@npm:18.2.14": + version: 18.2.14 + resolution: "@types/react-dom@npm:18.2.14" + dependencies: + "@types/react": "*" + checksum: 890289c70d1966c168037637c09cacefe6205bdd27a33252144a6b432595a2943775ac1a1accac0beddaeb67f8fdf721e076acb1adc990b08e51c3d9fd4e780c + languageName: node + linkType: hard + +"@types/react-dom@npm:^18.0.0": version: 18.2.13 resolution: "@types/react-dom@npm:18.2.13" dependencies: @@ -4701,19 +4751,28 @@ __metadata: languageName: node linkType: hard -"@types/react-redux@npm:7.1.27": - version: 7.1.27 - resolution: "@types/react-redux@npm:7.1.27" +"@types/react-redux@npm:7.1.28": + version: 7.1.28 + resolution: "@types/react-redux@npm:7.1.28" dependencies: "@types/hoist-non-react-statics": ^3.3.0 "@types/react": "*" hoist-non-react-statics: ^3.3.0 redux: ^4.0.0 - checksum: 38fcc56f013e81e9a3125fd75acdacb4cdb5f9fe49402330b4783923f236d2d12ccdd2240ffa42e5bbb75900acd55393c00e0ca5dd6cab91a7b7e39e74ac62b4 + checksum: 521729d7ba781a700e39bf8f8723632ea0abe3698f43a7e131f7613f64613b5724b33ecb5446623ee698182407036f19fc56918c427d7b1fd05ad2a2bd9b6643 + languageName: node + linkType: hard + +"@types/react-transition-group@npm:4.4.8": + version: 4.4.8 + resolution: "@types/react-transition-group@npm:4.4.8" + dependencies: + "@types/react": "*" + checksum: ad7ba2bce97631fda9d89b4ed9772489bd050fec3ccd7563041b206dbe219d37d22e0d7731b1f90f56e89daf40e69ba16beba8066c42165bf8a584533feb6a2c languageName: node linkType: hard -"@types/react-transition-group@npm:4.4.7, @types/react-transition-group@npm:^4.4.0": +"@types/react-transition-group@npm:^4.4.0": version: 4.4.7 resolution: "@types/react-transition-group@npm:4.4.7" dependencies: @@ -4733,12 +4792,12 @@ __metadata: languageName: node linkType: hard -"@types/redux-mock-store@npm:1.0.4": - version: 1.0.4 - resolution: "@types/redux-mock-store@npm:1.0.4" +"@types/redux-mock-store@npm:1.0.5": + version: 1.0.5 + resolution: "@types/redux-mock-store@npm:1.0.5" dependencies: redux: ^4.0.5 - checksum: 37a985fffe714cedefa9faaff1f0490e2d1638a2b926690a5e9680ed1aca89ae0a9a01f7ffa09951ed5c5b85e3fefaabcad86e0a502532e7028faf56c31ed6f3 + checksum: 0ecae2d503f388c0e51e0b0eff10bedf3bfe331943c57b43efc66b84b1c1755079c736e8e099ab5be6336ff20d17a3377847c6d7abe39244cd78a69d0db3063d languageName: node linkType: hard @@ -4795,10 +4854,10 @@ __metadata: languageName: node linkType: hard -"@types/speakingurl@npm:13.0.4": - version: 13.0.4 - resolution: "@types/speakingurl@npm:13.0.4" - checksum: 1d9d0d5554ebbf8f1752be0c714680a49f414409053d19e7d5f83756c1f0bc2f7d483c46c92154585ac4e23576924585ae242f97ccaa80be0973d8afdbe512fb +"@types/speakingurl@npm:13.0.5": + version: 13.0.5 + resolution: "@types/speakingurl@npm:13.0.5" + checksum: 66b6ee271a684658be9e36625aa72fcc685ea329306f143e36b344cd55295cf384376a1d8942664d610153993b78acf66e06fe217b0e104a131204145b6747cb languageName: node linkType: hard @@ -4823,10 +4882,10 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:1.11.11": - version: 1.11.11 - resolution: "@types/underscore@npm:1.11.11" - checksum: 56fbd04305d0982178f32fd4adcfcf6012f75005a3cf2e33eca7f0bf99dadc31c6c8c4884cb0d3896af16c4e06ccf02810305e970899d4766ec2760b19f61511 +"@types/underscore@npm:1.11.12": + version: 1.11.12 + resolution: "@types/underscore@npm:1.11.12" + checksum: d9ee8505db35c9d07323613afb0df43197815efaeb34034ce2f37386ffd03637324bda27dab9d1ba7ac1c982824e1e3e1454f7d3ed2d56e0dd1d3d07959ecd4d languageName: node linkType: hard @@ -4837,10 +4896,10 @@ __metadata: languageName: node linkType: hard -"@types/webpack-env@npm:1.18.2": - version: 1.18.2 - resolution: "@types/webpack-env@npm:1.18.2" - checksum: 883908ade827d35a10efc574fb6f2728a7c520d4296cf1507633ac7457204ccd697bc6c8cadac99bc5d96074a6109c658ebfde59f42ba5ba0fdfffc538892b0f +"@types/webpack-env@npm:1.18.3": + version: 1.18.3 + resolution: "@types/webpack-env@npm:1.18.3" + checksum: f24e82485d8e325b1875608766ba6dad2b2f53a0fb182bc96173b4590110c6b791163402cdf19b50c04e8c05292f227a08aafe9230b2bba52c40c5f7ceccc101 languageName: node linkType: hard @@ -5045,6 +5104,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": version: 1.11.6 resolution: "@webassemblyjs/ast@npm:1.11.6" @@ -5250,10 +5316,10 @@ __metadata: languageName: node linkType: hard -"@wireapp/avs@npm:9.4.18": - version: 9.4.18 - resolution: "@wireapp/avs@npm:9.4.18" - checksum: 6786018a880336bba8b1e00cc35de0b5834efedd3ecd8cc5c905fb40ef9cb3b8adc63d23bd202e3ba58e104fd4b9b011900d76f951e84e62de0bea0b5a8f32be +"@wireapp/avs@npm:9.5.2": + version: 9.5.2 + resolution: "@wireapp/avs@npm:9.5.2" + checksum: e728dfed25af97c6dee587a7f9174466971866993e098a0017eba37ed40a70b4a4155f4f9ab25968020e048ee1a31ced8ae9a224ee77cb793a556b40a01950af languageName: node linkType: hard @@ -5435,9 +5501,9 @@ __metadata: languageName: node linkType: hard -"@wireapp/react-ui-kit@npm:9.9.10": - version: 9.9.10 - resolution: "@wireapp/react-ui-kit@npm:9.9.10" +"@wireapp/react-ui-kit@npm:9.9.11": + version: 9.9.11 + resolution: "@wireapp/react-ui-kit@npm:9.9.11" dependencies: "@types/color": 3.0.4 color: 4.2.3 @@ -5452,7 +5518,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 6946ac8f7be096e09d9abb8ad5d44d5fd0b4196f9e7581bd026ec04d941f12ec436dd6f9e6793658583ce2d9e494e1bc19bb587130967509685252d1a885d38e + checksum: 2bb5023b171b9b5da5084b783b8a55221f971b60acd8f7f8770f81578dd2edfa05ed8854c856235f0648231278762ac977305d602a953f31c89e762abb396b85 languageName: node linkType: hard @@ -7210,10 +7276,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.33.0": - version: 3.33.0 - resolution: "core-js@npm:3.33.0" - checksum: dd62217935ac281faf6f833bb306fb891162919fcf9c1f0c975b1b91e82ac09a940f5deb5950bbb582739ceef716e8bd7e4f9eab8328932fb029d3bc2ecb2881 +"core-js@npm:3.33.1": + version: 3.33.1 + resolution: "core-js@npm:3.33.1" + checksum: 3a95003b0e77995203587117f3bde7f4e96adf434b6b78033dbe60347ffe38b2bac31eafab6a4cc641e5766062846b52f336ab4553fc0902c278959af4778e53 languageName: node linkType: hard @@ -7266,10 +7332,10 @@ __metadata: languageName: node linkType: hard -"countly-sdk-web@npm:23.6.1": - version: 23.6.1 - resolution: "countly-sdk-web@npm:23.6.1" - checksum: d519b155f4377dec56b9142f72e3c9cbb7a5614e7a1076b01b5afcb89ef956dc72fc1e960652c3b77bdb7772757468cb204543080bf4f284a876b5027df15b00 +"countly-sdk-web@npm:23.6.2": + version: 23.6.2 + resolution: "countly-sdk-web@npm:23.6.2" + checksum: 37a40c7c48bd9c7f2d496a5a5f3e1f86370f3724a425f97f25423d8c013383c1e29978137fbc835da78d833233936e8625c4ae0a9adb6b0302062d9c00dda2f2 languageName: node linkType: hard @@ -8252,14 +8318,14 @@ __metadata: languageName: node linkType: hard -"emoji-picker-react@npm:4.5.2": - version: 4.5.2 - resolution: "emoji-picker-react@npm:4.5.2" +"emoji-picker-react@npm:4.5.3": + version: 4.5.3 + resolution: "emoji-picker-react@npm:4.5.3" dependencies: clsx: ^1.2.1 peerDependencies: react: ">=16" - checksum: b0f8aa3347cf804abcc209b5a707319d405f90bfc2a27a3b98900f2f8233b1a967328cca034b930d388d4a9480d7c12b2ded795976a554d9962eadaa607aaa06 + checksum: 79681d0e4f3a733c9fa715559b8e3e6c20810e745844718d7072d909f9e5eb2870ba782df5d23d74e0b3ce6d733bbe045b42bf4b27bf1cfc0f5ecfbc971cfe61 languageName: node linkType: hard @@ -8912,7 +8978,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8, eslint@npm:^8.51.0": +"eslint@npm:^8": version: 8.51.0 resolution: "eslint@npm:8.51.0" dependencies: @@ -8959,6 +9025,54 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.52.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.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 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.2 + eslint-visitor-keys: ^3.4.3 + espree: ^9.6.1 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + strip-ansi: ^6.0.1 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: fd22d1e9bd7090e31b00cbc7a3b98f3b76020a4c4641f987ae7d0c8f52e1b88c3b268bdfdabac2e1a93513e5d11339b718ff45cbff48a44c35d7e52feba510ed + languageName: node + linkType: hard + "espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -10550,15 +10664,15 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:10.5.3": - version: 10.5.3 - resolution: "intl-messageformat@npm:10.5.3" +"intl-messageformat@npm:10.5.4": + version: 10.5.4 + resolution: "intl-messageformat@npm:10.5.4" dependencies: "@formatjs/ecma402-abstract": 1.17.2 "@formatjs/fast-memoize": 2.2.0 - "@formatjs/icu-messageformat-parser": 2.6.2 + "@formatjs/icu-messageformat-parser": 2.7.0 tslib: ^2.4.0 - checksum: cc71cfa9b93aacba3f9d8ddcef7fab2bf81125de3edb7ee73b58185d107e86c301d54bd0e7a87141e311f5104ee90182f1ca52d8ee94446a0dfccc5c7db8541f + checksum: 1ac36b37134c435956762b5e9078314bb1d999992253c7ec884d135bf94eea160a3feede9d3c61c3b8a3016ca1553ba3fa3132bf95be4400c59ea95cb85a5ad6 languageName: node linkType: hard @@ -15281,27 +15395,27 @@ __metadata: languageName: node linkType: hard -"react-intl@npm:6.4.7": - version: 6.4.7 - resolution: "react-intl@npm:6.4.7" +"react-intl@npm:6.5.1": + version: 6.5.1 + resolution: "react-intl@npm:6.5.1" dependencies: "@formatjs/ecma402-abstract": 1.17.2 - "@formatjs/icu-messageformat-parser": 2.6.2 - "@formatjs/intl": 2.9.3 - "@formatjs/intl-displaynames": 6.5.2 - "@formatjs/intl-listformat": 7.4.2 + "@formatjs/icu-messageformat-parser": 2.7.0 + "@formatjs/intl": 2.9.5 + "@formatjs/intl-displaynames": 6.6.1 + "@formatjs/intl-listformat": 7.5.0 "@types/hoist-non-react-statics": ^3.3.1 "@types/react": 16 || 17 || 18 hoist-non-react-statics: ^3.3.2 - intl-messageformat: 10.5.3 + intl-messageformat: 10.5.4 tslib: ^2.4.0 peerDependencies: react: ^16.6.0 || 17 || 18 - typescript: ^4.7 || 5 + typescript: 5 peerDependenciesMeta: typescript: optional: true - checksum: 5b53096e4bf1e2a60b1175d381d645b284723280575054eba7c2f3a0b7aaf338228ffb7f1319caa7f7cdd7a463c9ba564e7ee09e3be794aa4c556fc850bbd670 + checksum: 9cdf4549d490f34bc664923b3eaaf42aed8e3a9453ebdeffb96cf7a21f5db251b39ae2a3885cdb365579759f6e46ec2bb8f6717324851768efd77e2ccf7fc820 languageName: node linkType: hard @@ -15358,27 +15472,27 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.16.0": - version: 6.16.0 - resolution: "react-router-dom@npm:6.16.0" +"react-router-dom@npm:6.17.0": + version: 6.17.0 + resolution: "react-router-dom@npm:6.17.0" dependencies: - "@remix-run/router": 1.9.0 - react-router: 6.16.0 + "@remix-run/router": 1.10.0 + react-router: 6.17.0 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 18b398924bb0f0d97cf2f71dab71d860b960a7a267b2f77388990c662bb6d8738bdc3042d92f713fd63d269ae9ad90df39c8e97661b6ba758bbb7386b9f20ae0 + checksum: e0ba4f4c507681e2ffdecdf2e67edf7ec0e2bf4be35222e29d013afdb03866a5e6ecacc8b452bd55797b9672785d02f81bd6dbf6b05ac93a59e48e774b0060de languageName: node linkType: hard -"react-router@npm:6.16.0": - version: 6.16.0 - resolution: "react-router@npm:6.16.0" +"react-router@npm:6.17.0": + version: 6.17.0 + resolution: "react-router@npm:6.17.0" dependencies: - "@remix-run/router": 1.9.0 + "@remix-run/router": 1.10.0 peerDependencies: react: ">=16.8" - checksum: b31c187e3fdcdf7294ffdad6ff834e14d012840c94d8ee8c7fbe451062a8fcb6e31e8bc7827fb1ff45445dd40fad2b8c96a7e98f0ac1c3eb1d716c257a0821c9 + checksum: 99c30d94fbb34657e4c8c3ef1aaae33b143167d3869b442e06c83b4006f35200fde810029180e209654bef2f47f0b27a928f77cc2d859a358a2722cc9d494f03 languageName: node linkType: hard @@ -18373,47 +18487,47 @@ __metadata: "@emotion/eslint-plugin": ^11.11.0 "@emotion/react": 11.11.1 "@faker-js/faker": 8.1.0 - "@formatjs/cli": 6.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.5 + "@types/dexie-batch": 0.4.6 "@types/eslint": ^8 - "@types/fs-extra": 11.0.2 - "@types/generate-changelog": 1.8.1 - "@types/jest": 29.5.5 + "@types/fs-extra": 11.0.3 + "@types/generate-changelog": 1.8.2 + "@types/jest": 29.5.6 "@types/jquery": ^3 - "@types/js-cookie": 3.0.4 - "@types/jsdom": 21.1.3 - "@types/keyboardjs": 2.5.1 + "@types/js-cookie": 3.0.5 + "@types/jsdom": 21.1.4 + "@types/keyboardjs": 2.5.2 "@types/libsodium-wrappers": ^0 - "@types/linkify-it": 3.0.3 + "@types/linkify-it": 3.0.4 "@types/loadable__component": ^5 - "@types/markdown-it": 13.0.2 - "@types/node": ^20.8.6 - "@types/open-graph": 0.2.3 - "@types/platform": 1.3.4 + "@types/markdown-it": 13.0.4 + "@types/node": ^20.8.7 + "@types/open-graph": 0.2.4 + "@types/platform": 1.3.5 "@types/react": 18.2.28 - "@types/react-dom": 18.2.13 - "@types/react-redux": 7.1.27 - "@types/react-transition-group": 4.4.7 - "@types/redux-mock-store": 1.0.4 + "@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/speakingurl": 13.0.4 - "@types/underscore": 1.11.11 - "@types/webpack-env": 1.18.2 - "@wireapp/avs": 9.4.18 + "@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/eslint-config": 3.0.4 "@wireapp/lru-cache": 3.8.1 "@wireapp/prettier-config": 0.6.3 - "@wireapp/react-ui-kit": 9.9.10 + "@wireapp/react-ui-kit": 9.9.11 "@wireapp/store-engine": ^5.1.4 "@wireapp/store-engine-dexie": 2.1.6 "@wireapp/store-engine-sqleet": 1.8.9 @@ -18427,8 +18541,8 @@ __metadata: beautiful-react-hooks: ^5.0.0 classnames: 2.3.2 copy-webpack-plugin: 11.0.0 - core-js: 3.33.0 - countly-sdk-web: 23.6.1 + core-js: 3.33.1 + countly-sdk-web: 23.6.2 cross-env: 7.0.3 cspell: 7.3.8 css-loader: ^6.8.1 @@ -18438,8 +18552,8 @@ __metadata: dexie-batch: 0.4.3 dotenv: 16.3.1 dpdm: 3.14.0 - emoji-picker-react: 4.5.2 - eslint: ^8.51.0 + emoji-picker-react: 4.5.3 + eslint: ^8.52.0 eslint-plugin-prettier: ^5.0.1 fake-indexeddb: 4.0.2 generate-changelog: 1.8.0 @@ -18485,10 +18599,10 @@ __metadata: react: 18.2.0 react-dom: 18.2.0 react-error-boundary: 4.0.11 - react-intl: 6.4.7 + react-intl: 6.5.1 react-redux: 8.1.3 - react-router: 6.16.0 - react-router-dom: 6.16.0 + react-router: 6.17.0 + react-router-dom: 6.17.0 react-transition-group: 4.4.5 redux: 4.2.1 redux-devtools-extension: 2.13.9 @@ -18520,7 +18634,7 @@ __metadata: webpack-hot-middleware: 2.25.4 webrtc-adapter: 8.2.3 workbox-webpack-plugin: 7.0.0 - zustand: 4.4.3 + zustand: 4.4.4 languageName: unknown linkType: soft @@ -19034,9 +19148,9 @@ __metadata: languageName: node linkType: hard -"zustand@npm:4.4.3": - version: 4.4.3 - resolution: "zustand@npm:4.4.3" +"zustand@npm:4.4.4": + version: 4.4.4 + resolution: "zustand@npm:4.4.4" dependencies: use-sync-external-store: 1.2.0 peerDependencies: @@ -19050,6 +19164,6 @@ __metadata: optional: true react: optional: true - checksum: 3ed16457a3a4b9fe6523f52d397af37db8fab5687dd21a23ede25f657346b25df374490baea27f10d416faae5e96acf7b4065c86044746d775881d266d1500f0 + checksum: 371fd842dc704ed5983c6d64a77994c9c91867338c742d162ac95c4252b5f98fc38aeb2d5a07f48311babed5ca7dbff2d2258301db0ae143d32897bcf3ae651b languageName: node linkType: hard