diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc60..677ad2724 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npx lint-staged +npx git-format-staged --formatter "swiftformat stdin --stdinpath '{}'" "*.swift" "!Pods/*" \ No newline at end of file diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index b1b1e2427..f084a8f53 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -113,4 +113,4 @@ after_pipeline: jobs: - name: Publish Results commands: - - test-results gen-pipeline-report + - test-results gen-pipeline-report \ No newline at end of file diff --git a/.swift-version b/.swift-version index 2df33d769..760606e1f 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.6 +5.7 diff --git a/.swiftlint.yml b/.swiftlint.yml index 6500b6193..c625f64a6 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -18,6 +18,80 @@ disabled_rules: # - class_delegate_protocol #disabled this rule since it cannot track inheritance of protocol if protocols declared in different files # - operator_whitespace +analyzer_rules: + - unused_declaration + - unused_import +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + # - discouraged_none_name + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - extension_access_modifier + - fallthrough + - fatal_error_message + # - file_header + # - file_name + - first_where + - flatmap_over_map_reduce + - identical_operands + - joined_default_parameter + - last_where + - legacy_multiple + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - nimble_operator + # - nslocalizedstring_key + - number_separator + # - object_literal + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - return_value_from_void_function + - single_test_class + - sorted_first_last + # - sorted_imports + - static_operator + - strong_iboutlet + - test_case_accessibility + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + # - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + included: - FlowCrypt - FlowCryptUI @@ -25,12 +99,21 @@ included: excluded: - FlowCrypt/Core - FlowCrypt/Functionality/Imap + + +attributes: + always_on_same_line: + ["@objc"] line_length: 140 function_body_length: 50 -# type_body_length: -# - 400 # Warning -# - 550 # Error +type_body_length: + - 500 # Warning + - 600 # Error + +number_separator: + minimum_length: 5 + minimum_fraction_length: 7 # warning_threshold: 1 diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index a935ad029..412fc7b85 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.18"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.0"), ], targets: [.target(name: "BuildTools", path: "")] ) diff --git a/Core/package-lock.json b/Core/package-lock.json index 465890478..c3aa8a7d9 100644 --- a/Core/package-lock.json +++ b/Core/package-lock.json @@ -12,7 +12,7 @@ "@openpgp/web-stream-tools": "^0.0.12", "encoding-japanese": "^2.0.0", "openpgp": "5.5.0", - "sanitize-html": "2.7.1", + "sanitize-html": "2.7.2", "zxcvbn": "4.4.2" }, "devDependencies": { @@ -91,13 +91,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -483,9 +483,9 @@ } }, "node_modules/ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true, "engines": { "node": ">=12" @@ -759,9 +759,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "funding": [ { @@ -774,10 +774,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" }, "bin": { "browserslist": "cli.js" @@ -829,9 +829,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true, "funding": [ { @@ -881,9 +881,9 @@ } }, "node_modules/chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.0.tgz", + "integrity": "sha512-56zD4khRTBoIyzUYAFgDDaPhUMN/fC/rySe6aZGqbj/VWiU2eI3l6ZLOtYGFZAV5v02mwPjtpzlrOveJiz5eZQ==", "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -944,9 +944,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "node_modules/ci-parallel-vars": { @@ -996,14 +996,17 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/cliui/node_modules/ansi-regex": { @@ -1452,9 +1455,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.237.tgz", - "integrity": "sha512-vxVyGJcsgArNOVUJcXm+7iY3PJAfmSapEszQD1HbyPLl0qoCmNQ1o/EX3RI7Et5/88In9oLxX3SGF8J3orkUgA==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "node_modules/emittery": { @@ -1651,9 +1654,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2184,9 +2187,9 @@ "dev": true }, "node_modules/is-unicode-supported": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.2.0.tgz", - "integrity": "sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "engines": { "node": ">=12" @@ -2867,9 +2870,9 @@ } }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "funding": [ { "type": "opencollective", @@ -3134,9 +3137,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.2.tgz", + "integrity": "sha512-DggSTe7MviO+K4YTCwprG6W1vsG+IIX67yp/QY55yQqKCJYSWzCA1rZbaXzkjoKeL9+jqwm56wD6srYLtUNivg==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -3176,9 +3179,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3450,9 +3453,9 @@ } }, "node_modules/terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -3588,9 +3591,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", "dev": true, "funding": [ { @@ -3940,12 +3943,12 @@ "dev": true }, "node_modules/yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", @@ -4081,13 +4084,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@nodelib/fs.scandir": { @@ -4422,9 +4425,9 @@ "dev": true }, "ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true }, "anymatch": { @@ -4631,15 +4634,15 @@ } }, "browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" } }, "buffer": { @@ -4665,9 +4668,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true }, "caseless": { @@ -4701,9 +4704,9 @@ } }, "chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.0.tgz", + "integrity": "sha512-56zD4khRTBoIyzUYAFgDDaPhUMN/fC/rySe6aZGqbj/VWiU2eI3l6ZLOtYGFZAV5v02mwPjtpzlrOveJiz5eZQ==", "dev": true }, "check-error": { @@ -4741,9 +4744,9 @@ "dev": true }, "ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "ci-parallel-vars": { @@ -4778,13 +4781,13 @@ } }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "dependencies": { @@ -5132,9 +5135,9 @@ } }, "electron-to-chromium": { - "version": "1.4.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.237.tgz", - "integrity": "sha512-vxVyGJcsgArNOVUJcXm+7iY3PJAfmSapEszQD1HbyPLl0qoCmNQ1o/EX3RI7Et5/88In9oLxX3SGF8J3orkUgA==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "emittery": { @@ -5275,9 +5278,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -5651,9 +5654,9 @@ "dev": true }, "is-unicode-supported": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.2.0.tgz", - "integrity": "sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true }, "isexe": { @@ -6144,9 +6147,9 @@ } }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "requires": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", @@ -6309,9 +6312,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.2.tgz", + "integrity": "sha512-DggSTe7MviO+K4YTCwprG6W1vsG+IIX67yp/QY55yQqKCJYSWzCA1rZbaXzkjoKeL9+jqwm56wD6srYLtUNivg==", "requires": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -6340,9 +6343,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -6533,9 +6536,9 @@ "dev": true }, "terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.2", @@ -6616,9 +6619,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -6853,12 +6856,12 @@ "dev": true }, "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", diff --git a/Core/package.json b/Core/package.json index 78951c2c0..822b9ec67 100644 --- a/Core/package.json +++ b/Core/package.json @@ -6,7 +6,7 @@ "@openpgp/web-stream-tools": "^0.0.12", "encoding-japanese": "^2.0.0", "openpgp": "5.5.0", - "sanitize-html": "2.7.1", + "sanitize-html": "2.7.2", "zxcvbn": "4.4.2" }, "devDependencies": { diff --git a/Core/source/mobile-interface/endpoints.ts b/Core/source/mobile-interface/endpoints.ts index bae090dbd..c65220dea 100644 --- a/Core/source/mobile-interface/endpoints.ts +++ b/Core/source/mobile-interface/endpoints.ts @@ -5,7 +5,7 @@ import { Buffers, EndpointRes, fmtContentBlock, fmtRes, isContentBlock } from './format-output'; import { DecryptErrTypes, PgpMsg } from '../core/pgp-msg'; import { KeyDetails, PgpKey } from '../core/pgp-key'; -import { Mime, RichHeaders } from '../core/mime'; +import { Mime, RichHeaders, SendableMsgBody } from '../core/mime'; import { Att } from '../core/att'; import { Buf } from '../core/buf'; @@ -55,9 +55,12 @@ export class Endpoints { if (req.format === 'plain') { const atts = (req.atts || []).map(({ name, type, base64 }) => new Att({ name, type, data: Buf.fromBase64Str(base64) })); - return fmtRes({}, Buf.fromUtfStr(await Mime.encode( - // eslint-disable-next-line @typescript-eslint/naming-convention - { 'text/plain': req.text, 'text/html': req.html }, mimeHeaders, atts))); + // eslint-disable-next-line @typescript-eslint/naming-convention + const body: SendableMsgBody = { 'text/plain': req.text }; + if (req.html) { + body['text/html'] = req.html; + } + return fmtRes({}, Buf.fromUtfStr(await Mime.encode(body, mimeHeaders, atts))); } else if (req.format === 'encrypt-inline') { const encryptedAtts: Att[] = []; for (const att of req.atts || []) { diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index d38dd9b73..c8f708107 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -87,7 +87,7 @@ 51114DB6280EB699006C252A /* gmp-interface.m in Sources */ = {isa = PBXBuildFile; fileRef = 51114DB5280EB699006C252A /* gmp-interface.m */; }; 5113D22D28C0C43700A131E0 /* Gmail+MessageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5113D22C28C0C43700A131E0 /* Gmail+MessageExtension.swift */; }; 511737682851F31700337B9F /* GoogleScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511737672851F31700337B9F /* GoogleScope.swift */; }; - 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */; }; + 511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */; }; 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */; }; 512C1414271077F8002DE13F /* GoogleAPIClientForREST_PeopleService in Frameworks */ = {isa = PBXBuildFile; productRef = 512C1413271077F8002DE13F /* GoogleAPIClientForREST_PeopleService */; }; 5133B6702716320F00C95463 /* ContactKeyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */; }; @@ -99,6 +99,7 @@ 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; }; + 51592FB528D9B03600FE9ACC /* MessageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */; }; 515B7C2528119CED0066AB4F /* GMP in Frameworks */ = {isa = PBXBuildFile; productRef = 515B7C2428119CED0066AB4F /* GMP */; }; 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */; }; 51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */; }; @@ -116,6 +117,8 @@ 51B7421B27F318D300E702C8 /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B7421A27F318D300E702C8 /* XCTestCaseExtension.swift */; }; 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */; }; 51C0C1EF271982A1000C9738 /* MailCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1EE271982A1000C9738 /* MailCore */; }; + 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */; }; + 51CE196728D8AC5300A5B200 /* ComposeMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */; }; 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD52721AB07001C4359 /* PubKeyState.swift */; }; 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD92722C82E001C4359 /* RecipientTests.swift */; }; 51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */; }; @@ -128,6 +131,7 @@ 51E1675D270F36A400D27C52 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675C270F36A400D27C52 /* Realm */; }; 51E1675F270F36A400D27C52 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675E270F36A400D27C52 /* RealmSwift */; }; 51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */; }; + 51FC336128C236770098313D /* ComposeViewController+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */; }; 5298EA408FEC36021F7558BD /* Pods_FlowCrypt.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4753E9A27694B4D34C980FFA /* Pods_FlowCrypt.framework */; }; 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42C239EC321001F4607 /* SettingsViewController.swift */; }; 5A39F430239EC396001F4607 /* SettingsViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */; }; @@ -178,7 +182,6 @@ 9F3861A227A18AAF00851419 /* .swiftformat in Resources */ = {isa = PBXBuildFile; fileRef = 9F3861A127A18AAF00851419 /* .swiftformat */; }; 9F3861A427A18ABA00851419 /* .swiftformat in Resources */ = {isa = PBXBuildFile; fileRef = 9F3861A327A18ABA00851419 /* .swiftformat */; }; 9F3EF32523B15C1400FA0CEF /* ImapHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */; }; - 9F3EF33123B1785600FA0CEF /* MsgListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */; }; 9F4163B8265ED61C00106194 /* SetupInitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */; }; 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163E5266520B600106194 /* CommonNodesInputs.swift */; }; 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F416427266575DC00106194 /* BackupServiceType.swift */; }; @@ -224,7 +227,7 @@ 9F7E8F19269C538E0021C07F /* NavigationChildController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E8F18269C538E0021C07F /* NavigationChildController.swift */; }; 9F7E903926A1AD7A0021C07F /* KeyDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E903826A1AD7A0021C07F /* KeyDetailsTests.swift */; }; 9F7ECCA7272C3FB4008A1770 /* TextImageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */; }; - 9F7ECCA9272C47DB008A1770 /* InboxRenderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */; }; + 9F7ECCA9272C47DB008A1770 /* InboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */; }; 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */; }; 9F883912271F242900669B56 /* ThreadDetailsDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F883911271F242900669B56 /* ThreadDetailsDecorator.swift */; }; 9F8839142721EB5000669B56 /* MessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8839132721EB5000669B56 /* MessageAction.swift */; }; @@ -333,7 +336,7 @@ D2531F2F23FEEF52007E5198 /* FlowCryptCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = D2531F2D23FEEF52007E5198 /* FlowCryptCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2531F3423FEEF5F007E5198 /* StyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56BD3923438D3700A7371A /* StyleExtensions.swift */; }; D2531F3723FFF043007E5198 /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C2723194E8500299985 /* CommonExtensions.swift */; }; - D2531F3D24000E37007E5198 /* UIVIewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */; }; + D2531F3D24000E37007E5198 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */; }; D2531F3F24000E57007E5198 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA0E63F2F0473D0A8EDB0 /* StringExtensions.swift */; }; D2531F462402C62D007E5198 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2531F452402C62D007E5198 /* CollectionExtensions.swift */; }; D2531F472402C9DE007E5198 /* IntExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37A1CF523C6254F001CF774 /* IntExtensions.swift */; }; @@ -405,7 +408,6 @@ D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6965243115EC007182F0 /* SetupImapViewController.swift */; }; D2FF6968243115F9007182F0 /* SetupImapViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */; }; F191F621272511790053833E /* BlurViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F191F620272511790053833E /* BlurViewController.swift */; }; - F80E95362720B6640093F243 /* DraftsListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80E95352720B6640093F243 /* DraftsListProvider.swift */; }; F8678DCC2722143300BB1710 /* GmailService+draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8678DCB2722143300BB1710 /* GmailService+draft.swift */; }; F8A72FA12729F82800E4BCAB /* DraftGatewayMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */; }; /* End PBXBuildFile section */ @@ -530,7 +532,7 @@ 32DCA38E87F2B7196E0E1F1F /* CodableExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableExtensions.swift; sourceTree = ""; }; 32DCA4B11D4531B3B04D01D1 /* AppErr.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppErr.swift; sourceTree = ""; }; 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Imap+msg.swift"; sourceTree = ""; }; - 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIVIewExtensions.swift; sourceTree = ""; }; + 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 32DCA7E0AFE19FACB0F233ED /* URLSessionExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionExtensions.swift; sourceTree = ""; }; 32DCA9701B2D5052225A0414 /* SignInViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 32DCAAE9F459F48178CAF8F5 /* ComposeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; @@ -556,7 +558,7 @@ 51114DBA280EBB78006C252A /* gmp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gmp.h; sourceTree = ""; }; 5113D22C28C0C43700A131E0 /* Gmail+MessageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+MessageExtension.swift"; sourceTree = ""; }; 511737672851F31700337B9F /* GoogleScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleScope.swift; sourceTree = ""; }; - 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePasswordCellNode.swift; sourceTree = ""; }; + 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionCellNode.swift; sourceTree = ""; }; 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonWithPaddingNode.swift; sourceTree = ""; }; 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailViewController.swift; sourceTree = ""; }; 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailDecorator.swift; sourceTree = ""; }; @@ -567,6 +569,7 @@ 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = ""; }; + 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIdentifier.swift; sourceTree = ""; }; 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientEmailTextFieldNode.swift; sourceTree = ""; }; 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessage.swift; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; @@ -583,6 +586,8 @@ 51B4AE5227144E590001F33B /* PubKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKey.swift; sourceTree = ""; }; 51B7421A27F318D300E702C8 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRecipientsNode.swift; sourceTree = ""; }; + 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewController+ErrorHandling.swift"; sourceTree = ""; }; + 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageAction.swift; sourceTree = ""; }; 51DA5BD52721AB07001C4359 /* PubKeyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyState.swift; sourceTree = ""; }; 51DA5BD92722C82E001C4359 /* RecipientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientTests.swift; sourceTree = ""; }; 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeNode.swift; sourceTree = ""; }; @@ -591,6 +596,7 @@ 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 51E4F0B427348E310017DABB /* ErrorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorExtensions.swift; sourceTree = ""; }; 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithIconNode.swift; sourceTree = ""; }; + 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewController+Contacts.swift"; sourceTree = ""; }; 55652F68438D6EDFE71EA13C /* Pods-FlowCryptUIApplication.enterprise.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.enterprise.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.enterprise.xcconfig"; sourceTree = ""; }; 5A39F42C239EC321001F4607 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDecorator.swift; sourceTree = ""; }; @@ -653,7 +659,6 @@ 9F3861A327A18ABA00851419 /* .swiftformat */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftformat; sourceTree = ""; }; 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapHelper.swift; sourceTree = ""; }; 9F3EF32923B15C9500FA0CEF /* ImapHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapHelperTest.swift; sourceTree = ""; }; - 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgListViewController.swift; sourceTree = ""; }; 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupInitialViewController.swift; sourceTree = ""; }; 9F4163E5266520B600106194 /* CommonNodesInputs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonNodesInputs.swift; sourceTree = ""; }; 9F416427266575DC00106194 /* BackupServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceType.swift; sourceTree = ""; }; @@ -710,7 +715,7 @@ 9F7E8F18269C538E0021C07F /* NavigationChildController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationChildController.swift; sourceTree = ""; }; 9F7E903826A1AD7A0021C07F /* KeyDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailsTests.swift; sourceTree = ""; }; 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextImageNode.swift; sourceTree = ""; }; - 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxRenderable.swift; sourceTree = ""; }; + 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxItem.swift; sourceTree = ""; }; 9F8220D426336626004B2009 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 9F8277952373732000E19C07 /* UIImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextSubjectNode.swift; sourceTree = ""; }; @@ -859,7 +864,6 @@ D9381A4EE6BAAD97294A7F9A /* Pods-FlowCryptUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUI.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUI/Pods-FlowCryptUI.release.xcconfig"; sourceTree = ""; }; E26D5E20275AA417007B8802 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; F191F620272511790053833E /* BlurViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurViewController.swift; sourceTree = ""; }; - F80E95352720B6640093F243 /* DraftsListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsListProvider.swift; sourceTree = ""; }; F8678DCB2722143300BB1710 /* GmailService+draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GmailService+draft.swift"; sourceTree = ""; }; F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftGatewayMock.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -925,18 +929,20 @@ 049E607227FDBCC30089EE2A /* Extensions */ = { isa = PBXGroup; children = ( + 049E606C27FDBB5B0089EE2A /* ComposeViewController+ActionHandling.swift */, 049E605827FDB6310089EE2A /* ComposeViewController+Attachment.swift */, - 049E605A27FDB6BE0089EE2A /* ComposeViewController+TableView.swift */, - 049E605C27FDB7D10089EE2A /* ComposeViewController+Nodes.swift */, - 049E605E27FDB8500089EE2A /* ComposeViewController+MessageSend.swift */, - 049E606027FDB9370089EE2A /* ComposeViewController+Setup.swift */, + 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */, + 049E607027FDBC690089EE2A /* ComposeViewController+Drafts.swift */, + 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */, 049E606227FDB9C70089EE2A /* ComposeViewController+Keyboard.swift */, - 049E606427FDBA9F0089EE2A /* ComposeViewController+State.swift */, + 049E605E27FDB8500089EE2A /* ComposeViewController+MessageSend.swift */, + 049E605C27FDB7D10089EE2A /* ComposeViewController+Nodes.swift */, + 049E606E27FDBBD50089EE2A /* ComposeViewController+Picker.swift */, 049E606627FDBAEF0089EE2A /* ComposeViewController+RecipientInput.swift */, 049E606827FDBB2D0089EE2A /* ComposeViewController+RecipientPopup.swift */, - 049E606C27FDBB5B0089EE2A /* ComposeViewController+ActionHandling.swift */, - 049E606E27FDBBD50089EE2A /* ComposeViewController+Picker.swift */, - 049E607027FDBC690089EE2A /* ComposeViewController+Drafts.swift */, + 049E606027FDB9370089EE2A /* ComposeViewController+Setup.swift */, + 049E606427FDBA9F0089EE2A /* ComposeViewController+State.swift */, + 049E605A27FDB6BE0089EE2A /* ComposeViewController+TableView.swift */, 049E607327FDBDBE0089EE2A /* ComposeViewController+TapActions.swift */, ); path = Extensions; @@ -957,11 +963,12 @@ 04B4728F1ECE29F600B8266F /* Compose */ = { isa = PBXGroup; children = ( + 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */, 32DCAAE9F459F48178CAF8F5 /* ComposeViewController.swift */, - 049E607227FDBCC30089EE2A /* Extensions */, D269E02624103A20000495C3 /* ComposeViewControllerInput.swift */, 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */, 042B140127F596C70018BDC4 /* ComposeRecipientPopupViewController.swift */, + 049E607227FDBCC30089EE2A /* Extensions */, ); path = Compose; sourceTree = ""; @@ -1693,7 +1700,6 @@ 9FC4114A25961CD6001180A8 /* Mail Provider */ = { isa = PBXGroup; children = ( - F80E95342720B64A0093F243 /* DraftsListProvider */, 9FF0671B25520D9D00FCC9E6 /* MailProvider.swift */, 9FC4114B25961CEA001180A8 /* MailServiceProviderType.swift */, 9FF0670E25520D4B00FCC9E6 /* Gmail */, @@ -1767,6 +1773,7 @@ 9FE1B39F2565B0CD00D6D086 /* Message.swift */, 9F5C2A76257D705100DE9B4B /* MessageLabel.swift */, 51938DC0274CC291007AD57B /* MessageQuoteType.swift */, + 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */, ); path = Model; sourceTree = ""; @@ -1915,7 +1922,6 @@ 04B4728F1ECE29F600B8266F /* Compose */, D29AFFF02409300600C1387D /* Search */, 5A39F42E239EC32B001F4607 /* Settings */, - D29A0001240C137700C1387D /* MessageList Extension */, 9F778E7F271620AF001D4B21 /* Threads */, ); path = Controllers; @@ -1938,7 +1944,7 @@ C132B9D71EC30E0B00763715 /* Inbox */ = { isa = PBXGroup; children = ( - 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */, + 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */, C132B9D81EC30E1D00763715 /* InboxViewController.swift */, 9FAFD75A2713880300321FA4 /* InboxViewController+Factory.swift */, 9FAFD7582713870800321FA4 /* InboxViewController+State.swift */, @@ -2068,7 +2074,7 @@ 9F31AB9D232BF2A600CF87EA /* UIColorExtensions.swift */, 9FD505262785C2CD00FAA82F /* UIDeviceExtensions.swift */, 9FEED1B7230C08D700700F8E /* UIViewControllerExtensions.swift */, - 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */, + 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */, 9F8277952373732000E19C07 /* UIImageExtensions.swift */, 9FD5052A278B2C8600FAA82F /* UIPopoverPresentationControllerExtensions.swift */, 32DCA7E0AFE19FACB0F233ED /* URLSessionExtensions.swift */, @@ -2101,14 +2107,6 @@ path = Views; sourceTree = ""; }; - D29A0001240C137700C1387D /* MessageList Extension */ = { - isa = PBXGroup; - children = ( - 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */, - ); - path = "MessageList Extension"; - sourceTree = ""; - }; D29A0002240C140500C1387D /* Key Detail Info */ = { isa = PBXGroup; children = ( @@ -2185,7 +2183,7 @@ 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 04CF208127F5ABE100BC4E3D /* ComposeRecipientPopupNameNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, - 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */, + 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */, 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, @@ -2282,14 +2280,6 @@ path = SetupImap; sourceTree = ""; }; - F80E95342720B64A0093F243 /* DraftsListProvider */ = { - isa = PBXGroup; - children = ( - F80E95352720B6640093F243 /* DraftsListProvider.swift */, - ); - path = DraftsListProvider; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2335,7 +2325,6 @@ buildConfigurationList = C132B9C21EC2DBD800763715 /* Build configuration list for PBXNativeTarget "FlowCrypt" */; buildPhases = ( 85C230714DC341D246864AD0 /* [CP] Check Pods Manifest.lock */, - D2324CAA2471D47B00FD1C6F /* Format code */, D2324CA92471CED200FD1C6F /* SwiftLint */, C132B9AC1EC2DBD800763715 /* Sources */, C132B9AD1EC2DBD800763715 /* Frameworks */, @@ -2457,7 +2446,7 @@ }; }; buildConfigurationList = C132B9AB1EC2DBD800763715 /* Build configuration list for PBXProject "FlowCrypt" */; - compatibilityVersion = "Xcode 12.0"; + compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -2654,24 +2643,6 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - D2324CAA2471D47B00FD1C6F /* Format code */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Format code"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n# swift package update\n# Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --exclude appium,fastlane,Pods,vendor\n"; - }; E531C3B50A9C90454C72F878 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2788,7 +2759,7 @@ 2CC50FB12744167A0051629A /* Folder.swift in Sources */, 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */, D2F41373243CC7990066AFB5 /* UserRealmObject.swift in Sources */, - F80E95362720B6640093F243 /* DraftsListProvider.swift in Sources */, + 51CE196728D8AC5300A5B200 /* ComposeMessageAction.swift in Sources */, 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */, 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */, 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, @@ -2847,7 +2818,6 @@ 04722B45281ABD2C00D0242F /* EKMVcHelper.swift in Sources */, 9F6F3BEF26ADF5DE005BD9C6 /* ComposeMessageError.swift in Sources */, 5ADEDCAF23A3EA9E00EC495E /* KeySettingsViewDecorator.swift in Sources */, - 9F3EF33123B1785600FA0CEF /* MsgListViewController.swift in Sources */, 9F31ABA0232C071700CF87EA /* GlobalRouter.swift in Sources */, 9F5C2A92257E94DF00DE9B4B /* Imap+MessageOperations.swift in Sources */, D27B911F24EFE828002DF0A1 /* RecipientWithSortedPubKeys.swift in Sources */, @@ -2861,17 +2831,19 @@ 215897E8267A553300423694 /* FilesManager.swift in Sources */, 9F5C2A77257D705100DE9B4B /* MessageLabel.swift in Sources */, 9F93623F2573D16F0009912F /* Gmail+Message.swift in Sources */, - 9F7ECCA9272C47DB008A1770 /* InboxRenderable.swift in Sources */, + 9F7ECCA9272C47DB008A1770 /* InboxItem.swift in Sources */, 042B140227F596C70018BDC4 /* ComposeRecipientPopupViewController.swift in Sources */, 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 049E606327FDB9C70089EE2A /* ComposeViewController+Keyboard.swift in Sources */, 5A948DC5239EF2F4006284D7 /* LegalViewController.swift in Sources */, + 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */, 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */, A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */, 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */, D2891AC224C59EFA008918E3 /* KeyAndPassPhraseStorage.swift in Sources */, D269E02724103A20000495C3 /* ComposeViewControllerInput.swift in Sources */, 510BB63527BE92CC00B1011F /* RecipientBase.swift in Sources */, + 51592FB528D9B03600FE9ACC /* MessageIdentifier.swift in Sources */, 9FAFD75D2714A06400321FA4 /* InboxProviders.swift in Sources */, 2C141B2C274572D50038A3F8 /* Recipient.swift in Sources */, 9F0C3C142316E69300299985 /* User.swift in Sources */, @@ -2916,6 +2888,7 @@ 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */, D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */, 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */, + 51FC336128C236770098313D /* ComposeViewController+Contacts.swift in Sources */, 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */, 9FEED1D2230DAD1E00700F8E /* InboxViewModel.swift in Sources */, 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */, @@ -2997,7 +2970,7 @@ D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, - 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */, + 511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */, D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */, D2E26F7224F26FFF00612AF1 /* ContactUserCellNode.swift in Sources */, D211CE6F23FC358000D1CE38 /* ButtonNode.swift in Sources */, @@ -3064,7 +3037,7 @@ D2CDC3D42402D50A002B045F /* CodableExtensions.swift in Sources */, 9FD505272785C2CD00FAA82F /* UIDeviceExtensions.swift in Sources */, 750A6C3D28244A780048E1CC /* OptionalExtensions.swift in Sources */, - D2531F3D24000E37007E5198 /* UIVIewExtensions.swift in Sources */, + D2531F3D24000E37007E5198 /* UIViewExtensions.swift in Sources */, D2531F3723FFF043007E5198 /* CommonExtensions.swift in Sources */, D2CDC3D32402D4FE002B045F /* DataExtensions.swift in Sources */, 759739BE2833E986004867CD /* Task+Retry.swift in Sources */, @@ -3587,7 +3560,7 @@ CODE_SIGN_ENTITLEMENTS = FlowCrypt/FlowCrypt.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6DZ6CC3YMY; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; @@ -3651,7 +3624,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6DZ6CC3YMY; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 36e18bdc1..8b6b9e5da 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "ae4278abe1fcdcd616b410e098a744c1cc73e0bd", - "version" : "10.31.0" + "revision" : "7055d820a133da4395742758bd48bab0841cc4bf", + "version" : "10.32.0" } }, { diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift index 67f321260..c1384fec7 100644 --- a/FlowCrypt/App/AppContext.swift +++ b/FlowCrypt/App/AppContext.swift @@ -3,10 +3,9 @@ // FlowCrypt // // Created by Tom on 30.11.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// Copyright © 2017-present FlowCrypt a.s. All rights reserved. // -import Foundation import UIKit class AppContext { @@ -105,26 +104,26 @@ class AppContext { @MainActor func getBackupService() throws -> BackupService { - let mailProvider = try self.getRequiredMailProvider() + let mailProvider = try getRequiredMailProvider() return BackupService( backupProvider: try mailProvider.backupProvider, - messageSender: try mailProvider.messageSender + messageGateway: try mailProvider.messageGateway ) } @MainActor func getFoldersService() throws -> FoldersService { return FoldersService( - encryptedStorage: self.encryptedStorage, - remoteFoldersProvider: try self.getRequiredMailProvider().remoteFoldersProvider + encryptedStorage: encryptedStorage, + remoteFoldersProvider: try getRequiredMailProvider().remoteFoldersProvider ) } @MainActor func getSendAsService() throws -> SendAsService { return SendAsService( - encryptedStorage: self.encryptedStorage, - remoteSendAsProvider: try self.getRequiredMailProvider().remoteSendAsProvider + encryptedStorage: encryptedStorage, + remoteSendAsProvider: try getRequiredMailProvider().remoteSendAsProvider ) } } diff --git a/FlowCrypt/App/AppDelegate.swift b/FlowCrypt/App/AppDelegate.swift index ff9d63133..064c3e5fc 100644 --- a/FlowCrypt/App/AppDelegate.swift +++ b/FlowCrypt/App/AppDelegate.swift @@ -4,9 +4,7 @@ // import AppAuth -import UIKit import GTMAppAuth -import FlowCryptCommon import Combine @main diff --git a/FlowCrypt/App/AppStartup.swift b/FlowCrypt/App/AppStartup.swift index e41075586..0a1bd9a05 100644 --- a/FlowCrypt/App/AppStartup.swift +++ b/FlowCrypt/App/AppStartup.swift @@ -134,7 +134,7 @@ struct AppStartup { guard let mailProvider = try await appContext.getOptionalMailProvider() else { return } - return try await mailProvider.sessionProvider.renewSession() + try await mailProvider.sessionProvider.renewSession() } @MainActor @@ -247,8 +247,8 @@ struct AppStartup { Task { do { try await appContext.globalRouter.signOut(appContext: appContext) - } catch let logoutError { - Logger.logError("Logout failed due to \(logoutError.localizedDescription)") + } catch { + Logger.logError("Logout failed due to \(error.errorMessage)") } } } diff --git a/FlowCrypt/App/GlobalRouter.swift b/FlowCrypt/App/GlobalRouter.swift index 1ff283393..3bbff6f20 100644 --- a/FlowCrypt/App/GlobalRouter.swift +++ b/FlowCrypt/App/GlobalRouter.swift @@ -2,7 +2,7 @@ // GlobalRouter.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 9/13/19. +// Created by Anton Kharchevskyi on 9/13/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // @@ -75,7 +75,7 @@ extension GlobalRouter: GlobalRouterType { keyWindow.rootViewController?.showAlert( title: "error".localized, message: message, - onOk: { fatalError() } + onOk: { fatalError(message) } ) return diff --git a/FlowCrypt/Common UI/AttachmentManager.swift b/FlowCrypt/Common UI/AttachmentManager.swift index c4bcaddad..7471efd05 100644 --- a/FlowCrypt/Common UI/AttachmentManager.swift +++ b/FlowCrypt/Common UI/AttachmentManager.swift @@ -25,22 +25,14 @@ final class AttachmentManager: NSObject { self.filesManager = filesManager } + @MainActor private func showFileSharedAlert(with url: URL) { - let alert = UIAlertController( + controller?.showAlertWithAction( title: "message_attachment_saved_successfully_title".localized, message: "message_attachment_saved_successfully_message".localized, - preferredStyle: .alert + actionButtonTitle: "open".localized, + onAction: { _ in UIApplication.shared.open(url) } ) - - let cancel = UIAlertAction(title: "cancel".localized, style: .cancel) { _ in } - let open = UIAlertAction(title: "open".localized, style: .default) { _ in - UIApplication.shared.open(url) - } - - alert.addAction(cancel) - alert.addAction(open) - - controller?.present(alert, animated: true) } @MainActor diff --git a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift index 852290867..82292a496 100644 --- a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift +++ b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift @@ -5,7 +5,7 @@ // Created by Parag Dulam on 24/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation + import UIKit final class BlurViewController: UIViewController { @@ -16,6 +16,7 @@ final class BlurViewController: UIViewController { self.blurView = UIVisualEffectView(effect: blurEffect) super.init(nibName: nil, bundle: nil) } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift index df8f526ff..fb52c4753 100644 --- a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift +++ b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift @@ -102,7 +102,7 @@ extension InvalidStorageViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ node: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self, let part = Parts(rawValue: indexPath.row) else { + guard let self, let part = Parts(rawValue: indexPath.row) else { return ASCellNode() } diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index 67b028654..6cac1b56f 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon import FlowCryptUI import UIKit diff --git a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift new file mode 100644 index 000000000..9d5a38c10 --- /dev/null +++ b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift @@ -0,0 +1,15 @@ +// +// ComposeMessageAction.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 19/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +enum ComposeMessageAction { + case update(MessageIdentifier), + delete(MessageIdentifier), + sent(MessageIdentifier) +} diff --git a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift index ef4fd6190..b8be5b130 100644 --- a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import AsyncDisplayKit import FlowCryptUI @@ -25,12 +24,12 @@ final class ComposeRecipientPopupViewController: TableNodeViewController { case nameEmail, divider, copy, edit, remove } - var parts: [Parts] { - return [.nameEmail, .divider, .copy, .edit, .divider, .remove] + private var parts: [Parts] { + [.nameEmail, .divider, .copy, .edit, .divider, .remove] } private let recipient: ComposeMessageRecipient - internal let type: RecipientType + let type: RecipientType var delegate: ComposeRecipientPopupViewControllerProtocol? init( diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ebfd8aa5c..8428927a9 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -6,7 +6,6 @@ import AsyncDisplayKit import Combine import FlowCryptCommon import FlowCryptUI -import Foundation /** * View controller to compose the message and send it @@ -15,18 +14,12 @@ import Foundation **/ final class ComposeViewController: TableNodeViewController { - internal enum Constants { + enum Constants { static let endTypingCharacters = [",", "\n", ";"] static let minRecipientsPartHeight: CGFloat = 32 } - internal struct ComposedDraft: Equatable { - let email: String - let input: ComposeMessageInput - let contextToSend: ComposeMessageContext - } - - internal enum State { + enum State { case main, searchEmails([Recipient]) } @@ -34,7 +27,7 @@ final class ComposeViewController: TableNodeViewController { case recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts static var recipientsSections: [Section] { - RecipientType.allCases.map { Section.recipients($0) } + RecipientType.allCases.map { Self.recipients($0) } } } @@ -42,67 +35,73 @@ final class ComposeViewController: TableNodeViewController { case delete, reload, add, scrollToBottom } - internal enum ComposePart: Int, CaseIterable { + enum ComposePart: Int, CaseIterable { case topDivider, subject, subjectDivider, text } - internal var shouldDisplaySearchResult = false - internal var userTappedOutSideRecipientsArea = false - internal var shouldShowEmailRecipientsLabel = false - internal let appContext: AppContextWithUser - internal let composeMessageService: ComposeMessageService - internal var decorator: ComposeViewDecorator - internal let localContactsProvider: LocalContactsProviderType - internal let pubLookup: PubLookupType - internal let googleUserService: GoogleUserServiceType - internal let filesManager: FilesManagerType - internal let photosManager: PhotosManagerType - internal let router: GlobalRouterType - internal let clientConfiguration: ClientConfiguration - internal let sendAsService: SendAsServiceType + var shouldDisplaySearchResult = false + var userTappedOutSideRecipientsArea = false + var shouldShowEmailRecipientsLabel = false + let appContext: AppContextWithUser + let composeMessageService: ComposeMessageService + var decorator: ComposeViewDecorator + let localContactsProvider: LocalContactsProviderType + let messageService: MessageService + let pubLookup: PubLookupType + let googleUserService: GoogleUserServiceType + let filesManager: FilesManagerType + let photosManager: PhotosManagerType + let router: GlobalRouterType - internal var isMessagePasswordSupported: Bool { - return clientConfiguration.isUsingFes - } + private let clientConfiguration: ClientConfiguration + var isMessagePasswordSupported: Bool { clientConfiguration.isUsingFes } - internal let search = PassthroughSubject() - internal var cancellable = Set() + let search = PassthroughSubject() + var cancellable = Set() - internal var input: ComposeMessageInput - internal var contextToSend = ComposeMessageContext() + var input: ComposeMessageInput + var contextToSend: ComposeMessageContext - internal var state: State = .main - internal var shouldEvaluateRecipientInput = true + var state: State = .main + var shouldEvaluateRecipientInput = true - internal weak var saveDraftTimer: Timer? - internal var composedLatestDraft: ComposedDraft? + weak var saveDraftTimer: Timer? + var composedLatestDraft: ComposedDraft? - internal lazy var alertsFactory = AlertsFactory() - internal var messagePasswordAlertController: UIAlertController? - internal var didLayoutSubviews = false - internal var topContentInset: CGFloat { + var messagePasswordAlertController: UIAlertController? + lazy var alertsFactory = AlertsFactory() + + var didFinishSetup = false { + didSet { + if didFinishSetup { setupTextNode() } + } + } + private var didLayoutSubviews = false + private var topContentInset: CGFloat { navigationController?.navigationBar.frame.maxY ?? 0 } - internal var selectedRecipientType: RecipientType? = .to - internal var shouldShowAllRecipientTypes = false - internal var popoverVC: ComposeRecipientPopupViewController! + var selectedRecipientType: RecipientType? = .to + var shouldShowAllRecipientTypes = false + var popoverVC: ComposeRecipientPopupViewController! - internal var sectionsList: [Section] = [] - var composeTextNode: ASCellNode! - var composeSubjectNode: ASCellNode! - var fromCellNode: RecipientFromCellNode! + var sectionsList: [Section] = [] + var composeTextNode: ASCellNode? + var composeSubjectNode: ASCellNode? var sendAsList: [SendAsModel] = [] - var selectedFromEmail = "" + + let handleAction: ((ComposeMessageAction) -> Void)? init( appContext: AppContextWithUser, decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, composeMessageService: ComposeMessageService? = nil, + messageService: MessageService? = nil, filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), - keyMethods: KeyMethodsType = KeyMethods() + keyMethods: KeyMethodsType = KeyMethods(), + handleAction: ((ComposeMessageAction) -> Void)? = nil ) async throws { self.appContext = appContext self.input = input @@ -117,10 +116,18 @@ final class ComposeViewController: TableNodeViewController { appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate, shouldRunWarmupQuery: true ) - self.composeMessageService = composeMessageService ?? ComposeMessageService( - appContext: appContext, - keyMethods: keyMethods - ) + let draftGateway = try appContext.getRequiredMailProvider().draftGateway + + if let composeMessageService = composeMessageService { + self.composeMessageService = composeMessageService + } else { + self.composeMessageService = ComposeMessageService( + appContext: appContext, + keyMethods: keyMethods, + draftGateway: draftGateway + ) + } + self.filesManager = filesManager self.photosManager = photosManager self.pubLookup = PubLookup( @@ -128,13 +135,27 @@ final class ComposeViewController: TableNodeViewController { localContactsProvider: self.localContactsProvider ) self.router = appContext.globalRouter - self.contextToSend.subject = input.subject - self.contextToSend.attachments = input.attachments self.clientConfiguration = clientConfiguration - self.sendAsService = try appContext.getSendAsService() - self.sendAsList = try await sendAsService.fetchList(isForceReload: false, for: appContext.user) - self.sendAsList = self.sendAsList.filter { $0.verificationStatus == .accepted || $0.isDefault } - self.selectedFromEmail = appContext.user.email + + let mailProvider = try appContext.getRequiredMailProvider() + self.messageService = try messageService ?? MessageService( + localContactsProvider: localContactsProvider, + pubLookup: PubLookup(clientConfiguration: clientConfiguration, localContactsProvider: localContactsProvider), + keyAndPassPhraseStorage: appContext.keyAndPassPhraseStorage, + messageProvider: try mailProvider.messageProvider, + combinedPassPhraseStorage: appContext.combinedPassPhraseStorage + ) + + self.sendAsList = try await appContext.getSendAsService() + .fetchList(isForceReload: false, for: appContext.user) + .filter { $0.verificationStatus == .accepted || $0.isDefault } + + self.contextToSend = ComposeMessageContext( + sender: appContext.user.email, + subject: input.subject, + attachments: input.attachments + ) + self.handleAction = handleAction super.init(node: TableNode()) } @@ -148,23 +169,24 @@ final class ComposeViewController: TableNodeViewController { setupUI() setupNavigationBar() - setupNodes() + setupSubjectNode() observeKeyboardNotifications() observerAppStates() observeComposeUpdates() - setupQuote() + fillDataFromInput() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) node.view.endEditing(true) - stopDraftTimer() + stopDraftTimer(withSave: false) + navigationController?.interactivePopGestureRecognizer?.isEnabled = true } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - startDraftTimer() + startDraftTimer(withFire: true) guard shouldEvaluateRecipientInput else { shouldEvaluateRecipientInput = true @@ -191,29 +213,15 @@ final class ComposeViewController: TableNodeViewController { NotificationCenter.default.removeObserver(self) } - func update(with message: Message) { - self.contextToSend.subject = message.subject - self.contextToSend.message = message.raw - for recipient in message.to { - evaluateMessage(recipient: recipient, type: .to) - } - for recipient in message.cc { - evaluateMessage(recipient: recipient, type: .cc) - } - for recipient in message.bcc { - evaluateMessage(recipient: recipient, type: .bcc) - } - } - - func evaluateMessage(recipient: Recipient, type: RecipientType) { - let recipient = ComposeMessageRecipient( + func add(recipient: Recipient, type: RecipientType) { + let composeRecipient = ComposeMessageRecipient( email: recipient.email, name: recipient.name, type: type, state: decorator.recipientIdleState ) - contextToSend.add(recipient: recipient) - evaluate(recipient: recipient) + contextToSend.add(recipient: composeRecipient) + evaluate(recipient: composeRecipient) } private func observeComposeUpdates() { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 4a88f9a3a..3c2647a7b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -12,15 +12,20 @@ struct ComposeMessageInput: Equatable { static let empty = ComposeMessageInput(type: .idle) struct MessageQuoteInfo: Equatable { + let id: Identifier? let recipients: [Recipient] let ccRecipients: [Recipient] + let bccRecipients: [Recipient] let sender: Recipient? let subject: String? let sentDate: Date - let message: String + let text: String let threadId: String? let replyToMsgId: String? let inReplyTo: String? + let rfc822MsgId: String? + let draftId: Identifier? + let shouldEncrypt: Bool let attachments: [MessageAttachment] } @@ -28,26 +33,21 @@ struct ComposeMessageInput: Equatable { case idle case reply(MessageQuoteInfo) case forward(MessageQuoteInfo) + case draft(MessageQuoteInfo) } let type: InputType - var quoteRecipients: [Recipient] { - guard case .reply(let info) = type else { - return [] - } - return info.recipients + var subject: String? { + type.info?.subject } - var quoteCCRecipients: [Recipient] { - guard case .reply(let info) = type else { - return [] - } - return info.ccRecipients + var text: String? { + type.info?.text } - var subject: String? { - type.info?.subject + var isPgp: Bool { + text?.isPgp ?? false } var replyToMsgId: String? { @@ -70,7 +70,7 @@ struct ComposeMessageInput: Equatable { extension ComposeMessageInput { var successfullySentToast: String { switch type { - case .idle: + case .idle, .draft: return "compose_encrypted_sent".localized case .forward: return "compose_forward_successful".localized @@ -82,14 +82,21 @@ extension ComposeMessageInput { extension ComposeMessageInput { var isQuote: Bool { - type != .idle + switch type { + case .reply, .forward: + return true + case .idle, .draft: + return false + } } - var isReply: Bool { - guard case .reply = type else { + var shouldFocusTextNode: Bool { + switch type { + case .reply: + return true + case .idle, .forward, .draft: return false } - return true } } @@ -98,8 +105,28 @@ extension ComposeMessageInput.InputType { switch self { case .idle: return nil - case .reply(let info), .forward(let info): + case .reply(let info), .forward(let info), .draft(let info): return info } } } + +extension ComposeMessageInput.MessageQuoteInfo { + init(message: Message, processed: ProcessedMessage? = nil) { + self.id = message.identifier + self.recipients = message.to + self.ccRecipients = message.cc + self.bccRecipients = message.bcc + self.sender = message.sender + self.subject = message.subject + self.sentDate = message.date + self.text = processed?.text ?? message.body.text + self.threadId = message.threadId + self.rfc822MsgId = message.rfc822MsgId + self.draftId = message.draftId + self.replyToMsgId = message.replyToMsgId + self.inReplyTo = message.inReplyTo + self.shouldEncrypt = message.isPgp + self.attachments = processed?.attachments ?? message.attachments + } +} diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 4faf8a72f..c54f0dd8a 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -107,7 +107,7 @@ struct ComposeViewDecorator { guard let info = input.type.info else { return NSAttributedString(string: "") } let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short + dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .none let date = dateFormatter.string(from: info.sentDate) @@ -116,30 +116,32 @@ struct ComposeViewDecorator { dateFormatter.timeStyle = .short let time = dateFormatter.string(from: info.sentDate) - let from = info.sender?.email ?? "unknown sender" + let from = info.sender?.formatted ?? "unknown sender" - let text: String = "\n\n" + let text = "\n\n" + "compose_quote_from".localizeWithArguments(date, time, from) + "\n" - let message = " > " + info.message.replacingOccurrences(of: "\n", with: "\n > ") + let message = " > " + info.text.replacingOccurrences(of: "\n", with: "\n > ") return (text + message).attributed(.regular(17)) } - func styledEmptyMessagePasswordInput() -> MessagePasswordCellNode.Input { - messagePasswordInput( + func styledEmptyMessagePasswordInput() -> MessageActionCellNode.Input { + messageActionInput( text: "compose_password_placeholder".localized, color: .warningColor, - imageName: "lock" + imageName: "lock", + accessibilityIdentifier: "aid-message-password-cell" ) } - func styledFilledMessagePasswordInput() -> MessagePasswordCellNode.Input { - messagePasswordInput( + func styledFilledMessagePasswordInput() -> MessageActionCellNode.Input { + messageActionInput( text: "compose_password_set_message".localized, color: .main, - imageName: "checkmark.circle" + imageName: "checkmark.circle", + accessibilityIdentifier: "aid-message-password-cell" ) } @@ -161,7 +163,7 @@ struct ComposeViewDecorator { type: RecipientType, completion: (() -> Void)? = nil ) { - let currentHeight = self.recipientsNodeHeight(type: type) + let currentHeight = recipientsNodeHeight(type: type) guard currentHeight != layoutHeight, layoutHeight > 0 else { return @@ -169,26 +171,28 @@ struct ComposeViewDecorator { switch type { case .to: - self.calculatedRecipientsToPartHeight = layoutHeight + calculatedRecipientsToPartHeight = layoutHeight case .cc: - self.calculatedRecipientsCcPartHeight = layoutHeight + calculatedRecipientsCcPartHeight = layoutHeight case .bcc: - self.calculatedRecipientsBccPartHeight = layoutHeight + calculatedRecipientsBccPartHeight = layoutHeight default: break } completion?() } - private func messagePasswordInput( + private func messageActionInput( text: String, color: UIColor, - imageName: String - ) -> MessagePasswordCellNode.Input { + imageName: String, + accessibilityIdentifier: String? + ) -> MessageActionCellNode.Input { .init( text: text.attributed(.regular(14), color: color), color: color, - image: UIImage(systemName: imageName)?.tinted(color) + image: UIImage(systemName: imageName)?.tinted(color), + accessibilityIdentifier: accessibilityIdentifier ) } @@ -244,7 +248,7 @@ extension ComposeViewDecorator { backgroundColor: .titleNodeBackgroundColor, borderColor: .borderColor, textColor: .mainTextColor, - image: #imageLiteral(resourceName: "retry"), + image: UIImage(named: "retry"), accessibilityIdentifier: "gray" ) } @@ -314,7 +318,7 @@ extension ComposeViewDecorator { backgroundColor: .red, borderColor: .borderColor, textColor: .white, - image: #imageLiteral(resourceName: "retry"), + image: UIImage(named: "retry"), accessibilityIdentifier: "red" ) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift index 8fc7a7b6d..1e0fe0b99 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift @@ -12,24 +12,21 @@ import UIKit // MARK: - Action Handling extension ComposeViewController { - internal func searchEmail(with query: String) { + func searchEmail(with query: String) { Task { do { let cloudRecipients = try await googleUserService.searchContacts(query: query) let localRecipients = try localContactsProvider.searchRecipients(query: query) - let recipients = (cloudRecipients + localRecipients) - .unique() - .sorted() - + let recipients = (cloudRecipients + localRecipients).unique().sorted() updateView(newState: .searchEmails(recipients)) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } - internal func evaluate(recipient: ComposeMessageRecipient) { + func evaluate(recipient: ComposeMessageRecipient) { guard recipient.email.isValidEmail else { updateRecipient( email: recipient.email, @@ -62,7 +59,7 @@ extension ComposeViewController { } } - internal func handleEvaluation(for recipient: RecipientWithSortedPubKeys) { + func handleEvaluation(for recipient: RecipientWithSortedPubKeys) { let state = getRecipientState(from: recipient) updateRecipient( @@ -73,7 +70,7 @@ extension ComposeViewController { ) } - internal func getRecipientState(from recipient: RecipientWithSortedPubKeys) -> RecipientState { + func getRecipientState(from recipient: RecipientWithSortedPubKeys) -> RecipientState { switch recipient.keyState { case .active: return decorator.recipientKeyFoundState @@ -86,7 +83,7 @@ extension ComposeViewController { } } - internal func handleEvaluation(error: Error, with email: String, contact: RecipientWithSortedPubKeys?) { + func handleEvaluation(error: Error, with email: String, contact: RecipientWithSortedPubKeys?) { let recipientState: RecipientState = { if let contact = contact, contact.keyState == .active { return getRecipientState(from: contact) @@ -106,7 +103,7 @@ extension ComposeViewController { ) } - internal func updateRecipient( + func updateRecipient( email: String, name: String? = nil, state: RecipientState, @@ -117,7 +114,7 @@ extension ComposeViewController { } var displayName = name - if let name = name, let address = MCOAddress.init(nonEncodedRFC822String: name), address.displayName != nil { + if let name = name, let address = MCOAddress(nonEncodedRFC822String: name), address.displayName != nil { displayName = address.displayName } @@ -138,7 +135,7 @@ extension ComposeViewController { } } - internal func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { + func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } let isSelected = recipient.state.isSelected @@ -158,7 +155,7 @@ extension ComposeViewController { textField?.reset() } - internal func handleRecipientAction(with indexPath: IndexPath, type: RecipientType) { + func handleRecipientAction(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } switch recipient.state { @@ -185,15 +182,17 @@ extension ComposeViewController { } // MARK: - Message password - internal func setMessagePassword() { + func setMessagePassword() { Task { + stopDraftTimer(withSave: false) contextToSend.messagePassword = await enterMessagePassword() reload(sections: [.password]) + startDraftTimer() } } - internal func enterMessagePassword() async -> String? { - return await withCheckedContinuation { (continuation: CheckedContinuation) in + func enterMessagePassword() async -> String? { + return await withCheckedContinuation { continuation in self.messagePasswordAlertController = createMessagePasswordAlert(continuation: continuation) self.present(self.messagePasswordAlertController!, animated: true, completion: nil) } @@ -214,8 +213,8 @@ extension ComposeViewController { $0.addTarget(self, action: #selector(self.messagePasswordTextFieldDidChange), for: .editingChanged) } - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) { _ in - return continuation.resume(returning: self.contextToSend.messagePassword) + let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in + return continuation.resume(returning: self?.contextToSend.messagePassword) } alert.addAction(cancelAction) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift index 73933640d..6b6012db1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift @@ -10,7 +10,7 @@ import UIKit // MARK: - Attachments sheet handling extension ComposeViewController { - internal func openAttachmentsInputSourcesSheet() { + func openAttachmentsInputSourcesSheet() { let alert = UIAlertController( title: "files_picking_select_input_source_title".localized, message: nil, @@ -42,7 +42,7 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - internal func takePhoto() { + func takePhoto() { Task { do { try await photosManager.takePhoto(from: self) @@ -52,80 +52,26 @@ extension ComposeViewController { } } - internal func selectPhoto() { + private func selectPhoto() { Task { await photosManager.selectPhoto(from: self) } } - internal func selectFromFilesApp() { + private func selectFromFilesApp() { Task { await filesManager.selectFromFilesApp(from: self) } } - internal func showNoAccessToCameraAlert() { - let alert = UIAlertController( + private func showNoAccessToCameraAlert() { + showAlertWithAction( title: "files_picking_no_camera_access_error_title".localized, message: "files_picking_no_camera_access_error_message".localized, - preferredStyle: .alert - ) - let okAction = UIAlertAction( - title: "ok".localized, - style: .cancel - ) { _ in } - let settingsAction = UIAlertAction( - title: "settings".localized, - style: .default - ) { _ in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) - } - alert.addAction(okAction) - alert.addAction(settingsAction) - - present(alert, animated: true, completion: nil) - } - - internal func askForContactsPermission() { - shouldEvaluateRecipientInput = false - - Task { - do { - try await router.askForContactsPermission(for: .gmailLogin(self), appContext: appContext) - shouldEvaluateRecipientInput = true - reload(sections: [.contacts]) - } catch { - shouldEvaluateRecipientInput = true - handleContactsPermissionError(error) + actionButtonTitle: "settings".localized, + onAction: { _ in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) } - } - } - - internal func handleContactsPermissionError(_ error: Error) { - guard let gmailUserError = error as? GoogleUserServiceError, - case .userNotAllowedAllNeededScopes(let missingScopes, _) = gmailUserError - else { return } - - let scopes = missingScopes.map(\.title).joined(separator: ", ") - - let alert = UIAlertController( - title: "error".localized, - message: "compose_missing_contacts_scopes".localizeWithArguments(scopes), - preferredStyle: .alert - ) - let laterAction = UIAlertAction( - title: "later".localized, - style: .cancel ) - let allowAction = UIAlertAction( - title: "allow".localized, - style: .default - ) { [weak self] _ in - self?.askForContactsPermission() - } - alert.addAction(laterAction) - alert.addAction(allowAction) - - present(alert, animated: true, completion: nil) } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift new file mode 100644 index 000000000..9c41fb42b --- /dev/null +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift @@ -0,0 +1,47 @@ +// +// ComposeViewController+Contacts.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 02/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +extension ComposeViewController { + func askForContactsPermission() { + shouldEvaluateRecipientInput = false + + Task { + do { + try await router.askForContactsPermission( + for: .gmailLogin(self), + appContext: appContext + ) + shouldEvaluateRecipientInput = true + reload(sections: [.contacts]) + } catch { + shouldEvaluateRecipientInput = true + handleContactsPermissionError(error) + } + } + } + + private func handleContactsPermissionError(_ error: Error) { + guard let gmailUserError = error as? GoogleUserServiceError, + case .userNotAllowedAllNeededScopes(let missingScopes, _) = gmailUserError + else { return } + + let scopes = missingScopes.map(\.title).joined(separator: ", ") + + showAlertWithAction( + title: "error".localized, + message: "compose_missing_contacts_scopes".localizeWithArguments(scopes), + cancelButtonTitle: "later".localized, + actionButtonTitle: "allow".localized, + onAction: { [weak self] _ in + self?.askForContactsPermission() + } + ) + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 7b2191d44..087468e25 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -8,53 +8,78 @@ // MARK: - Drafts extension ComposeViewController { - @objc internal func startDraftTimer() { - saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in + @objc func startDraftTimer(withFire: Bool = false) { + guard saveDraftTimer == nil else { return } + + saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in self?.saveDraftIfNeeded() } - saveDraftTimer?.fire() + + if withFire { + saveDraftTimer?.fire() + } } - @objc internal func stopDraftTimer() { + @objc func stopDraftTimer(withSave: Bool = true) { + guard saveDraftTimer != nil else { return } + saveDraftTimer?.invalidate() saveDraftTimer = nil - saveDraftIfNeeded() + + if withSave { + saveDraftIfNeeded() + } } - private func shouldSaveDraft() -> Bool { - // https://github.com/FlowCrypt/flowcrypt-ios/issues/975 - return false -// let newDraft = ComposedDraft(email: email, input: input, contextToSend: contextToSend) -// guard let oldDraft = composedLatestDraft else { -// composedLatestDraft = newDraft -// return true -// } -// let result = newDraft != oldDraft -// composedLatestDraft = newDraft -// return result + private func createDraft() -> ComposedDraft? { + let newDraft = ComposedDraft( + input: input, + contextToSend: contextToSend + ) + + guard let existingDraft = composedLatestDraft else { + composedLatestDraft = newDraft + return nil + } + + return newDraft != existingDraft ? newDraft : nil } - internal func saveDraftIfNeeded() { - guard shouldSaveDraft() else { return } + func saveDraftIfNeeded(handler: ((DraftSaveState) -> Void)? = nil) { + guard let draft = createDraft() else { + handler?(.cancelled) + return + } + + handler?(.saving(draft)) + Task { do { + let shouldEncrypt = draft.input.type.info?.shouldEncrypt == true || + contextToSend.hasRecipientsWithActivePubKey + let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( - senderEmail: selectedFromEmail, - input: input, - contextToSend: contextToSend, - includeAttachments: false + input: draft.input, + contextToSend: draft.contextToSend, + isDraft: true, + withPubKeys: shouldEncrypt ) - try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) + + try await composeMessageService.saveDraft( + message: sendableMsg, + threadId: draft.input.threadId, + shouldEncrypt: shouldEncrypt + ) + + composedLatestDraft = draft + handler?(.success(sendableMsg)) } catch { - if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { - requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) - } if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast - // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? - showToast("Error saving draft: \(error.errorMessage)") + showToast("draft_error".localizeWithArguments(error.errorMessage), position: .top) } + handler?(.error(error)) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift new file mode 100644 index 000000000..321de06dc --- /dev/null +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -0,0 +1,91 @@ +// +// ComposeViewController+ErrorHandling.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 14/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +// MARK: - Error handling +extension ComposeViewController { + func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { + let alert = alertsFactory.makePassPhraseAlert( + onCancel: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onCompletion: { [weak self] passPhrase in + guard let self = self else { return } + + Task { + do { + let matched = try await self.composeMessageService.handlePassPhraseEntry( + passPhrase, + for: signingKey + ) + + if matched { + self.handleMatchedPassphrase(isDraft: isDraft) + } else { + self.handle(error: ComposeMessageError.passPhraseNoMatch) + } + } catch { + self.handle(error: error) + } + } + } + ) + present(alert, animated: true, completion: nil) + } + + private func handleMatchedPassphrase(isDraft: Bool) { + guard isDraft else { + handleSendTap() + return + } + + guard didFinishSetup else { + fillDataFromInput() + return + } + + saveDraftIfNeeded() + } + + func handle(error: Error) { + reEnableSendButton() + + if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { + requestMissingPassPhraseWithModal(for: keyPair) + return + } + + let hideSpinnerAnimationDuration: TimeInterval = 1 + DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in + guard let self else { return } + + if self.isMessagePasswordSupported { + switch error { + case MessageValidationError.noPubRecipients: + self.setMessagePassword() + case MessageValidationError.notUniquePassword, + MessageValidationError.subjectContainsPassword, + MessageValidationError.weakPassword: + self.showAlert(message: error.errorMessage) + default: + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } + } else { + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } + } + } + + private func reEnableSendButton() { + UIApplication.shared.isIdleTimerDisabled = false + startDraftTimer() + hideSpinner() + navigationItem.rightBarButtonItem?.isEnabled = true + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift index 837a0839c..214a73c18 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift @@ -11,7 +11,7 @@ import FlowCryptUI // MARK: - Keyboard extension ComposeViewController { - internal func observeKeyboardNotifications() { + func observeKeyboardNotifications() { // swiftlint:disable discarded_notification_center_observer NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillShowNotification, @@ -31,7 +31,7 @@ extension ComposeViewController { } } - internal func observerAppStates() { + func observerAppStates() { NotificationCenter.default.addObserver( self, selector: #selector(startDraftTimer), @@ -45,7 +45,7 @@ extension ComposeViewController { object: nil) } - internal func adjustForKeyboard(height: CGFloat) { + private func adjustForKeyboard(height: CGFloat) { node.contentInset.bottom = height + 8 guard let textView = node.visibleNodes.compactMap({ $0 as? TextViewCellNode }).first?.textView.textView, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 5248072e0..5eb92e9ae 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -7,13 +7,13 @@ // import UIKit -import FlowCryptCommon import FlowCryptUI // MARK: - Message Sending extension ComposeViewController { - internal func sendMessage() async throws { + func sendMessage() async throws { view.endEditing(true) + navigationItem.rightBarButtonItem?.isEnabled = false let spinnerTitle = contextToSend.attachments.isEmpty ? "sending_title" : "encrypting_title" @@ -28,87 +28,27 @@ extension ComposeViewController { // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms - let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg( - senderEmail: selectedFromEmail, - input: self.input, - contextToSend: self.contextToSend + let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( + input: input, + contextToSend: contextToSend ) UIApplication.shared.isIdleTimerDisabled = true - try await composeMessageService.encryptAndSend( + let identifier = try await composeMessageService.encryptAndSend( message: sendableMsg, threadId: input.threadId ) - handleSuccessfullySentMessage() - } - internal func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { - let alert = alertsFactory.makePassPhraseAlert( - onCancel: { - self.handle(error: ComposeMessageError.passPhraseRequired) - }, - onCompletion: { [weak self] passPhrase in - guard let self = self else { - return - } - Task { - do { - let matched = try await self.composeMessageService.handlePassPhraseEntry( - passPhrase, - for: signingKey - ) - if matched { - if isDraft { - self.saveDraftIfNeeded() - } else { - self.handleSendTap() - } - } else { - self.handle(error: ComposeMessageError.passPhraseNoMatch) - } - } catch { - self.handle(error: error) - } - } - } + let messageIdentifier = MessageIdentifier( + draftId: input.type.info?.id, + threadId: Identifier(stringId: input.threadId), + messageId: identifier ) - present(alert, animated: true, completion: nil) - } - - internal func handle(error: Error) { - UIApplication.shared.isIdleTimerDisabled = false - hideSpinner() - navigationItem.rightBarButtonItem?.isEnabled = true + handleAction?(.sent(messageIdentifier)) - if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { - requestMissingPassPhraseWithModal(for: keyPair) - return - } - - let hideSpinnerAnimationDuration: TimeInterval = 1 - DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in - guard let self = self else { return } - - if self.isMessagePasswordSupported { - switch error { - case MessageValidationError.noPubRecipients: - self.setMessagePassword() - case MessageValidationError.notUniquePassword, - MessageValidationError.subjectContainsPassword, - MessageValidationError.weakPassword: - self.showAlert(message: error.errorMessage) - default: - self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) - } - } else { - self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) - } - } + handleSuccessfullySentMessage() } private func handleSuccessfullySentMessage() { - UIApplication.shared.isIdleTimerDisabled = false - hideSpinner() - navigationItem.rightBarButtonItem?.isEnabled = true showToast(input.successfullySentToast) navigationController?.popViewController(animated: true) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index 167633d67..8bcd7f5e6 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -12,7 +12,7 @@ import FlowCryptUI // MARK: - Nodes extension ComposeViewController { - internal func recipientTextNode() -> ComposeRecipientCellNode { + func recipientTextNode() -> ComposeRecipientCellNode { let recipients = contextToSend.recipients.map(RecipientEmailsCellNode.Input.init) let textNode = ComposeRecipientCellNode( input: ComposeRecipientCellNode.Input(recipients: recipients), @@ -25,26 +25,28 @@ extension ComposeViewController { return textNode } - internal func showRecipientLabelIfNecessary() { - let isRecipientLoading = self.contextToSend.recipients.filter { $0.state == decorator.recipientIdleState }.isNotEmpty + func showRecipientLabelIfNecessary() { + let isRecipientLoading = contextToSend.recipients.contains(where: { + $0.state == decorator.recipientIdleState + }) + guard !isRecipientLoading, - self.contextToSend.recipients.isNotEmpty, - self.userTappedOutSideRecipientsArea else { - return - } - if !self.shouldShowEmailRecipientsLabel { - self.shouldShowEmailRecipientsLabel = true - self.userTappedOutSideRecipientsArea = false - self.reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) - } + contextToSend.recipients.isNotEmpty, + userTappedOutSideRecipientsArea, + !shouldShowEmailRecipientsLabel + else { return } + + shouldShowEmailRecipientsLabel = true + userTappedOutSideRecipientsArea = false + reload(sections: Section.recipientsSections + [.recipientsLabel]) } - internal func hideRecipientLabel() { - self.shouldShowEmailRecipientsLabel = false - self.reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) + func hideRecipientLabel() { + shouldShowEmailRecipientsLabel = false + reload(sections: Section.recipientsSections + [.recipientsLabel]) } - internal func setupSubjectNode() { + func setupSubjectNode() { composeSubjectNode = TextFieldCellNode( input: decorator.styledTextFieldInput( with: "compose_subject".localized, @@ -62,7 +64,7 @@ extension ComposeViewController { } } .onShouldReturn { [weak self] _ in - guard let self = self else { return true } + guard let self else { return true } if !self.input.isQuote, let node = self.node.visibleNodes.compactMap({ $0 as? TextViewCellNode }).first { node.becomeFirstResponder() } else { @@ -75,13 +77,13 @@ extension ComposeViewController { } } - internal func setupFromNode() { - fromCellNode = RecipientFromCellNode( - toggleButtonAction: { - self.presentSendAsActionSheet() + func fromCellNode() -> RecipientFromCellNode { + RecipientFromCellNode( + fromEmail: contextToSend.sender, + toggleButtonAction: { [weak self] in + self?.presentSendAsActionSheet() } ) - fromCellNode.fromEmail = selectedFromEmail } private func presentSendAsActionSheet() { @@ -96,15 +98,16 @@ extension ComposeViewController { for aliasEmail in sendAsList { let action = UIAlertAction( - title: aliasEmail.descriptoin, - style: .default) { [weak self] _ in - self?.changeSendAs(to: aliasEmail.sendAsEmail) - } + title: aliasEmail.description, + style: .default + ) { [weak self] _ in + self?.changeSendAs(to: aliasEmail.sendAsEmail) + } // Remove @, . in email part as appium throws error for identifiers which contain @, . - let emailIentifier = aliasEmail.sendAsEmail + let emailIdentifier = aliasEmail.sendAsEmail .replacingOccurrences(of: "@", with: "-") .replacingOccurrences(of: ".", with: "-") - action.accessibilityIdentifier = "aid-send-as-\(emailIentifier)" + action.accessibilityIdentifier = "aid-send-as-\(emailIdentifier)" alert.addAction(action) } alert.addAction(cancelAction) @@ -113,26 +116,22 @@ extension ComposeViewController { } private func changeSendAs(to email: String) { - guard let section = sectionsList.firstIndex(of: .recipients(.from)), - let fromCell = node.nodeForRow(at: IndexPath(row: 0, section: section)) as? RecipientFromCellNode else { - return - } - fromCell.fromEmail = email - self.selectedFromEmail = email + contextToSend.sender = email + reload(sections: [.recipients(.from)]) } - internal func messagePasswordNode() -> ASCellNode { + func messagePasswordNode() -> ASCellNode { let input = contextToSend.hasMessagePassword ? decorator.styledFilledMessagePasswordInput() : decorator.styledEmptyMessagePasswordInput() - return MessagePasswordCellNode( + return MessageActionCellNode( input: input, - setMessagePassword: { [weak self] in self?.setMessagePassword() } + action: { [weak self] in self?.setMessagePassword() } ) } - internal func setupTextNode() { + func setupTextNode() { let styledQuote = decorator.styledQuote(with: input) let height = max(decorator.frame(for: styledQuote).height, 40) composeTextNode = TextViewCellNode( @@ -141,7 +140,7 @@ extension ComposeViewController { accessibilityIdentifier: "aid-message-text-view" ) ) { [weak self] event in - guard let self = self else { return } + guard let self else { return } switch event { case .didBeginEditing: self.userTappedOutSideRecipientsArea = true @@ -153,31 +152,36 @@ extension ComposeViewController { } } .then { - let messageText = decorator.styledMessage(with: contextToSend.message ?? "") - let mutableString = NSMutableAttributedString(attributedString: messageText) + let message = contextToSend.message ?? "" + let attributedString = decorator.styledMessage(with: message) + let mutableString = NSMutableAttributedString(attributedString: attributedString) let textNode = $0 - if input.isQuote && !messageText.string.contains(styledQuote.string) { + if input.isQuote && !mutableString.string.contains(styledQuote.string) { mutableString.append(styledQuote) } DispatchQueue.main.async { - textNode.textView.attributedText = mutableString + if !mutableString.string.isEmpty { + textNode.textView.attributedText = mutableString + } + // Set cursor position to start of text view textNode.textView.textView.selectedTextRange = textNode.textView.textView.textRange( from: textNode.textView.textView.beginningOfDocument, to: textNode.textView.textView.beginningOfDocument ) - if self.input.isReply { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { + + if self.input.shouldFocusTextNode { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { textNode.becomeFirstResponder() - }) + } } } } } - internal func ensureCursorVisible(textView: UITextView) { + func ensureCursorVisible(textView: UITextView) { guard let range = textView.selectedTextRange else { return } let cursorRect = textView.caretRect(for: range.start) @@ -191,7 +195,7 @@ extension ComposeViewController { } } - internal func recipientsNode(type: RecipientType) -> ASCellNode { + func recipientsNode(type: RecipientType) -> ASCellNode { let recipients = contextToSend.recipients(type: type) let shouldShowToggleButton = type == .to @@ -214,50 +218,52 @@ extension ComposeViewController { type: type, completion: { if let indexPath = self?.recipientsIndexPath(type: type), - let emailNode = self?.node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode { - emailNode.style.preferredSize.height = layoutHeight - emailNode.setNeedsLayout() + let emailsNode = self?.node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode { + emailsNode.style.preferredSize.height = layoutHeight + emailsNode.setNeedsLayout() } } ) } - .onItemSelect { [weak self] (action: RecipientEmailsCellNode.RecipientEmailTapAction) in + .onItemSelect { [weak self] action in + guard let self else { return } + switch action { case let .imageTap(indexPath): - self?.handleRecipientAction(with: indexPath, type: type) + self.handleRecipientAction(with: indexPath, type: type) case let .select(indexPath, sender): - self?.handleRecipientSelection(with: indexPath, type: type) - self?.displayRecipientPopOver(with: indexPath, type: type, sender: sender) + self.handleRecipientSelection(with: indexPath, type: type) + self.displayRecipientPopOver(with: indexPath, type: type, sender: sender) } } } - internal func recipientInput(type: RecipientType) -> RecipientEmailTextFieldNode { - return RecipientEmailTextFieldNode( + func recipientInput(type: RecipientType) -> RecipientEmailTextFieldNode { + RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( with: "", keyboardType: .emailAddress, accessibilityIdentifier: "aid-recipients-text-field-\(type.rawValue)", - insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + insets: .zero ), action: { [weak self] action in self?.handle(textFieldAction: action, for: type) } ) - .onShouldReturn { [weak self] textField -> (Bool) in + .onShouldReturn { [weak self] textField in if let isValid = self?.showAlertIfTextFieldNotValidEmail(textField: textField), isValid { textField.resignFirstResponder() return true } return false } - .onShouldEndEditing { [weak self] textField -> (Bool) in + .onShouldEndEditing { [weak self] textField in if let isValid = self?.showAlertIfTextFieldNotValidEmail(textField: textField), isValid { return true } return false } - .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in + .onShouldChangeCharacters { [weak self] textField, character in self?.shouldChange(with: textField, and: character, for: type) ?? true } .then { @@ -267,7 +273,7 @@ extension ComposeViewController { } } - internal func showAlertIfTextFieldNotValidEmail(textField: UITextField) -> Bool { + func showAlertIfTextFieldNotValidEmail(textField: UITextField) -> Bool { if let text = textField.text, text.isEmpty || text.isValidEmail { return true } @@ -275,7 +281,7 @@ extension ComposeViewController { return false } - internal func attachmentNode(for index: Int) -> ASCellNode { + func attachmentNode(for index: Int) -> ASCellNode { AttachmentNode( input: .init( attachment: contextToSend.attachments[index], @@ -288,7 +294,7 @@ extension ComposeViewController { ) } - internal func noSearchResultsNode() -> ASCellNode { + func noSearchResultsNode() -> ASCellNode { TextCellNode(input: .init( backgroundColor: .clear, title: "compose_no_contacts_found".localized, @@ -299,7 +305,7 @@ extension ComposeViewController { ) } - internal func enableGoogleContactsNode() -> ASCellNode { + func enableGoogleContactsNode() -> ASCellNode { TextWithIconNode(input: .init( title: "compose_enable_google_contacts_search" .localized diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift index 4ef66b2eb..7dc7624af 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift @@ -34,7 +34,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo reload(sections: [.attachments]) } - internal func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { + private func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { let totalSize = contextToSend.attachments.map(\.size).reduce(0, +) + attachment.size if totalSize > GeneralConstants.Global.attachmentSizeLimit { showToast("files_picking_size_error_message".localized) @@ -53,7 +53,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } } - internal func handleResults(_ results: [PHPickerResult]) { + private func handleResults(_ results: [PHPickerResult]) { guard let itemProvider = results.first?.itemProvider else { return } enum MediaType: String { @@ -79,7 +79,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { ) } - internal func handleRepresentation(url: URL?, error: Error?, isVideo: Bool) { + private func handleRepresentation(url: URL?, error: Error?, isVideo: Bool) { guard let url = url, let composeMessageAttachment = MessageAttachment(fileURL: url) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift index 5e64ee8f9..d0407aac5 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift @@ -11,7 +11,7 @@ import FlowCryptUI // MARK: - Recipients Input extension ComposeViewController { - internal func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { + func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } node.becomeFirstResponder() @@ -38,7 +38,7 @@ extension ComposeViewController { return true } - internal func handle(textFieldAction: TextFieldActionType, for recipientType: RecipientType) { + func handle(textFieldAction: TextFieldActionType, for recipientType: RecipientType) { switch textFieldAction { case let .deleteBackward(textField): handleBackspaceAction(with: textField, for: recipientType) case let .didEndEditing(text): handleEndEditingAction(with: text, for: recipientType) @@ -48,7 +48,7 @@ extension ComposeViewController { } } - internal func handleEndEditingAction(with email: String?, name: String? = nil, for recipientType: RecipientType) { + func handleEndEditingAction(with email: String?, name: String? = nil, for recipientType: RecipientType) { guard shouldEvaluateRecipientInput, let email = email, email.isNotEmpty else { return } @@ -76,7 +76,7 @@ extension ComposeViewController { state: decorator.recipientIdleState ) - if idleRecipients.firstIndex(where: { $0.email == newRecipient.email }) == nil { + if !idleRecipients.contains(where: { $0.email == newRecipient.email }) { // add new recipient contextToSend.add(recipient: newRecipient) @@ -101,7 +101,7 @@ extension ComposeViewController { /// - Parameter type: Recipient type. /// - Parameter refreshType: Refresh type (delete/add/reload/scrollToBottom). /// - Parameter TempRecipients: Temp recipients (Optional). Used to get deleted recipient index - internal func refreshRecipient( + func refreshRecipient( for email: String, type: RecipientType, refreshType: RefreshType, @@ -134,18 +134,18 @@ extension ComposeViewController { } } - internal func recipientsIndexPath(type: RecipientType) -> IndexPath? { + func recipientsIndexPath(type: RecipientType) -> IndexPath? { guard let section = sectionsList.firstIndex(of: .recipients(type)) else { return nil } return IndexPath(row: 0, section: section) } - internal func recipientsTextField(type: RecipientType) -> TextFieldNode? { + func recipientsTextField(type: RecipientType) -> TextFieldNode? { guard let indexPath = recipientsIndexPath(type: type) else { return nil } return (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.recipientInput.textField } - internal func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { - guard textField.text == "" else { return } + func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { + guard let text = textField.text, text.isEmpty else { return } var recipients = contextToSend.recipients(type: recipientType) @@ -176,17 +176,18 @@ extension ComposeViewController { } } - internal func handleEditingChanged(with text: String?) { - shouldDisplaySearchResult = text != "" - search.send(text ?? "") + func handleEditingChanged(with text: String?) { + let inputText = text ?? "" + shouldDisplaySearchResult = !inputText.isEmpty + search.send(inputText) } - internal func handleDidBeginEditing(recipientType: RecipientType) { + func handleDidBeginEditing(recipientType: RecipientType) { selectedRecipientType = recipientType node.view.keyboardDismissMode = .none } - internal func toggleRecipientsList() { + func toggleRecipientsList() { shouldShowAllRecipientTypes.toggle() reload(sections: [.recipients(.cc), .recipients(.bcc)]) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift index ff3ce24f1..3958fa027 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift @@ -10,7 +10,7 @@ import FlowCryptUI import UIKit extension ComposeViewController { - internal func displayRecipientPopOver(with indexPath: IndexPath, type: RecipientType, sender: CellNode) { + func displayRecipientPopOver(with indexPath: IndexPath, type: RecipientType, sender: CellNode) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } popoverVC = ComposeRecipientPopupViewController( @@ -22,10 +22,10 @@ extension ComposeViewController { popoverVC.popoverPresentationController?.permittedArrowDirections = .up popoverVC.popoverPresentationController?.delegate = self popoverVC.delegate = self - self.present(popoverVC, animated: true, completion: nil) + present(popoverVC, animated: true, completion: nil) } - internal func hideRecipientPopOver() { + func hideRecipientPopOver() { if popoverVC != nil { popoverVC.dismiss(animated: true, completion: nil) } @@ -41,9 +41,8 @@ extension ComposeViewController: UIPopoverPresentationControllerDelegate { } public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - guard let popoverVC = presentationController.presentedViewController as? ComposeRecipientPopupViewController else { - return - } + guard let popoverVC = presentationController.presentedViewController as? ComposeRecipientPopupViewController + else { return } let recipients = contextToSend.recipients(type: popoverVC.type) let selectedRecipients = recipients.filter { $0.state.isSelected } // Deselect previous selected receipients @@ -56,21 +55,21 @@ extension ComposeViewController: UIPopoverPresentationControllerDelegate { extension ComposeViewController: ComposeRecipientPopupViewControllerProtocol { func removeRecipient(email: String, type: RecipientType) { - let tempRecipients = self.contextToSend.recipients(type: type) - self.contextToSend.remove(recipient: email, type: type) + let tempRecipients = contextToSend.recipients(type: type) + contextToSend.remove(recipient: email, type: type) reload(sections: [.password]) refreshRecipient(for: email, type: type, refreshType: .delete, tempRecipients: tempRecipients) } func editRecipient(email: String, type: RecipientType) { removeRecipient(email: email, type: type) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if let textField = self.recipientsTextField(type: type) { textField.text = email if !textField.isFirstResponder() { textField.becomeFirstResponder() } } - }) + } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 71564aeb8..202612dda 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -11,30 +11,35 @@ import FlowCryptUI // MARK: - Setup UI extension ComposeViewController { - internal func setupNavigationBar() { + func setupNavigationBar() { + let deleteButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "trash"), + accessibilityId: "aid-compose-delete" + ) { [weak self] in + self?.handleTrashTap() + } + let helpButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "questionmark.circle") + ) { [weak self] in + self?.handleInfoTap() + } + let attachmentButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "paperclip") + ) { [weak self] in + self?.handleAttachTap() + } + let sendButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "paperplane"), + accessibilityId: "aid-compose-send" + ) { [weak self] in + self?.handleSendTap() + } navigationItem.rightBarButtonItem = NavigationBarItemsView( - with: [ - NavigationBarItemsView.Input( - image: UIImage(systemName: "questionmark.circle") - ) { [weak self] in - self?.handleInfoTap() - }, - NavigationBarItemsView.Input( - image: UIImage(systemName: "paperclip") - ) { [weak self] in - self?.handleAttachTap() - }, - NavigationBarItemsView.Input( - image: UIImage(systemName: "paperplane"), - accessibilityId: "aid-compose-send" - ) { [weak self] in - self?.handleSendTap() - } - ] + with: [deleteButton, helpButton, attachmentButton, sendButton] ) } - internal func setupUI() { + func setupUI() { let tap = UITapGestureRecognizer(target: self, action: #selector(handleTableTap)) node.do { @@ -49,36 +54,80 @@ extension ComposeViewController { updateView(newState: .main) } - internal func setupQuote() { - guard input.isQuote else { return } + func fillDataFromInput() { + guard let info = input.type.info else { + didFinishSetup = true + return + } - for recipient in input.quoteRecipients { - evaluateMessage(recipient: recipient, type: .to) + if case .draft = input.type { + composeMessageService.fetchMessageIdentifier(info: info) } - for recipient in input.quoteCCRecipients { - evaluateMessage(recipient: recipient, type: .cc) + contextToSend.subject = info.subject + addRecipients(from: info) + + if input.isPgp { + decodeDraft(from: info) + } else { + if case .draft = input.type { + contextToSend.message = input.text + } + reload(sections: Section.recipientsSections) + didFinishSetup = true + } + } + + private func addRecipients(from info: ComposeMessageInput.MessageQuoteInfo) { + guard contextToSend.recipients.isEmpty else { return } + + for recipient in info.recipients { + add(recipient: recipient, type: .to) } - if input.quoteCCRecipients.isNotEmpty { + for recipient in info.ccRecipients { + add(recipient: recipient, type: .cc) + } + + for recipient in info.bccRecipients { + add(recipient: recipient, type: .bcc) + } + + if info.ccRecipients.isNotEmpty || info.bccRecipients.isNotEmpty { shouldShowAllRecipientTypes.toggle() } } - internal func setupNodes() { - setupTextNode() - setupSubjectNode() - setupFromNode() + private func decodeDraft(from info: ComposeMessageInput.MessageQuoteInfo) { + Task { + do { + let decrypted = try await messageService.decrypt( + text: info.text, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + contextToSend.message = decrypted + didFinishSetup = true + reload(sections: Section.recipientsSections + [.compose]) + } catch { + if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { + requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) + return + } else { + handle(error: error) + } + } + } } } // MARK: - Search extension ComposeViewController { - internal func setupSearch() { + func setupSearch() { search .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() - .map { [weak self] query -> String in + .map { [weak self] query in if query.isEmpty { self?.updateView(newState: .main) } @@ -91,3 +140,34 @@ extension ComposeViewController { .store(in: &cancellable) } } + +// MARK: - NavigationChildController +extension ComposeViewController: NavigationChildController { + func handleBackButtonTap() { + stopDraftTimer(withSave: false) + + saveDraftIfNeeded { [weak self] state in + guard let self else { return } + + switch state { + case .cancelled: + self.handleUpdateAction() + case .error(let error): + self.showToast("draft_error".localizeWithArguments(error.errorMessage)) + case .success: + self.handleUpdateAction() + self.showToast("draft_saved".localized, duration: 1.0) + case .saving: + self.showToast("draft_saving".localized, duration: 10.0) + } + } + + navigationController?.popViewController(animated: true) + } + + private func handleUpdateAction() { + guard var messageIdentifier = composeMessageService.messageIdentifier else { return } + messageIdentifier.draftMessageId = input.type.info?.id + handleAction?(.update(messageIdentifier)) + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift index ae4eb7a72..311624639 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift @@ -10,8 +10,8 @@ import UIKit // MARK: - State Handling extension ComposeViewController { - internal func updateView(newState: State) { - if case .searchEmails = newState, !self.shouldDisplaySearchResult { + func updateView(newState: State) { + if case .searchEmails = newState, !shouldDisplaySearchResult { return } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 8de5a1f1b..e0e17366c 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -48,13 +48,13 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { // swiftlint:disable cyclomatic_complexity func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self, + guard let self, let section = self.sectionsList[safe: indexPath.section] else { return ASCellNode() } switch (self.state, section) { case (_, .recipients(.from)): - return self.fromCellNode + return self.fromCellNode() case (_, .recipients(.to)), (_, .recipients(.cc)), (_, .recipients(.bcc)): let recipientType = RecipientType.allCases[indexPath.section] return self.recipientsNode(type: recipientType) @@ -65,8 +65,8 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, .compose): guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } switch part { - case .subject: return self.composeSubjectNode - case .text: return self.composeTextNode + case .subject: return self.composeSubjectNode ?? ASCellNode() + case .text: return self.composeTextNode ?? ASCellNode() case .topDivider, .subjectDivider: return DividerCellNode() } case (.main, .attachments): @@ -77,7 +77,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case let (.searchEmails(recipients), .searchResults): guard indexPath.row > 0 else { return DividerCellNode() } guard recipients.isNotEmpty else { return self.noSearchResultsNode() } - guard let recipient = recipients[safe: indexPath.row-1] else { return ASCellNode() } + guard let recipient = recipients[safe: indexPath.row - 1] else { return ASCellNode() } if let name = recipient.name { let input = self.decorator.styledRecipientInfo( @@ -103,7 +103,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch section { case .searchResults: - let recipient = recipients[safe: indexPath.row-1] + let recipient = recipients[safe: indexPath.row - 1] handleEndEditingAction(with: recipient?.email, name: recipient?.name, for: recipientType) case .contacts: askForContactsPermission() @@ -119,7 +119,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } } - internal func reload(sections: [Section]) { + func reload(sections: [Section]) { let indexes = sectionsList.enumerated().compactMap { index, section in sections.contains(section) ? index : nil } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 9ee5db2b6..d0ac340f6 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -8,15 +8,17 @@ // MARK: - Handle actions extension ComposeViewController { - internal func handleInfoTap() { + func handleInfoTap() { showToast("Please email us at human@flowcrypt.com for help") } - internal func handleAttachTap() { + func handleAttachTap() { openAttachmentsInputSourcesSheet() } - internal func handleSendTap() { + func handleSendTap() { + stopDraftTimer(withSave: false) + Task { do { guard contextToSend.hasMessagePasswordIfNeeded else { @@ -30,7 +32,39 @@ extension ComposeViewController { } } - @objc internal func handleTableTap() { + func handleTrashTap() { + showAlertWithAction( + title: "draft_delete_confirmation".localized, + message: nil, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + self?.deleteDraft() + } + ) + } + + private func deleteDraft() { + stopDraftTimer(withSave: false) + + Task { + do { + try await composeMessageService.deleteDraft() + + if let messageIdentifier = composeMessageService.messageIdentifier { + composeMessageService.messageIdentifier = nil + handleAction?(.delete(messageIdentifier)) + } + + showToast("draft_deleted".localized, duration: 1.0) + navigationController?.popViewController(animated: true) + } catch { + handle(error: error) + } + } + } + + @objc func handleTableTap() { if case .searchEmails = state, let selectedRecipientType = selectedRecipientType, let textField = recipientsTextField(type: selectedRecipientType), diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift new file mode 100644 index 000000000..6acc7028b --- /dev/null +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -0,0 +1,186 @@ +// +// InboxItem.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 11.10.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +struct InboxItem: Equatable { + var messages: [Message] + let folderPath: String + let type: InboxItemType + + enum InboxItemType: Equatable { + case message(Identifier), thread(Identifier) + } +} + +extension InboxItem { + var threadId: String? { + switch type { + case .thread(let id): + return id.stringId + case .message: + return nil + } + } + + var subject: String? { + messages + .compactMap(\.subject) + .first(where: { $0.isNotEmpty }) + } + + var labels: Set { + Set(messages.flatMap(\.labels)) + } + + var isInbox: Bool { + labels.contains(.inbox) + } + + var isDraft: Bool { + messages.count == 1 && labels.contains(.draft) + } + + var shouldShowMoveToInboxButton: Bool { + guard let firstMessageLabels = messages.first?.labels else { + return false + } + // Thread is treated as archived when labels don't contain `inbox` and first message label doesn't contain sent label + // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r931874353 + return !isInbox && !firstMessageLabels.contains(.sent) + } + + var isRead: Bool { + !messages.contains(where: { !$0.isRead }) + } + + var subtitle: String { + if let subject = subject, subject.hasContent { + return subject + } else { + return "message_missing_subject".localized + } + } + + var date: Date { + latestMessageDate(with: folderPath) + } + + var dateString: String { + DateFormatter().formatDate(date) + } + + var recipients: [String] { + messages + .flatMap(\.allRecipients) + .map(\.shortName) + .unique() + } + + var senderNames: [String] { + messages.compactMap(\.sender?.shortName).unique() + } + + var title: NSAttributedString { + let style: NSAttributedString.Style = isRead + ? .regular(17) + : .bold(17) + + let textColor: UIColor = isRead + ? .lightGray + : .mainTextUnreadColor + + if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { + let recipientsList = recipients.joined(separator: ",") + return "To: \(recipientsList)".attributed(style, color: textColor) + } else { + let hasDrafts = messages.contains(where: { $0.isDraft }) + + let sendersList = senderNames + .joined(separator: ",") + .attributed(style, color: textColor) + + if hasDrafts { + let draftLabel = "draft".localized + .attributed(style, color: .systemRed.withAlphaComponent(0.75)) + let title = sendersList.mutable() + title.append(",".attributed(style, color: textColor)) + title.append(draftLabel) + return title + } else { + return sendersList + } + } + } + + var badge: String? { + guard isInbox, folderPath.isEmpty || folderPath == MessageLabel.draft.value + else { + return nil + } + + return "folder_all_inbox".localized.lowercased() + } + + func messages(with label: String?) -> [Message] { + guard let label = label, !label.isEmpty else { return messages } + + let messageLabel = MessageLabel(gmailLabel: label) + return messages.filter { $0.labels.contains(messageLabel) } + } + + func latestMessageDate(with label: String?) -> Date { + messages(with: label).map(\.date).max() ?? .distantPast + } +} + +extension InboxItem { + init(message: Message) { + self.messages = [message] + self.folderPath = "" + self.type = .message(message.identifier) + } + + init(thread: MessageThread, folderPath: String?, identifier: MessageIdentifier? = nil) { + self.messages = thread.messages + self.folderPath = folderPath ?? "" + self.type = .thread(Identifier(stringId: thread.identifier)) + + if let draftId = identifier?.draftId, let messageId = identifier?.messageId { + guard let index = self.messages.firstIndex(where: { $0.identifier == messageId }) else { return } + self.messages[index].draftId = draftId + } + } + + mutating func update(labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = []) { + for index in messages.indices { + messages[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) + } + } + + mutating func markAsRead(_ isRead: Bool) { + if isRead { + update(labelsToRemove: [.unread, .none]) + } else { + update(labelsToAdd: [.unread, .none]) + } + } +} + +extension [InboxItem] { + func firstIndex(with messageIdentifier: MessageIdentifier) -> Int? { + firstIndex(where: { + switch $0.type { + case .thread(let threadId): + return threadId == messageIdentifier.threadId + case .message(let messageId): + return messageId == messageIdentifier.messageId + } + }) + } +} diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 2349286cb..5335c3247 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -9,14 +9,13 @@ import Foundation struct InboxContext { - let data: [InboxRenderable] + let data: [InboxItem] let pagination: MessagesListPagination } -class InboxDataProvider { - func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { - fatalError("Should be implemented") - } +protocol InboxDataProvider { + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext } // used when displaying conversations (threads) in inbox (Gmail API default) @@ -27,15 +26,20 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? { + guard let id = identifier.threadId?.stringId else { return nil } + let thread = try await provider.fetchThread(identifier: id, path: path) + return InboxItem(thread: thread, folderPath: path, identifier: identifier) + } + + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) - let inboxData = result.threads.map { - InboxRenderable( - thread: $0, - folderPath: context.folderPath - ) - } + let inboxData = result.threads + .map { InboxItem(thread: $0, folderPath: context.folderPath) } + .sorted { + $0.latestMessageDate(with: context.folderPath) > $1.latestMessageDate(with: context.folderPath) + } let inboxContext = InboxContext( data: inboxData, @@ -54,10 +58,16 @@ class InboxMessageListProvider: InboxDataProvider { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? { + guard let id = identifier.messageId else { return nil } + let message = try await provider.fetchMessage(id: id, folder: path) + return InboxItem(message: message) + } + + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchMessages(using: context) - let inboxData = result.messages.map(InboxRenderable.init) + let inboxData = result.messages.map(InboxItem.init) let inboxContext = InboxContext( data: inboxData, diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift deleted file mode 100644 index 6e3575e2d..000000000 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// InboxRenderable.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 11.10.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -struct InboxRenderable: Equatable { - enum WrappedType: Equatable { - case message(Message) - case thread(MessageThread) - } - - let title: String - let messageCount: Int - let subtitle: String - let dateString: String - var badge: String? - var isRead: Bool - - let date: Date - - var wrappedType: WrappedType - - private let folderPath: String? -} - -extension InboxRenderable { - var wrappedMessage: Message? { - guard case .message(let message) = wrappedType else { - return nil - } - return message - } - var wrappedThread: MessageThread? { - guard case .thread(let thread) = wrappedType else { - return nil - } - return thread - } -} - -extension InboxRenderable { - - init(message: Message) { - self.title = message.sender?.shortName ?? "message_unknown_sender".localized - self.messageCount = 1 - self.subtitle = message.subject ?? "message_missing_subject".localized - self.dateString = DateFormatter().formatDate(message.date) - self.isRead = message.isMessageRead - self.date = message.date - self.wrappedType = .message(message) - self.badge = nil - self.folderPath = nil - } - - init(thread: MessageThread, folderPath: String?) { - - self.title = InboxRenderable.messageTitle(for: thread, folderPath: folderPath) - - self.messageCount = thread.messages.count - self.subtitle = thread.subject ?? "message_missing_subject".localized - self.isRead = thread.isRead - let date = thread.messages.last?.date - if let date = date { - self.dateString = DateFormatter().formatDate(date) - } else { - self.dateString = "" - } - self.date = date ?? Date() - self.wrappedType = .thread(thread) - self.folderPath = folderPath - - self.updateBadge() - } - - private static func messageTitle(for thread: MessageThread, folderPath: String?) -> String { - // for now its not exactly clear how titles on other folders should look like - // so in scope of this PR we are applying this title presentation only for "sent" folder - if folderPath == MessageLabel.sent.value { - let recipients = thread.messages - .flatMap(\.allRecipients) - .map(\.shortName) - .unique() - .joined(separator: ", ") - return "To: \(recipients)" - } else { - return thread.messages - .compactMap(\.sender?.shortName) - .unique() - .joined(separator: ",") - } - } - - mutating func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel]) { - switch wrappedType { - case .thread(var thread): - thread.update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - wrappedType = .thread(thread) - case .message(var message): - message.update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - wrappedType = .message(message) - } - - updateBadge() - } - - mutating func updateBadge() { - // show 'inbox' badge in 'All Mail' folder - switch wrappedType { - case .thread(let thread): - self.badge = folderPath.isEmptyOrNil && thread.isInbox - ? "folder_all_inbox".localized.lowercased() - : nil - case .message: - self.badge = nil - } - } -} diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift index e28c09cd4..0826769d4 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import UIKit class InboxViewControllerFactory { diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 57b944b9e..014d2ba69 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -5,7 +5,6 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI -import Foundation @MainActor class InboxViewController: ViewController { @@ -13,33 +12,33 @@ class InboxViewController: ViewController { private let numberOfInboxItemsToLoad: Int - private let appContext: AppContextWithUser + let appContext: AppContextWithUser + let tableNode: ASTableNode + private let decorator: InboxViewDecorator - private let draftsListProvider: DraftsListProvider? private let messageOperationsProvider: MessageOperationsProvider private let refreshControl = UIRefreshControl() - internal let tableNode: ASTableNode private lazy var composeButton = ComposeButtonNode { [weak self] in self?.btnComposeTap() } private let inboxDataProvider: InboxDataProvider private let viewModel: InboxViewModel - internal var inboxInput: [InboxRenderable] = [] - internal var state: InboxViewController.State = .idle + private var inboxInput: [InboxItem] = [] + var state: InboxViewController.State = .idle private var inboxTitle: String { viewModel.folderName.isEmpty ? "Inbox" : viewModel.folderName } private var shouldShowEmptyView: Bool { - inboxInput.isNotEmpty && (viewModel.path == "SPAM" || viewModel.path == "TRASH") + inboxInput.isNotEmpty && (["SPAM", "TRASH"].contains(viewModel.path)) } var path: String { viewModel.path } // Search related varaibles - internal var isSearch = false - internal var searchedExpression = "" - var shouldBeginFetch = true + private var isSearch = false + private var shouldBeginFetch = true + var searchedExpression = "" private var isVisible = false private var didLayoutSubviews = false @@ -49,7 +48,6 @@ class InboxViewController: ViewController { viewModel: InboxViewModel, numberOfInboxItemsToLoad: Int = 50, provider: InboxDataProvider, - draftsListProvider: DraftsListProvider? = nil, decorator: InboxViewDecorator = InboxViewDecorator(), isSearch: Bool = false ) throws { @@ -59,7 +57,6 @@ class InboxViewController: ViewController { self.inboxDataProvider = provider let mailProvider = try appContext.getRequiredMailProvider() - self.draftsListProvider = try draftsListProvider ?? mailProvider.draftsProvider self.messageOperationsProvider = try mailProvider.messageOperationsProvider self.decorator = decorator self.tableNode = TableNode() @@ -124,7 +121,7 @@ extension InboxViewController { refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) } - internal func setupTableNode() { + func setupTableNode() { tableNode.do { $0.delegate = self $0.dataSource = self @@ -178,8 +175,6 @@ extension InboxViewController { private func messagesToLoad() -> Int { switch state { - case .fetched(.byNextPage): - return numberOfInboxItemsToLoad case .fetched(.byNumber(let totalNumberOfMessages)): guard let total = totalNumberOfMessages else { return numberOfInboxItemsToLoad @@ -194,35 +189,6 @@ extension InboxViewController { // MARK: - Functionality extension InboxViewController { - private func fetchAndRenderEmails(_ batchContext: ASBatchContext?) { - if let provider = draftsListProvider, viewModel.isDrafts { - fetchAndRenderDrafts(batchContext, draftsProvider: provider) - } else { - fetchAndRenderEmailsOnly(batchContext) - } - } - - private func fetchAndRenderDrafts(_ batchContext: ASBatchContext?, draftsProvider: DraftsListProvider) { - Task { - do { - let context = try await draftsProvider.fetchDrafts( - using: FetchMessageContext( - folderPath: viewModel.path, - count: numberOfInboxItemsToLoad, - pagination: currentMessagesListPagination() - ) - ) - let inboxContext = InboxContext( - data: context.messages.map(InboxRenderable.init), - pagination: context.pagination - ) - handleEndFetching(with: inboxContext, context: batchContext) - } catch { - handle(error: error) - } - } - } - private func getSearchQuery() -> String? { guard searchedExpression.isNotEmpty else { return nil } @@ -231,12 +197,12 @@ extension InboxViewController { return "\(searchedExpression) OR subject:\(searchedExpression)" } - internal func fetchAndRenderEmailsOnly(_ batchContext: ASBatchContext?) { + func fetchAndRenderEmails(_ batchContext: ASBatchContext?) { Task { do { if isSearch { state = .searching - await self.tableNode.reloadData() + await tableNode.reloadData() } else { state = .fetching } @@ -247,7 +213,7 @@ extension InboxViewController { count: numberOfInboxItemsToLoad, searchQuery: getSearchQuery(), pagination: currentMessagesListPagination() - ), userEmail: appContext.user.email + ) ) state = .refresh handleEndFetching(with: context, context: batchContext) @@ -273,7 +239,7 @@ extension InboxViewController { folderPath: viewModel.path, count: messagesToLoad(), pagination: pagination - ), userEmail: appContext.user.email + ) ) state = .fetched(context.pagination) handleEndFetching(with: context, context: batchContext) @@ -350,11 +316,7 @@ extension InboxViewController { shouldBeginFetch = false inboxInput = input.data if inboxInput.isEmpty { - if isSearch { - state = .searchEmpty - } else { - state = .empty - } + state = isSearch ? .searchEmpty : .empty } else { state = .fetched(input.pagination) } @@ -415,7 +377,7 @@ extension InboxViewController { let viewController = try SearchViewController( appContext: appContext, viewModel: viewModel, - provider: self.inboxDataProvider, + provider: inboxDataProvider, isSearch: true ) navigationController?.pushViewController(viewController, animated: false) @@ -434,7 +396,15 @@ extension InboxViewController { Task { do { TapTicFeedback.generate(.light) - let composeVc = try await ComposeViewController(appContext: appContext) + let composeVc = try await ComposeViewController( + appContext: appContext, + handleAction: { [weak self] action in + switch action { + case .update(let identifier), .sent(let identifier), .delete(let identifier): + self?.fetchUpdatedInboxItem(identifier: identifier) + } + } + ) navigationController?.pushViewController(composeVc, animated: true) } catch { showAlert(message: error.localizedDescription) @@ -467,20 +437,20 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - var rowNumber = indexPath.row - if shouldShowEmptyView { - rowNumber -= 1 - } - guard let message = inboxInput[safe: rowNumber] else { + let rowNumber = shouldShowEmptyView ? indexPath.row - 1 : indexPath.row + + guard let inboxItem = inboxInput[safe: rowNumber] else { return } + tableNode.deselectRow(at: indexPath, animated: true) - open(message: message, path: viewModel.path, appContext: appContext) + + open(inboxItem: inboxItem, path: viewModel.path) } private func cellNode(for indexPath: IndexPath, and size: CGSize) -> ASCellNodeBlock { return { [weak self] in - guard let self = self else { return ASCellNode() } + guard let self else { return ASCellNode() } switch self.state { case .empty: @@ -505,15 +475,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { var rowNumber = indexPath.row if self.shouldShowEmptyView { if indexPath.row == 0 { - return EmptyFolderCellNode(path: self.viewModel.path, emptyFolder: { - self.showConfirmAlert( - message: "folder_empty_confirm".localized, - onConfirm: { [weak self] _ in - self?.emptyInboxFolder() - } - ) - - }) + return self.emptyFolderNode() } rowNumber -= 1 } @@ -529,7 +491,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { return InboxCellNode(input: .init(input)) case let .error(message): return TextCellNode( - input: TextCellNode.Input( + input: .init( backgroundColor: .backgroundColor, title: message, withSpinner: false, @@ -540,29 +502,41 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } } + private func emptyFolderNode() -> ASCellNode { + return EmptyFolderCellNode( + path: viewModel.path, + emptyFolder: { [weak self] in + self?.showConfirmAlert( + message: "folder_empty_confirm".localized, + onConfirm: { [weak self] _ in + self?.emptyInboxFolder() + } + ) + }) + } + private func emptyInboxFolder() { Task { do { - self.showSpinner() + showSpinner() try await self.messageOperationsProvider.emptyFolder(path: viewModel.path) self.state = .empty self.inboxInput = [] await tableNode.reloadData() - self.hideSpinner() + hideSpinner() } catch { - self.showAlert(message: error.errorMessage) + showAlert(message: error.errorMessage) } } } } -// MARK: - MsgListViewController -extension InboxViewController: MsgListViewController { - func getUpdatedIndex(for message: InboxRenderable) -> Int? { +extension InboxViewController { + func getUpdatedIndex(for inboxItem: InboxItem) -> Int? { let index = inboxInput.firstIndex(where: { - $0.title == message.title && $0.subtitle == message.subtitle && $0.wrappedType == message.wrappedType + $0.title == inboxItem.title && $0.subtitle == inboxItem.subtitle && $0.type == inboxItem.type }) - logger.logInfo("Try to update message at \(String(describing: index))") + logger.logInfo("Try to update inbox item at \(String(describing: index))") return index } @@ -570,18 +544,9 @@ extension InboxViewController: MsgListViewController { guard inboxInput.count > index else { return } logger.logInfo("Mark as read \(isRead) at \(index)") - inboxInput[index].isRead = isRead // Mark wrapped message/thread(all mails in thread) as read/unread - if var wrappedThread = inboxInput[index].wrappedThread { - for i in 0 ..< wrappedThread.messages.count { - wrappedThread.messages[i].markAsRead(isRead) - } - inboxInput[index].wrappedType = .thread(wrappedThread) - } else if var wrappedMessage = inboxInput[index].wrappedMessage { - wrappedMessage.markAsRead(isRead) - inboxInput[index].wrappedType = .message(wrappedMessage) - } + inboxInput[index].markAsRead(isRead) let animationDuration = 0.3 DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { [weak self] in @@ -592,7 +557,7 @@ extension InboxViewController: MsgListViewController { func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel], at index: Int) { guard inboxInput.count > index else { return } - inboxInput[index].updateMessage(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) + inboxInput[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) let animationDuration = 0.3 DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { [weak self] in @@ -637,4 +602,123 @@ extension InboxViewController: MsgListViewController { } } } + + func open(inboxItem: InboxItem, path: String) { + if inboxItem.isDraft, let draft = inboxItem.messages.first { + open(draft: draft, appContext: appContext) + } else { + Task { + do { + let viewController = try await ThreadDetailsViewController( + appContext: appContext, + inboxItem: inboxItem, + onComposeMessageAction: { [weak self] action in + guard let self else { return } + + switch action { + case .update(let identifier), .sent(let identifier), .delete(let identifier): + self.fetchUpdatedInboxItem(identifier: identifier) + } + }, + completion: { [weak self] action, message in + self?.handleMessageOperation(message: message, action: action) + } + ) + navigationController?.pushViewController(viewController, animated: true) + } catch { + showAlert(message: error.errorMessage) + } + } + } + } + + private func open(draft: Message, appContext: AppContextWithUser) { + Task { + do { + let draftInfo = ComposeMessageInput.MessageQuoteInfo( + message: draft, + processed: nil + ) + + let controller = try await ComposeViewController( + appContext: appContext, + input: .init(type: .draft(draftInfo)), + handleAction: { [weak self] action in + switch action { + case .update(let identifier): + self?.fetchUpdatedInboxItem(identifier: identifier) + case .sent(let identifier), .delete(let identifier): + self?.deleteInboxItem(identifier: identifier) + } + } + ) + navigationController?.pushViewController(controller, animated: true) + } catch { + showAlert(message: error.errorMessage) + } + } + } + + private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { + guard !inboxInput.isEmpty else { + fetchAndRenderEmails(nil) + return + } + + Task { + guard let inboxItem = try await inboxDataProvider.fetchInboxItem( + identifier: identifier, + path: path + ), !inboxItem.messages(with: path).isEmpty else { + deleteInboxItem(identifier: identifier) + return + } + + guard let index = inboxInput.firstIndex(with: identifier) else { + inboxInput.insert(inboxItem, at: 0) + tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + return + } + + inboxInput[index] = inboxItem + tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + } + + private func deleteInboxItem(identifier: MessageIdentifier) { + guard let index = inboxInput.firstIndex(with: identifier) else { return } + + inboxInput.remove(at: index) + + if inboxInput.isEmpty { + state = .empty + tableNode.reloadData() + } else { + tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + } + + // MARK: Operation + private func handleMessageOperation(message: InboxItem, action: MessageAction) { + guard let indexToUpdate = getUpdatedIndex(for: message) else { + return + } + + switch action { + case .markAsRead(let isRead): + updateMessage(isRead: isRead, at: indexToUpdate) + case .moveToTrash, .permanentlyDelete: + removeMessage(at: indexToUpdate) + case .archive, .moveToInbox: + if path.isEmpty { // no need to remove in 'All Mail' folder + updateMessage( + labelsToAdd: action == .moveToInbox ? [.inbox] : [], + labelsToRemove: action == .archive ? [.inbox] : [], + at: indexToUpdate + ) + } else { + removeMessage(at: indexToUpdate) + } + } + } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift index cbe024fdc..bddbdfeb3 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift @@ -10,7 +10,7 @@ import FlowCryptUI import UIKit extension InboxCellNode.Input { - init(_ element: InboxRenderable) { + init(_ element: InboxItem) { let email = element.title let date = element.dateString let msg = element.subtitle @@ -30,10 +30,10 @@ extension InboxCellNode.Input { : .mainTextUnreadColor self.init( - emailText: NSAttributedString.text(from: email, style: style, color: textColor), + emailText: email, countText: { - guard element.messageCount > 1 else { return nil } - let count = element.messageCount > 99 ? "99+" : String(element.messageCount) + guard element.messages.count > 1 else { return nil } + let count = element.messages.count > 99 ? "99+" : String(element.messages.count) return NSAttributedString.text(from: "(\(count))", style: style, color: textColor) }(), dateText: NSAttributedString.text(from: date, style: style, color: dateColor), @@ -49,7 +49,8 @@ struct InboxViewDecorator { backgroundColor: .backgroundColor, title: title + " " + "empty".localized, size: size, - imageName: imageName + imageName: imageName, + accessibilityIdentifier: "aid-empty-cell-node" ) } diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift deleted file mode 100644 index 38575db08..000000000 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// MsgListViewController.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 23/12/2019. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import UIKit - -@MainActor -protocol MsgListViewController { - var path: String { get } - - func open(message: InboxRenderable, path: String, appContext: AppContextWithUser) - - func getUpdatedIndex(for message: InboxRenderable) -> Int? - func updateMessage(isRead: Bool, at index: Int) - func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel], at index: Int) - func removeMessage(at index: Int) -} - -extension MsgListViewController where Self: UIViewController { - - // todo - tom - don't know how to add AppContext into init of protocol/extension - func open(message: InboxRenderable, path: String, appContext: AppContextWithUser) { - switch message.wrappedType { - case .message(let message): - open(message: message, path: path, appContext: appContext) - case .thread(let thread): - open(thread: thread, appContext: appContext) - } - } - - // TODO: uncomment in "sent message from draft" feature - private func open(draft: Message, appContext: AppContextWithUser) { - Task { - do { - let controller = try await ComposeViewController(appContext: appContext) - controller.update(with: draft) - navigationController?.pushViewController(controller, animated: true) - } catch { - showAlert(message: error.localizedDescription) - } - } - } - - private func open(message: Message, path: String, appContext: AppContextWithUser) { - let thread = MessageThread( - identifier: message.threadId, - snippet: nil, - path: path, - messages: [message] - ) - open(thread: thread, appContext: appContext) - } - - private func open(thread: MessageThread, appContext: AppContextWithUser) { - Task { - do { - let viewController = try await ThreadDetailsViewController( - appContext: appContext, - thread: thread - ) { [weak self] (action, message) in - self?.handleMessageOperation(message: message, action: action) - } - navigationController?.pushViewController(viewController, animated: true) - } catch { - showAlert(message: error.localizedDescription) - } - } - } - - // MARK: Operation - private func handleMessageOperation(message: InboxRenderable, action: MessageAction) { - guard let indexToUpdate = getUpdatedIndex(for: message) else { - return - } - - switch action { - case .markAsRead(let isRead): - updateMessage(isRead: isRead, at: indexToUpdate) - case .moveToTrash, .permanentlyDelete: - removeMessage(at: indexToUpdate) - case .archive, .moveToInbox: - if path.isEmpty { // no need to remove in 'All Mail' folder - updateMessage( - labelsToAdd: action == .moveToInbox ? [.inbox] : [], - labelsToRemove: action == .archive ? [.inbox] : [], - at: indexToUpdate - ) - } else { - removeMessage(at: indexToUpdate) - } - } - } -} diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index 6edf21249..691bab5f9 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -15,12 +15,15 @@ class SearchViewController: InboxViewController { private let searchController = UISearchController(searchResultsController: nil) override func viewDidLoad() { - self.setupSearchUI() - self.setupSearch() + super.viewDidLoad() + + setupSearchUI() + setupSearch() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + searchController.isActive = true } @@ -86,14 +89,13 @@ extension SearchViewController: UISearchControllerDelegate, UISearchBarDelegate // MARK: - UISearchResultsUpdating extension SearchViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { - guard searchController.isActive else { - searchTask?.cancel() - return - } - guard let searchText = searchText(for: searchController.searchBar) else { + guard searchController.isActive, + let searchText = searchText(for: searchController.searchBar) + else { searchTask?.cancel() return } + guard searchedExpression != searchText else { return } @@ -123,6 +125,6 @@ extension SearchViewController: UISearchResultsUpdating { private func search(for searchText: String) { searchedExpression = searchText - fetchAndRenderEmailsOnly(nil) + fetchAndRenderEmails(nil) } } diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift index f5aa923a3..46aa00eb0 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift @@ -9,8 +9,7 @@ import UIKit struct BackupViewDecorator { - let sceneTitle: String = "backup_screen_title" - .localized + let sceneTitle = "backup_screen_title".localized func buttonTitle(for state: BackupViewController.State) -> NSAttributedString { (state.hasAnyBackups ? "backup_screen_found_action" : "backup_screen_not_found_action") diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift index 096812000..47bed1ae7 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift @@ -8,7 +8,6 @@ import AsyncDisplayKit import FlowCryptUI -import Foundation final class BackupSelectKeyViewController: TableNodeViewController { @@ -63,10 +62,10 @@ extension BackupSelectKeyViewController { // MARK: - Actions extension BackupSelectKeyViewController { @objc private func handleSave() { - if backupsContext.filter({ $0.1 == true }).isEmpty { - showAlert(message: "backup_select_key_screen_no_selection".localized) - } else { + if backupsContext.contains(where: { $0.1 == true }) { makeBackup() + } else { + showAlert(message: "backup_select_key_screen_no_selection".localized) } } diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift index e495c0d66..9ea0b8f37 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift @@ -7,7 +7,6 @@ // import FlowCryptUI -import Foundation struct ContactDetailDecorator { let title = "contact_detail_screen_title".localized diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift index c90039738..62ea9037a 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift @@ -7,7 +7,6 @@ // import FlowCryptUI -import Foundation struct ContactKeyDetailDecorator { let title = "contact_key_detail_screen_title".localized diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift index 4824f715a..2f46465c7 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift @@ -2,7 +2,7 @@ // KeySettingsItemDecorator.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 12/13/19. +// Created by Anton Kharchevskyi on 12/13/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift index 33b807346..852c176a9 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift @@ -9,7 +9,6 @@ import FlowCryptCommon import FlowCryptUI import UIKit -import Foundation extension DateFormatter { static let keySettingsFormatter: DateFormatter = { diff --git a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift index 44936b3a1..95a7a9963 100644 --- a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift @@ -33,7 +33,7 @@ final class SettingsViewController: TableNodeViewController { } static func filtered(with rules: ClientConfiguration) -> [SettingsMenuItem] { - var cases = SettingsMenuItem.allCases + var cases = Self.allCases if !rules.canBackupKeys { cases.removeAll(where: { $0 == .backups }) diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift index 916a60191..d9498dd09 100644 --- a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -146,7 +146,7 @@ extension SetupCreatePassphraseAbstractViewController { } private func awaitUserPassPhraseEntry() async throws -> String? { - return await withCheckedContinuation { (continuation: CheckedContinuation) in + return await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( title: "setup_pass_phrase_title".localized, diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index 3a0d0e5c0..f12b5187a 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -97,7 +97,7 @@ final class SetupGenerateKeyViewController: SetupCreatePassphraseAbstractViewCon ) throws { try appContext.encryptedStorage.putKeypairs( keyDetails: [encryptedPrv.key], - passPhrase: storageMethod == .persistent ? passPhrase: nil, + passPhrase: storageMethod == .persistent ? passPhrase : nil, source: .generated, for: appContext.user.email ) diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index b31cf31fa..1113f149f 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -177,7 +177,7 @@ extension SetupInitialViewController { func showRetryAlert(for errorMessage: String) { showRetryAlert( message: errorMessage, - cancelActionTitle: "log_out".localized, + cancelButtonTitle: "log_out".localized, onRetry: { [weak self] _ in self?.state = .fetchingKeysFromEKM }, diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift index b8ddb4d6e..5bc598099 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift @@ -162,17 +162,19 @@ extension SetupImapViewController { NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillShowNotification, object: nil, - queue: .main) { [weak self] notification in - guard let self = self else { return } - self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) - } + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) + } NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillHideNotification, object: nil, - queue: .main) { [weak self] _ in - self?.adjustForKeyboard(height: 0) - } + queue: .main + ) { [weak self] _ in + self?.adjustForKeyboard(height: 0) + } } private func adjustForKeyboard(height: CGFloat) { @@ -183,10 +185,7 @@ extension SetupImapViewController { private func update(for newState: State) { state = newState - node.reloadSections( - IndexSet(integer: 3), - with: .fade - ) + node.reloadSections([3], with: .fade) node.scrollToRow( at: IndexPath(row: 2, section: 3), @@ -290,14 +289,14 @@ extension SetupImapViewController { private func reloadImapSection() { node.reloadSections( - IndexSet(integer: Section.imap(.port).section), + [Section.imap(.port).section], with: .none ) } private func reloadSmtpSection() { node.reloadSections( - IndexSet(integer: Section.smtp(.port).section), + [Section.smtp(.port).section], with: .none ) } diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift index f661d916e..98ffdf335 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import ENSwiftSideMenu import FlowCryptUI +import UIKit /** * Menu view controller @@ -30,8 +31,8 @@ final class MyMenuViewController: ViewController { var arrowImage: UIImage? { switch self { - case .folders: return #imageLiteral(resourceName: "arrow_down").tinted(.white) - case .accountAdding: return #imageLiteral(resourceName: "arrow_up").tinted(.white) + case .folders: return UIImage(named: "arrow_down")?.tinted(.white) + case .accountAdding: return UIImage(named: "arrow_up")?.tinted(.white) } } } diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift index 9d55f9b56..170bbd905 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift @@ -72,7 +72,7 @@ extension InfoCellNode.Input { attributedText: "folder_add_account" .localized .attributed(.regular(17), color: .mainTextColor), - image: #imageLiteral(resourceName: "plus").tinted(.mainTextColor), + image: UIImage(named: "plus")?.tinted(.mainTextColor), insets: .side(16), backgroundColor: .backgroundColor, accessibilityIdentifier: "aid-add-account-btn" diff --git a/FlowCrypt/Controllers/Threads/AlertsFactory.swift b/FlowCrypt/Controllers/Threads/AlertsFactory.swift index a30854eed..2ec429189 100644 --- a/FlowCrypt/Controllers/Threads/AlertsFactory.swift +++ b/FlowCrypt/Controllers/Threads/AlertsFactory.swift @@ -35,6 +35,7 @@ class AlertsFactory { alert.addTextField { [weak self] tf in tf.isSecureTextEntry = true tf.delegate = self?.textFieldDelegate + tf.accessibilityIdentifier = "aid-message-passphrase-textfield" } let saveAction = UIAlertAction( title: "ok".localized, diff --git a/FlowCrypt/Controllers/Threads/MessageAction.swift b/FlowCrypt/Controllers/Threads/MessageAction.swift index 684aab5e5..bf424cc12 100644 --- a/FlowCrypt/Controllers/Threads/MessageAction.swift +++ b/FlowCrypt/Controllers/Threads/MessageAction.swift @@ -8,7 +8,7 @@ import Foundation -typealias MessageActionCompletion = (MessageAction, InboxRenderable) -> Void +typealias MessageActionCompletion = (MessageAction, InboxItem) -> Void enum MessageAction: Equatable { case moveToTrash, moveToInbox, archive, markAsRead(Bool), permanentlyDelete diff --git a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift index bb717bb8f..a6fd6d95e 100644 --- a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift +++ b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift @@ -30,11 +30,11 @@ extension MessageActionsHandler where Self: UIViewController { Logger.nested("MessageActions") } - func setupNavigationBar(thread: MessageThread) { + func setupNavigationBar(inboxItem: InboxItem) { Task { do { let path = try await trashFolderProvider.trashFolderPath - setupNavigationBarItems(thread: thread, trashFolderPath: path) + setupNavigationBarItems(inboxItem: inboxItem, trashFolderPath: path) } catch { // todo - handle? logger.logError("setupNavigationBar: \(error)") @@ -42,7 +42,7 @@ extension MessageActionsHandler where Self: UIViewController { } } - private func setupNavigationBarItems(thread: MessageThread, trashFolderPath: String?) { + private func setupNavigationBarItems(inboxItem: InboxItem, trashFolderPath: String?) { logger.logInfo("setup navigation bar with \(trashFolderPath ?? "N/A")") logger.logInfo("currentFolderPath \(currentFolderPath)") @@ -92,10 +92,10 @@ extension MessageActionsHandler where Self: UIViewController { default: // in any other folders items = [helpButton, trashButton, unreadButton] - if thread.isInbox { + if inboxItem.isInbox { logger.logInfo("inbox - helpButton, archiveButton, trashButton, unreadButton") items.insert(archiveButton, at: 1) - } else if thread.shouldShowMoveToInboxButton { + } else if inboxItem.shouldShowMoveToInboxButton { logger.logInfo("archive - helpButton, moveToInboxButton, trashButton, unreadButton") items.insert(moveToInboxButton, at: 1) } else { @@ -126,29 +126,19 @@ extension MessageActionsHandler where Self: UIViewController { private func deleteMessage(trashPath: String) { guard currentFolderPath.caseInsensitiveCompare(trashPath) != .orderedSame else { - awaitUserDeleteConfirmation { [weak self] in - self?.permanentlyDelete() - } + showAlertWithAction( + title: "message_permanently_delete_title".localized, + message: "message_permanently_delete".localized, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + self?.permanentlyDelete() + } + ) + return } moveToTrash(with: trashPath) } - - private func awaitUserDeleteConfirmation(_ completion: @escaping () -> Void) { - let alert = UIAlertController( - title: "message_permanently_delete_title".localized, - message: "message_permanently_delete".localized, - preferredStyle: .alert - ) - alert.addAction( - UIAlertAction(title: "cancel".localized, style: .default) - ) - alert.addAction( - UIAlertAction(title: "delete".localized, style: .destructive) { _ in - completion() - } - ) - present(alert, animated: true, completion: nil) - } } diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index 9efc1808b..7650f3c88 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -16,41 +16,5 @@ struct MessageThreadContext { struct MessageThread: Equatable { let identifier: String? let snippet: String? - let path: String var messages: [Message] - - var subject: String? { - messages - .compactMap(\.subject) - .first(where: { $0.isNotEmpty }) - } - - var labels: Set { - Set(messages.flatMap(\.labels)) - } - - var isInbox: Bool { - labels.contains(.inbox) - } - - var shouldShowMoveToInboxButton: Bool { - guard let firstMessageLabels = messages.first?.labels else { - return false - } - // Thread is treaded as archived when labels don't contain `inbox` and first message label doesn't contain sent label - // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r931874353 - return !isInbox && !firstMessageLabels.contains(.sent) - } - - var isRead: Bool { - !messages.contains(where: { !$0.isMessageRead }) - } -} - -extension MessageThread { - mutating func update(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel]) { - for index in messages.indices { - messages[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - } - } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index 499b00888..488566680 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -19,7 +19,7 @@ extension ThreadMessageInfoCellNode.Input { .joined(separator: ", ") let recipientLabel = [recipientPrefix, recipientsList].joined(separator: " ") let date = DateFormatter().formatDate(threadMessage.rawMessage.date) - let isMessageRead = threadMessage.rawMessage.isMessageRead + let isMessageRead = threadMessage.rawMessage.isRead let style: NSAttributedString.Style = isMessageRead ? .regular(16) @@ -74,11 +74,14 @@ extension AttachmentNode.Input { } } -private func makeEncryptionBadge(_ input: ThreadDetailsViewController.Input) -> BadgeNode.Input { +private func makeEncryptionBadge(_ input: ThreadDetailsViewController.Input) -> BadgeNode.Input? { + guard let type = input.processedMessage?.type else { return nil } + let icon: String let text: String let color: UIColor - switch input.processedMessage?.type { + + switch type { case .error: icon = "lock.open" text = "message_decrypt_error".localized diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 9ce207587..7fd536d43 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -9,7 +9,6 @@ import AsyncDisplayKit import FlowCryptUI import FlowCryptCommon -import Foundation import UIKit final class ThreadDetailsViewController: TableNodeViewController { @@ -22,34 +21,43 @@ final class ThreadDetailsViewController: TableNodeViewController { var shouldShowRecipientsList: Bool var processedMessage: ProcessedMessage? - init(message: Message, isExpanded: Bool = false, shouldShowRecipientsList: Bool = false) { + init( + message: Message, + isExpanded: Bool = false, + shouldShowRecipientsList: Bool = false, + processedMessage: ProcessedMessage? = nil + ) { self.rawMessage = message self.isExpanded = isExpanded self.shouldShowRecipientsList = shouldShowRecipientsList + self.processedMessage = processedMessage } } private enum Parts: Int, CaseIterable { - case thread, message + case thread, message, divider } private let appContext: AppContextWithUser + private let draftGateway: DraftGateway? private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider - private let thread: MessageThread + private var inboxItem: InboxItem private var input: [ThreadDetailsViewController.Input] let trashFolderProvider: TrashFolderProviderType var currentFolderPath: String { - thread.path + inboxItem.folderPath } + private let onComposeMessageAction: ((ComposeMessageAction) -> Void)? private let onComplete: MessageActionCompletion init( appContext: AppContextWithUser, messageService: MessageService? = nil, - thread: MessageThread, + inboxItem: InboxItem, + onComposeMessageAction: ((ComposeMessageAction) -> Void)?, completion: @escaping MessageActionCompletion ) async throws { self.appContext = appContext @@ -58,6 +66,7 @@ final class ThreadDetailsViewController: TableNodeViewController { encryptedStorage: appContext.encryptedStorage ) let mailProvider = try appContext.getRequiredMailProvider() + self.draftGateway = try mailProvider.draftGateway self.messageService = try messageService ?? MessageService( localContactsProvider: localContactsProvider, pubLookup: PubLookup(clientConfiguration: clientConfiguration, localContactsProvider: localContactsProvider), @@ -65,8 +74,7 @@ final class ThreadDetailsViewController: TableNodeViewController { messageProvider: try mailProvider.messageProvider, combinedPassPhraseStorage: appContext.combinedPassPhraseStorage ) - let threadOperationsProvider = try mailProvider.threadOperationsProvider - self.threadOperationsProvider = threadOperationsProvider + self.threadOperationsProvider = try mailProvider.threadOperationsProvider self.messageOperationsProvider = try mailProvider.messageOperationsProvider self.trashFolderProvider = TrashFolderProvider( user: appContext.user, @@ -75,9 +83,10 @@ final class ThreadDetailsViewController: TableNodeViewController { remoteFoldersProvider: try mailProvider.remoteFoldersProvider ) ) - self.thread = thread + self.inboxItem = inboxItem + self.onComposeMessageAction = onComposeMessageAction self.onComplete = completion - self.input = thread.messages + self.input = inboxItem.messages .sorted(by: >) .map { Input(message: $0) } @@ -94,15 +103,21 @@ final class ThreadDetailsViewController: TableNodeViewController { node.delegate = self node.dataSource = self - setupNavigationBar(thread: thread) + setupNavigationBar(inboxItem: inboxItem) expandThreadMessageAndMarkAsRead() } private func expandThreadMessageAndMarkAsRead() { Task { - try await threadOperationsProvider.mark(thread: thread, asRead: true, in: currentFolderPath) + try await threadOperationsProvider.mark( + messagesIds: inboxItem.messages.map(\.identifier), + asRead: true, + in: inboxItem.folderPath + ) } - let indexOfSectionToExpand = input.firstIndex(where: { $0.rawMessage.isMessageRead == false }) ?? input.count - 1 + let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) + ?? input.lastIndex(where: { !$0.rawMessage.isDraft }) + ?? input.count - 1 let indexPath = IndexPath(row: 0, section: indexOfSectionToExpand + 1) handleExpandTap(at: indexPath) } @@ -111,24 +126,21 @@ final class ThreadDetailsViewController: TableNodeViewController { extension ThreadDetailsViewController { private func handleExpandTap(at indexPath: IndexPath) { - guard let threadNode = node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode else { - logger.logError("Fail to handle tap at \(indexPath)") - return - } - input[indexPath.section - 1].isExpanded.toggle() - if input[indexPath.section-1].isExpanded { + if input[indexPath.section - 1].isExpanded { UIView.animate( - withDuration: 0.3, + withDuration: 0.2, animations: { - threadNode.expandNode.view.alpha = 0 + if let threadNode = self.node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode { + threadNode.expandNode.view.alpha = 0 + } }, completion: { [weak self] _ in - guard let self = self else { return } + guard let self else { return } - if let processedMessage = self.input[indexPath.section-1].processedMessage { - self.handleReceived(message: processedMessage, at: indexPath) + if let processedMessage = self.input[indexPath.section - 1].processedMessage { + self.handle(processedMessage: processedMessage, at: indexPath) } else { self.fetchDecryptAndRenderMsg(at: indexPath) } @@ -136,7 +148,7 @@ extension ThreadDetailsViewController { ) } else { UIView.animate(withDuration: 0.3) { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + self.node.reloadSections([indexPath.section], with: .automatic) } } } @@ -145,7 +157,7 @@ extension ThreadDetailsViewController { input[indexPath.section - 1].shouldShowRecipientsList.toggle() UIView.animate(withDuration: 0.3) { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + self.node.reloadSections([indexPath.section], with: .automatic) } } @@ -153,6 +165,30 @@ extension ThreadDetailsViewController { composeNewMessage(at: indexPath, quoteType: .reply) } + private func handleDraftTap(at indexPath: IndexPath) { + Task { + do { + let draft = input[indexPath.section - 1] + + let draftInfo = ComposeMessageInput.MessageQuoteInfo( + message: draft.rawMessage, + processed: draft.processedMessage + ) + + let controller = try await ComposeViewController( + appContext: appContext, + input: .init(type: .draft(draftInfo)), + handleAction: { [weak self] action in + self?.handleComposeMessageAction(action) + } + ) + navigationController?.pushViewController(controller, animated: true) + } catch { + showAlert(message: error.errorMessage) + } + } + } + private func handleMenuTap(at indexPath: IndexPath) { let alert = UIAlertController( title: nil, @@ -176,12 +212,90 @@ extension ThreadDetailsViewController { present(alert, animated: true, completion: nil) } + private func handleComposeMessageAction(_ action: ComposeMessageAction) { + onComposeMessageAction?(action) + + switch action { + case .update(let identifier): + updateMessage(identifier: identifier) + case .sent(let identifier): + handleSentMessage(identifier: identifier) + case .delete(let identifier): + deleteMessage(identifier: identifier) + } + } + + private func updateMessage(identifier: MessageIdentifier) { + Task { + guard let messageId = identifier.messageId else { return } + + let processedMessage = try await getAndProcessMessage( + identifier: messageId, + folder: inboxItem.folderPath + ) + + let section: Int + if let index = input.firstIndex(where: { $0.rawMessage.identifier == identifier.draftMessageId }) { + section = index + 1 + } else { + section = input.count + 1 + } + + handle(processedMessage: processedMessage, at: IndexPath(row: 0, section: section)) + } + } + + private func handleSentMessage(identifier: MessageIdentifier) { + Task { + if let draftId = identifier.draftId, + let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) + } + + guard let messageId = identifier.messageId else { return } + + let processedMessage = try await getAndProcessMessage( + identifier: messageId, + folder: inboxItem.folderPath + ) + let indexPath = IndexPath(row: 0, section: input.count + 1) + handle(processedMessage: processedMessage, at: indexPath) + } + } + + private func deleteMessage(identifier: MessageIdentifier) { + guard let messageId = identifier.messageId, + let index = input.firstIndex(where: { + $0.rawMessage.identifier == messageId + }) + else { return } + + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) + } + + private func getAndProcessMessage( + identifier: Identifier, + folder: String, + onlyLocalKeys: Bool = false + ) async throws -> ProcessedMessage { + return try await messageService.getAndProcess( + identifier: identifier, + folder: folder, + onlyLocalKeys: onlyLocalKeys, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + } + private func createComposeNewMessageAlertAction(at indexPath: IndexPath, type: MessageQuoteType) -> UIAlertAction { let action = UIAlertAction( title: type.actionLabel, - style: .default) { [weak self] _ in - self?.composeNewMessage(at: indexPath, quoteType: type) - } + style: .default + ) { [weak self] _ in + self?.composeNewMessage(at: indexPath, quoteType: type) + } action.accessibilityIdentifier = type.accessibilityIdentifier return action } @@ -202,7 +316,7 @@ extension ThreadDetailsViewController { defer { node.reloadRows(at: [indexPath], with: .automatic) } let trace = Trace(id: "Attachment") - let section = input[indexPath.section-1] + let section = input[indexPath.section - 1] let attachmentIndex = indexPath.row - 2 guard var attachment = section.processedMessage?.attachments[attachmentIndex] else { @@ -229,11 +343,11 @@ extension ThreadDetailsViewController { ) logger.logInfo("Got encrypted attachment - \(trace.finish())") - input[indexPath.section-1].processedMessage?.attachments[attachmentIndex] = decryptedAttachment + input[indexPath.section - 1].processedMessage?.attachments[attachmentIndex] = decryptedAttachment return decryptedAttachment } else { logger.logInfo("Got not encrypted attachment - \(trace.finish())") - input[indexPath.section-1].processedMessage?.attachments[attachmentIndex] = attachment + input[indexPath.section - 1].processedMessage?.attachments[attachmentIndex] = attachment return attachment } } @@ -244,7 +358,7 @@ extension ThreadDetailsViewController { } private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) { - guard let input = input[safe: indexPath.section-1], + guard let input = input[safe: indexPath.section - 1], let processedMessage = input.processedMessage else { return } @@ -280,18 +394,23 @@ extension ThreadDetailsViewController { let subject = input.rawMessage.subject ?? "(no subject)" let threadId = quoteType == .forward ? nil : input.rawMessage.threadId - let replyToMsgId = input.rawMessage.identifier.stringId + let replyToMsgId = quoteType == .forward ? nil : input.rawMessage.rfc822MsgId let replyInfo = ComposeMessageInput.MessageQuoteInfo( + id: nil, recipients: recipients, ccRecipients: ccRecipients, + bccRecipients: [], sender: input.rawMessage.sender, subject: [quoteType.subjectPrefix, subject].joined(), sentDate: input.rawMessage.date, - message: processedMessage.text, + text: processedMessage.text, threadId: threadId, replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, + rfc822MsgId: input.rawMessage.rfc822MsgId, + draftId: nil, + shouldEncrypt: input.rawMessage.isPgp, attachments: attachments ) @@ -304,16 +423,18 @@ extension ThreadDetailsViewController { } }() - let composeInput = ComposeMessageInput(type: composeType) - Task { do { - navigationController?.pushViewController( - try await ComposeViewController(appContext: appContext, input: composeInput), - animated: true + let composeVC = try await ComposeViewController( + appContext: appContext, + input: ComposeMessageInput(type: composeType), + handleAction: { [weak self] action in + self?.handleComposeMessageAction(action) + } ) + navigationController?.pushViewController(composeVC, animated: true) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } @@ -321,73 +442,85 @@ extension ThreadDetailsViewController { extension ThreadDetailsViewController { private func fetchDecryptAndRenderMsg(at indexPath: IndexPath) { - let message = input[indexPath.section-1].rawMessage + let message = input[indexPath.section - 1].rawMessage logger.logInfo("Start loading message") handleFetchProgress(state: .fetch) Task { do { - var processedMessage = try await messageService.getAndProcess( - message: message, - folder: thread.path, - onlyLocalKeys: true, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + var processedMessage = try await getAndProcessMessage( + identifier: message.identifier, + folder: inboxItem.folderPath, + onlyLocalKeys: true ) if case .missingPubkey = processedMessage.signature { processedMessage.signature = .pending retryVerifyingSignatureWithRemotelyFetchedKeys( message: message, - folder: thread.path, + folder: inboxItem.folderPath, indexPath: indexPath ) } - handleReceived(message: processedMessage, at: indexPath) + handle(processedMessage: processedMessage, at: indexPath) } catch { - handleError(error, at: indexPath) + handle(error: error, at: indexPath) } } } - private func handleReceived(message processedMessage: ProcessedMessage, at indexPath: IndexPath) { + private func handle(processedMessage: ProcessedMessage, at indexPath: IndexPath) { hideSpinner() let messageIndex = indexPath.section - 1 - let isAlreadyProcessed = input[messageIndex].processedMessage != nil + let isAlreadyProcessed = messageIndex < input.count && input[messageIndex].processedMessage != nil if !isAlreadyProcessed { - input[messageIndex].processedMessage = processedMessage - input[messageIndex].isExpanded = true + if messageIndex < input.count { + input[messageIndex].rawMessage = processedMessage.message + input[messageIndex].processedMessage = processedMessage + input[messageIndex].isExpanded = true + } else { + input.append(Input(message: processedMessage.message, isExpanded: true, processedMessage: processedMessage)) + } UIView.animate( withDuration: 0.2, animations: { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + if indexPath.section < self.node.numberOfSections { + self.node.reloadSections([indexPath.section], with: .automatic) + } else { + self.node.insertSections([indexPath.section], with: .automatic) + if indexPath.section > 0 { + self.node.reloadSections([indexPath.section - 1], with: .automatic) + } + } }, completion: { [weak self] _ in self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) + self?.decryptDrafts() }) } else { - input[messageIndex].processedMessage?.signature = processedMessage.signature - node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + input[messageIndex].rawMessage = processedMessage.message + input[messageIndex].processedMessage = processedMessage + node.reloadSections([indexPath.section], with: .automatic) } } - private func handleError(_ error: Error, at indexPath: IndexPath) { + private func handle(error: Error, at indexPath: IndexPath) { logger.logInfo("Error \(error)") hideSpinner() switch error as? MessageServiceError { - case let .missingPassPhrase(message): - handleWrongPassPhrase(for: message, at: indexPath) + case .missingPassPhrase: + handleWrongPassPhrase(indexPath: indexPath) default: // TODO: - Ticket - Improve error handling for ThreadDetailsViewController if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { // todo - the missing msg should be removed from the list in inbox view // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox - showToast("Message not found in folder: \(thread.path)") + showToast("message_not_found_in_folder".localized + inboxItem.folderPath) } else { showRetryAlert(message: error.errorMessage, onRetry: { [weak self] _ in self?.fetchDecryptAndRenderMsg(at: indexPath) @@ -400,32 +533,23 @@ extension ThreadDetailsViewController { } private func handleAttachmentDecryptError(_ error: Error, at indexPath: IndexPath) { - hideSpinner() let message = "message_attachment_corrupted_file".localized - let alertController = UIAlertController( + showAlertWithAction( title: "message_attachment_decrypt_error".localized, message: "\n\(error.errorMessage)\n\n\(message)", - preferredStyle: .alert - ) - - let downloadAction = UIAlertAction(title: "download".localized, style: .default) { [weak self] _ in - guard let attachment = self?.input[indexPath.section-1].processedMessage?.attachments[indexPath.row-2] else { - return + actionButtonTitle: "download".localized, + actionAccessibilityIdentifier: "aid-download-button", + onAction: { [weak self] _ in + guard let attachment = self?.input[indexPath.section - 1].processedMessage?.attachments[indexPath.row - 2] else { + return + } + self?.show(attachment: attachment) } - self?.show(attachment: attachment) - } - downloadAction.accessibilityIdentifier = "aid-download-button" - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) - cancelAction.accessibilityIdentifier = "aid-cancel-button" - - alertController.addAction(downloadAction) - alertController.addAction(cancelAction) - - present(alertController, animated: true) + ) } - private func handleWrongPassPhrase(_ passPhrase: String? = nil, for message: Message, at indexPath: IndexPath) { + private func handleWrongPassPhrase(_ passPhrase: String? = nil, indexPath: IndexPath) { let title = passPhrase == nil ? "setup_enter_pass_phrase".localized : "setup_wrong_pass_phrase_retry".localized @@ -436,14 +560,14 @@ extension ThreadDetailsViewController { self?.navigationController?.popViewController(animated: true) }, onCompletion: { [weak self] passPhrase in - self?.handlePassPhraseEntry(message: message, with: passPhrase, at: indexPath) + self?.handlePassPhraseEntry(passPhrase, indexPath: indexPath) } ) present(alert, animated: true, completion: nil) } - private func handlePassPhraseEntry(message: Message, with passPhrase: String, at indexPath: IndexPath) { + private func handlePassPhraseEntry(_ passPhrase: String, indexPath: IndexPath) { presentedViewController?.dismiss(animated: true) handleFetchProgress(state: .decrypt) @@ -454,21 +578,44 @@ extension ThreadDetailsViewController { passPhrase, userEmail: appContext.user.email ) + if matched { - let sender = input[indexPath.section-1].rawMessage.sender + let message = input[indexPath.section - 1].rawMessage + let processedMessage = try await messageService.decryptAndProcess( message: message, - sender: sender, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - handleReceived(message: processedMessage, at: indexPath) + + handle(processedMessage: processedMessage, at: indexPath) } else { - handleWrongPassPhrase(passPhrase, for: message, at: indexPath) + handleWrongPassPhrase(passPhrase, indexPath: indexPath) } } catch { - handleError(error, at: indexPath) + handle(error: error, at: indexPath) + } + } + } + + private func decryptDrafts() { + Task { + for (index, data) in input.enumerated() { + guard data.rawMessage.isDraft && data.rawMessage.isPgp && data.processedMessage == nil else { continue } + let indexPath = IndexPath(row: 0, section: index + 1) + do { + let decryptedText = try await messageService.decrypt( + text: data.rawMessage.body.text, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + + let processedMessage = ProcessedMessage(message: data.rawMessage, text: decryptedText, type: .plain, attachments: []) + handle(processedMessage: processedMessage, at: indexPath) + } catch { + handle(error: error, at: indexPath) + } } } } @@ -480,17 +627,14 @@ extension ThreadDetailsViewController { ) { Task { do { - let processedMessage = try await messageService.getAndProcess( - message: message, - folder: thread.path, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + let processedMessage = try await getAndProcessMessage( + identifier: message.identifier, + folder: inboxItem.folderPath ) - handleReceived(message: processedMessage, at: indexPath) + handle(processedMessage: processedMessage, at: indexPath) } catch { let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) - input[indexPath.section-1].processedMessage?.signature = .error(message) + input[indexPath.section - 1].processedMessage?.signature = .error(message) } } } @@ -508,36 +652,38 @@ extension ThreadDetailsViewController { } extension ThreadDetailsViewController: MessageActionsHandler { - private func handleSuccessfulMessage(action: MessageAction) { + private func handle(action: MessageAction, error: Error? = nil) { hideSpinner() + + if let error = error { + logger.logError("\(action.error ?? "Error: ") \(error)") + return + } + onComplete( action, - .init(thread: thread, folderPath: currentFolderPath) + inboxItem ) - navigationController?.popViewController(animated: true) - } - private func handleMessageAction(error: Error) { - logger.logError("Error mark as read \(error)") - hideSpinner() + navigationController?.popViewController(animated: true) } func permanentlyDelete() { logger.logInfo("permanently delete") - handle(action: .permanentlyDelete) + perform(action: .permanentlyDelete) } func moveToTrash(with trashPath: String) { logger.logInfo("move to trash \(trashPath)") - handle(action: .moveToTrash) + perform(action: .moveToTrash) } func handleArchiveTap() { - handle(action: .archive) + perform(action: .archive) } func handleMoveToInboxTap() { - handle(action: .moveToInbox) + perform(action: .moveToInbox) } func handleMarkUnreadTap() { @@ -545,33 +691,39 @@ extension ThreadDetailsViewController: MessageActionsHandler { guard messages.isNotEmpty else { return } - handle(action: .markAsRead(false)) + perform(action: .markAsRead(false)) } - func handle(action: MessageAction) { + func perform(action: MessageAction) { Task { do { showSpinner() switch action { case .archive: - try await threadOperationsProvider.archive(thread: thread, in: currentFolderPath) + try await threadOperationsProvider.archive( + messagesIds: inboxItem.messages.map(\.identifier), + in: inboxItem.folderPath + ) case .markAsRead(let isRead): guard !isRead else { return } Task { // Run mark as unread operation in another thread - try await threadOperationsProvider.mark(thread: thread, asRead: false, in: currentFolderPath) + try await threadOperationsProvider.markThreadAsUnread( + id: inboxItem.threadId, + folder: inboxItem.folderPath + ) } case .moveToTrash: - try await threadOperationsProvider.moveThreadToTrash(thread: thread) + try await threadOperationsProvider.moveThreadToTrash(id: inboxItem.threadId, labels: inboxItem.labels) case .moveToInbox: - try await threadOperationsProvider.moveThreadToInbox(thread: thread) + try await threadOperationsProvider.moveThreadToInbox(id: inboxItem.threadId) case .permanentlyDelete: - try await threadOperationsProvider.delete(thread: thread) + try await threadOperationsProvider.delete(id: inboxItem.threadId) } - handleSuccessfulMessage(action: action) + handle(action: action) } catch { - handleMessageAction(error: error) + handle(action: action, error: error) } } } @@ -583,25 +735,31 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { - guard section > 0, input[section-1].isExpanded else { return 1 } + guard section > 0, input[section - 1].isExpanded, + !input[section - 1].rawMessage.isDraft + else { return 2 } - let attachmentsCount = input[section-1].processedMessage?.attachments.count ?? 0 + let attachmentsCount = input[section - 1].processedMessage?.attachments.count ?? 0 return Parts.allCases.count + attachmentsCount } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self else { return ASCellNode() } + guard let self else { return ASCellNode() } guard indexPath.section > 0 else { - let subject = self.thread.subject ?? "no subject" - return MessageSubjectNode(subject.attributed(.medium(18))) + if indexPath.row == 0 { + let subject = self.inboxItem.subject ?? "no subject" + return MessageSubjectNode(subject.attributed(.medium(18))) + } else { + return self.dividerNode(indexPath: indexPath) + } } let messageIndex = indexPath.section - 1 let message = self.input[messageIndex] - if indexPath.row == 0 { + if !message.rawMessage.isDraft && indexPath.row == 0 { return ThreadMessageInfoCellNode( input: .init(threadMessage: message, index: messageIndex), onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, @@ -610,22 +768,32 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { ) } - guard let processedMessage = message.processedMessage else { - return ASCellNode() + if message.rawMessage.isDraft { + if indexPath.row == 0 { + return self.draftNode(messageIndex: messageIndex, isExpanded: message.isExpanded) + } else { + return self.dividerNode(indexPath: indexPath) + } } + guard message.isExpanded, let processedMessage = message.processedMessage + else { return self.dividerNode(indexPath: indexPath) } + guard indexPath.row > 1 else { return MessageTextSubjectNode(processedMessage.attributedMessage, index: messageIndex) } let attachmentIndex = indexPath.row - 2 - let attachment = processedMessage.attachments[attachmentIndex] - return AttachmentNode( - input: .init( - msgAttachment: attachment, - index: attachmentIndex + if let attachment = processedMessage.attachments[safe: attachmentIndex] { + return AttachmentNode( + input: .init( + msgAttachment: attachment, + index: attachmentIndex + ) ) - ) + } else { + return self.dividerNode(indexPath: indexPath) + } } } @@ -635,26 +803,78 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { handleExpandTap(at: indexPath) case is AttachmentNode: handleAttachmentTap(at: indexPath) - default: return + default: + guard let message = input[safe: indexPath.section - 1], + message.rawMessage.isDraft + else { return } + + handleDraftTap(at: indexPath) } } - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - dividerView() + private func draftNode(messageIndex: Int, isExpanded: Bool) -> ASCellNode { + let data = input[messageIndex] + + let body: String + if let processedMessage = data.processedMessage { + body = processedMessage.text + } else if data.rawMessage.isPgp { + body = "Waiting for pass phrase to open draft..." + } else { + body = data.rawMessage.body.text + } + + return LabelCellNode( + input: .init( + title: "draft".localized.attributed(color: .systemRed), + text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), + accessibilityIdentifier: "aid-draft-body-\(messageIndex)", + labelAccessibilityIdentifier: "aid-draft-label-\(messageIndex)", + buttonAccessibilityIdentifier: "aid-draft-delete-button-\(messageIndex)", + actionButtonImageName: "trash", + action: { [weak self] in + self?.deleteDraft(id: data.rawMessage.identifier) + } + ) + ) } - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - section > 0 && section < input.count ? 1 / UIScreen.main.nativeScale : 0 + private func deleteDraft(id: Identifier) { + showAlertWithAction( + title: "draft_delete_confirmation".localized, + message: nil, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + guard let self else { return } + + Task { + try await self.messageOperationsProvider.deleteMessage( + id: id, + from: nil + ) + + let messageIdentifier = MessageIdentifier( + threadId: Identifier(stringId: self.inboxItem.threadId) + ) + self.onComposeMessageAction?(.delete(messageIdentifier)) + + guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == id }) else { return } + + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) + } + } + ) } - private func dividerView() -> UIView { - UIView().then { - let frame = CGRect(x: 8, y: 0, width: view.frame.width - 16, height: 1 / UIScreen.main.nativeScale) - let divider = UIView(frame: frame) - $0.addSubview(divider) - $0.backgroundColor = .clear - divider.backgroundColor = .borderColor - } + private func dividerNode(indexPath: IndexPath) -> ASCellNode { + let height = indexPath.section < input.count ? 1 / UIScreen.main.nativeScale : 0 + return DividerCellNode( + inset: .init(top: 0, left: 8, bottom: 0, right: 8), + color: .borderColor, + height: height + ) } } @@ -662,8 +882,8 @@ extension ThreadDetailsViewController: NavigationChildController { func handleBackButtonTap() { logger.logInfo("Back button. Messages are all read") onComplete( - MessageAction.markAsRead(true), - .init(thread: thread, folderPath: currentFolderPath) + .markAsRead(true), + inboxItem ) navigationController?.popViewController(animated: true) } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index fb5bb8a76..71a54319e 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -211,7 +211,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { "inReplyTo": msg.inReplyTo, "atts": msg.atts.map { att in ["name": att.name, "type": att.type, "base64": att.base64] }, "format": fmt.rawValue, - "pubKeys": msg.pubKeys, + "pubKeys": fmt == .plain ? nil : msg.pubKeys, "signingPrv": msg.signingPrv.ifNotNil(\.prvKeyInfoJsonDictForCore) ], data: nil) return CoreRes.ComposeEmail(mimeEncoded: r.data) diff --git a/FlowCrypt/Core/CoreHost.swift b/FlowCrypt/Core/CoreHost.swift index 30d0d2e96..c39ae1450 100644 --- a/FlowCrypt/Core/CoreHost.swift +++ b/FlowCrypt/Core/CoreHost.swift @@ -4,7 +4,6 @@ import CommonCrypto // for hashing import FlowCryptCommon -import Foundation import IDZSwiftCommonCrypto // for aes import JavaScriptCore // for export to js import Security // for rng diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 0627c4bf4..466df14c6 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import RealmSwift struct CoreRes { diff --git a/FlowCrypt/Core/Models/KeyDetails.swift b/FlowCrypt/Core/Models/KeyDetails.swift index 11da3a464..acd96f5ce 100644 --- a/FlowCrypt/Core/Models/KeyDetails.swift +++ b/FlowCrypt/Core/Models/KeyDetails.swift @@ -8,7 +8,6 @@ import FlowCryptCommon import MailCore -import Foundation protocol ArmoredPrvWithIdentity { var primaryFingerprint: String { get throws } diff --git a/FlowCrypt/Extensions/UIColorExtensions.swift b/FlowCrypt/Extensions/UIColorExtensions.swift index 7d3f5ffb7..3d3f6b894 100644 --- a/FlowCrypt/Extensions/UIColorExtensions.swift +++ b/FlowCrypt/Extensions/UIColorExtensions.swift @@ -72,9 +72,9 @@ public extension UIColor { extension UIColor { convenience init(r: Int, g: Int, b: Int, alpha: CGFloat = 1) { self.init( - red: CGFloat(r)/CGFloat(255.0), - green: CGFloat(g)/CGFloat(255.0), - blue: CGFloat(b)/CGFloat(255.0), + red: CGFloat(r) / CGFloat(255.0), + green: CGFloat(g) / CGFloat(255.0), + blue: CGFloat(b) / CGFloat(255.0), alpha: alpha ) } diff --git a/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift b/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift index 7f1dc4d35..fcf5c3086 100644 --- a/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift +++ b/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift @@ -7,7 +7,7 @@ import Foundation /// Backend API for regular consumers and small businesses /// (not implemented on iOS yet) final class BackendApi { - static let shared: BackendApi = BackendApi() + static let shared = BackendApi() private init() {} diff --git a/FlowCrypt/Functionality/Api/ApiCall.swift b/FlowCrypt/Functionality/Api/ApiCall.swift index df746d6e6..43ce705fd 100644 --- a/FlowCrypt/Functionality/Api/ApiCall.swift +++ b/FlowCrypt/Functionality/Api/ApiCall.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon enum ApiCall {} diff --git a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift index 085555366..e426c2642 100644 --- a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol EmailKeyManagerApiType { @@ -59,7 +58,8 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { URLHeader( value: "Bearer \(idToken)", httpHeaderField: "Authorization" - )] + ) + ] let request = ApiCall.Request( apiName: Constants.apiName, url: urlString, diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift index ab074e19d..53f986c3c 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import FlowCryptCommon protocol AttesterApiType { diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift index aca8f59e6..87dd9000f 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift @@ -77,7 +77,7 @@ class WkdApi: WkdApiType { private func parseAndFilter(keysData: Data, email: String) async throws -> [KeyDetails] { return try await core.parseKeys(armoredOrBinary: keysData).keyDetails - .filter { !$0.users.filter { $0.contains(email) }.isEmpty } + .filter { $0.users.contains { user in user.contains(email) } } } private func urlLookup(_ urls: WkdUrls) async throws -> InternalResult { @@ -89,7 +89,7 @@ class WkdApi: WkdApiType { ) _ = try await ApiCall.call(request) } catch { - Logger.nested("WkdApi").logInfo("Failed to load \(urls.policy) with error \(error)") + Logger.nested("WkdApi").logInfo("Failed to load \(urls.policy) with error \(error.errorMessage)") return InternalResult(hasPolicy: false, keys: nil, method: urls.method) } diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift index 83196f6e1..e9d5667b7 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift @@ -7,7 +7,6 @@ // import CryptoKit -import Foundation /// WKD - Web Key Directory, follow this link for more information https://wiki.gnupg.org/WKD diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 4402e4290..b453ecf89 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -102,7 +102,7 @@ final class EncryptedStorage: EncryptedStorageType { return Realm.Configuration(inMemoryIdentifier: UUID().uuidString) } - let path = try EncryptedStorage.path + let path = try Self.path let latestSchemaVersion = currentSchema.version.dbSchemaVersion return Realm.Configuration( diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift index 7c9250143..c52054d35 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift @@ -2,12 +2,11 @@ // KeyChainService.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 25.11.2019. +// Created by Anton Kharchevskyi on 25.11.2019 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // import FlowCryptCommon -import Foundation import Security import UIKit diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift index 3654cd05f..fbd97d959 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift @@ -2,8 +2,8 @@ // Version6SchemaMigration.swift // FlowCrypt // -// Created by  Ivan Ushakov on 07.12.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// Created by Ivan Ushakov on 07.12.2021 +// Copyright © 2017-present FlowCrypt a.s. All rights reserved. // import FlowCryptCommon diff --git a/FlowCrypt/Functionality/DataManager/SessionService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift index 9ebbc4be5..2145ca32b 100644 --- a/FlowCrypt/Functionality/DataManager/SessionService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation enum SessionType: CustomStringConvertible { case google(_ email: String, name: String, token: String) @@ -36,7 +35,6 @@ protocol SessionServiceType { func startSessionFor(session: SessionType) throws func switchActiveSessionFor(user: User) throws -> SessionType? func startActiveSessionForNextUser() throws -> SessionType? - func logOutUsersThatDontHaveAnyKeysSetUp() throws func cleanup() throws } @@ -112,22 +110,6 @@ extension SessionService: SessionServiceType { return try switchActiveSession(for: currentUser) } - func logOutUsersThatDontHaveAnyKeysSetUp() throws { - logger.logInfo("Clean up sessions") - for user in try encryptedStorage.getAllUsers() { - if try !encryptedStorage.doesAnyKeypairExist(for: user.email) { - logger.logInfo("User session to clean up \(user.email)") - try logOut(user: user) - } - } - - let users = try encryptedStorage.getAllUsers() - if !users.contains(where: { $0.isActive }), - let user = try users.first(where: { try encryptedStorage.doesAnyKeypairExist(for: $0.email) }) { - try switchActiveSession(for: user) - } - } - @discardableResult private func switchActiveSession(for user: User) throws -> SessionType? { logger.logInfo("Try to switch session for \(user.email)") diff --git a/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift b/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift deleted file mode 100644 index e55efc276..000000000 --- a/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// DraftsListProvider.swift -// FlowCrypt -// -// Created by Evgenii Kyivskyi on 10/20/21 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -protocol DraftsListProvider { - func fetchDrafts(using context: FetchMessageContext) async throws -> MessageContext -} diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index 48f8b3cc2..9462bf895 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import GoogleAPIClientForREST_Gmail class GmailService: MailServiceProvider { @@ -63,5 +62,5 @@ extension String { static let bcc = "bcc" static let replyTo = "reply-to" static let inReplyTo = "in-reply-to" - static let identifier = "Message-ID" + static let identifier = "message-id" } diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift index d803847e5..fe2fc8512 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift @@ -54,9 +54,9 @@ extension GmailServiceError { static func convert(from error: NSError) -> GmailServiceError { switch error.code { case -10: // invalid_grant error code - return GmailServiceError.invalidGrant(error) + return .invalidGrant(error) default: - return GmailServiceError.providerError(error) + return .providerError(error) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift index 3fb85a03e..a81560ad5 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift index e9881ed66..d48ee9a8a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift index debbac664..e431f7322 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift index 7c067edf0..9fdb742fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { @@ -85,7 +84,7 @@ extension Imap { _ imapSess: MCOIMAPSession, _ executor: @escaping (MCOIMAPSession, @escaping (Error?) -> Void) -> Void ) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in executor(imapSess) { error in if let error = error { return continuation.resume(throwing: error) diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index a1f6daaef..77b286491 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import MailCore extension Imap { @@ -18,7 +17,7 @@ extension Imap { } func connectSmtp(session: SMTPSession) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in MCOSMTPSession(session: session) .startLogging() .loginOperation()? @@ -33,7 +32,7 @@ extension Imap { } func connectImap(session: IMAPSession) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in MCOIMAPSession(session: session) .startLogging() .connectOperation()? diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift b/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift index 13f86f2a4..2ea4ea4a6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol ImapHelperType { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift b/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift index 21ef99932..bdeed4465 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol MessageKindProviderType { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift index 42d494a79..3c0839ba6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore enum AuthType { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift index ff94ea365..f0179af42 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift @@ -6,7 +6,6 @@ // Copyright © 2020 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore struct IMAPSession { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift index 532597cd7..d9fbd8510 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore struct MailSettingsCredentials { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift index 74fa7b624..99ff35acd 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift @@ -6,7 +6,6 @@ // Copyright © 2020 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore struct SMTPSession { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift index 4da87a38d..afe07d4b1 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol SessionCredentialsProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index df18a1e6f..ba6f63abd 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail import UIKit @@ -29,7 +28,7 @@ final class MailProvider { } private let services: [MailServiceProvider] - var messageSender: MessageGateway { + var messageGateway: MessageGateway { get throws { try resolveService(of: MessageGateway.self) } @@ -89,12 +88,6 @@ final class MailProvider { } } - var draftsProvider: DraftsListProvider? { - get throws { - resolveOptionalService(of: DraftsListProvider.self) - } - } - var messagesThreadProvider: MessagesThreadProvider { get throws { try resolveService(of: MessagesThreadProvider.self) @@ -117,14 +110,18 @@ final class MailProvider { } private func resolveService(of type: T.Type) throws -> T { - guard let service = services.first(where: { $0.mailServiceProviderType == authType.mailServiceProviderType }) as? T else { + guard let service = services.first(where: { + $0.mailServiceProviderType == authType.mailServiceProviderType + }) as? T else { throw AppErr.general("Email Provider should support this functionality. Can't resolve dependency for \(type)") } return service } private func resolveOptionalService(of type: T.Type) -> T? { - guard let service = services.first(where: { $0.mailServiceProviderType == authType.mailServiceProviderType }) as? T else { + guard let service = services.first(where: { + $0.mailServiceProviderType == authType.mailServiceProviderType + }) as? T else { return nil } return service diff --git a/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift b/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift index e0015a6bc..a7a9d906f 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail enum MailServiceProviderType { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 5ff4b689c..cc19ec967 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -5,26 +5,74 @@ // Created by Evgenii Kyivskyi on 10/22/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation + import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? { + guard let identifier = id.stringId else { return nil } + let query = GTLRGmailQuery_UsersDraftsGet.query(withUserId: .me, identifier: identifier) + return try await withCheckedThrowingContinuation { continuation in + gmailService.executeQuery(query) { _, data, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } + guard let gmailDraft = data as? GTLRGmail_Draft else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) + } + + let draft = MessageIdentifier(gmailDraft: gmailDraft) + return continuation.resume(returning: draft) + } + } + } + + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { + guard let id = messageId.stringId else { return nil } + + let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) + query.q = "rfc822msgid:\(id)" + query.maxResults = 1 + + return try await withCheckedThrowingContinuation { continuation in + gmailService.executeQuery(query) { _, data, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } + + guard let list = data as? GTLRGmail_ListDraftsResponse else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_ListDraftsResponse")) + } + + guard let gmailDraft = list.drafts?.first else { + return continuation.resume(returning: nil) + } + + let draft = MessageIdentifier(gmailDraft: gmailDraft) + return continuation.resume(returning: draft) + } + } + } + + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier { + try await withCheckedThrowingContinuation { continuation in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } + let draftQuery = createQueryForDraftAction( raw: raw, threadId: input.threadId, - draft: draft) + draftId: draftId?.stringId + ) gmailService.executeQuery(draftQuery) { _, object, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) - } else if let draft = object as? GTLRGmail_Draft { - return continuation.resume(returning: (draft)) + } else if let gmailDraft = object as? GTLRGmail_Draft { + let draft = MessageIdentifier(gmailDraft: gmailDraft) + return continuation.resume(returning: draft) } else { return continuation.resume(throwing: GmailServiceError.failedToParseData(nil)) } @@ -32,43 +80,43 @@ extension GmailService: DraftGateway { } } - func deleteDraft(with identifier: String) async { - await withCheckedContinuation { (continuation: CheckedContinuation) in - let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) - gmailService.executeQuery(query) { _, _, _ in + func deleteDraft(with identifier: Identifier) async throws { + guard let id = identifier.stringId else { return } + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: id) + gmailService.executeQuery(query) { _, _, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } return continuation.resume() } } } - private func createQueryForDraftAction(raw: String, threadId: String?, draft: GTLRGmail_Draft?) -> GTLRGmailQuery { - guard - let createdDraft = draft, - let draftIdentifier = createdDraft.identifier - else { - // draft is not created yet. creating draft - let newDraft = GTLRGmail_Draft() - let gtlMessage = GTLRGmail_Message() - gtlMessage.raw = raw - gtlMessage.threadId = threadId - newDraft.message = gtlMessage + private func createQueryForDraftAction(raw: String, threadId: String?, draftId: String?) -> GTLRGmailQuery { + let draft = GTLRGmail_Draft() + + let message = GTLRGmail_Message() + message.raw = raw + message.threadId = threadId + draft.message = message + + if let draftId = draftId { + draft.identifier = draftId + + return GTLRGmailQuery_UsersDraftsUpdate.query( + withObject: draft, + userId: "me", + identifier: draftId, + uploadParameters: nil + ) + } else { return GTLRGmailQuery_UsersDraftsCreate.query( - withObject: newDraft, + withObject: draft, userId: "me", - uploadParameters: nil) + uploadParameters: nil + ) } - - // updating existing draft with new data - let gtlMessage = GTLRGmail_Message() - gtlMessage.raw = raw - gtlMessage.threadId = threadId - createdDraft.message = gtlMessage - - return GTLRGmailQuery_UsersDraftsUpdate.query( - withObject: createdDraft, - userId: "me", - identifier: draftIdentifier, - uploadParameters: nil) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift index ea28a723a..411057c62 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift @@ -6,12 +6,11 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { + try await withCheckedThrowingContinuation { continuation in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } @@ -28,12 +27,19 @@ extension GmailService: MessageGateway { uploadParameters: nil ) - gmailService.executeQuery(querySend) { [weak self] _, _, error in + gmailService.executeQuery(querySend) { [weak self] _, data, error in self?.progressHandler = nil + if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + + guard let gmailMessage = data as? GTLRGmail_Message else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Message")) + } + + let identifier = Identifier(stringId: gmailMessage.identifier) + return continuation.resume(returning: identifier) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift index f2ae9f8d9..87ab852ff 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift @@ -5,8 +5,8 @@ import Foundation extension Imap: MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { - try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { + try await withCheckedThrowingContinuation { [weak self] continuation in do { let session = try self?.smtpSess session?.sendOperation(with: input.mime) @@ -14,7 +14,7 @@ extension Imap: MessageGateway { if let error = error { return continuation.resume(throwing: error) } - return continuation.resume(returning: ()) + return continuation.resume(throwing: AppErr.unexpected("Not implemented")) } } catch { return continuation.resume(throwing: ImapError.noSession) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 902a722b3..197af3516 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -7,7 +7,6 @@ // import Foundation -import GoogleAPIClientForREST_Gmail struct MessageGatewayInput { let mime: Data @@ -15,10 +14,12 @@ struct MessageGatewayInput { } protocol MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier } protocol DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft - func deleteDraft(with identifier: String) async + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier + func deleteDraft(with identifier: Identifier) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift index 55dbd6777..4f94ac95e 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift @@ -11,7 +11,7 @@ import GTMSessionFetcherCore extension GmailService: MessageProvider { - func fetchMsg( + func fetchMessage( id: Identifier, folder: String ) async throws -> Message { @@ -20,7 +20,7 @@ extension GmailService: MessageProvider { } let query = createMessageQuery(identifier: identifier, format: kGTLRGmailFormatFull) - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -40,13 +40,13 @@ extension GmailService: MessageProvider { } } - func fetchRawMsg(id: Identifier) async throws -> String { + func fetchRawMessage(id: Identifier) async throws -> String { guard let identifier = id.stringId else { throw GmailServiceError.missingMessageInfo("id") } let query = createMessageQuery(identifier: identifier, format: kGTLRGmailFormatRaw) - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -85,7 +85,7 @@ extension GmailService: MessageProvider { let fetcher = createAttachmentFetcher(identifier: identifier, messageId: messageIdentifier) if let estimatedSize = estimatedSize { fetcher.receivedProgressBlock = { _, received in - let progress = min(Float(received)/estimatedSize, 1) + let progress = min(Float(received) / estimatedSize, 1) progressHandler?(progress) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift index 7a3b9dcc0..7bfbe32c7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift @@ -9,7 +9,7 @@ import GoogleAPIClientForREST_Gmail extension Message { - init(gmailMessage: GTLRGmail_Message, draftIdentifier: String? = nil) throws { + init(gmailMessage: GTLRGmail_Message) throws { guard let payload = gmailMessage.payload else { throw GmailServiceError.missingMessagePayload } @@ -38,6 +38,7 @@ extension Message { var bcc: String? var replyTo: String? var inReplyTo: String? + var rfc822MsgId: String? for messageHeader in messageHeaders.compactMap({ $0 }) { guard let name = messageHeader.name?.lowercased(), @@ -52,6 +53,7 @@ extension Message { case .bcc: bcc = value case .replyTo: replyTo = value case .inReplyTo: inReplyTo = value + case .identifier: rfc822MsgId = value default: break } } @@ -68,7 +70,7 @@ extension Message { body: body, attachments: attachments, threadId: gmailMessage.threadId, - draftIdentifier: draftIdentifier, + rfc822MsgId: rfc822MsgId, raw: gmailMessage.raw, to: to, cc: cc, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift index 9b87f0c94..e6f0958e8 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift @@ -9,7 +9,7 @@ import Foundation extension Imap: MessageProvider { - func fetchMsg( + func fetchMessage( id: Identifier, folder: String ) async throws -> Message { @@ -25,7 +25,7 @@ extension Imap: MessageProvider { // }) } - func fetchRawMsg(id: Identifier) async throws -> String { + func fetchRawMessage(id: Identifier) async throws -> String { throw AppErr.unexpected("Not implemented") } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift index 20bcf94e6..83ddb62f2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift @@ -53,7 +53,7 @@ extension MessageAttachment { } extension MessageAttachment { - func toSendableMsgAttachment() -> SendableMsg.Attachment { - return SendableMsg.Attachment(name: name, type: type, base64: data?.base64EncodedString() ?? "") + var sendableMsgAttachment: SendableMsg.Attachment { + SendableMsg.Attachment(name: name, type: type, base64: data?.base64EncodedString() ?? "") } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift index d443588d0..3a42714f3 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift @@ -9,8 +9,8 @@ import Foundation protocol MessageProvider { - func fetchMsg(id: Identifier, folder: String) async throws -> Message - func fetchRawMsg(id: Identifier) async throws -> String + func fetchMessage(id: Identifier, folder: String) async throws -> Message + func fetchRawMessage(id: Identifier) async throws -> String func fetchAttachment( id: Identifier, messageId: Identifier, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 8b9544bd3..3483b4391 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon import UIKit @@ -17,11 +16,11 @@ enum MessageFetchState { // MARK: - MessageServiceError enum MessageServiceError: Error, CustomStringConvertible { - case missingPassPhrase(_ message: Message) + case missingPassPhrase(Keypair?) case emptyKeys case emptyKeysForEKM case attachmentNotFound - case attachmentDecryptFailed(_ message: String) + case attachmentDecryptFailed(String) } extension MessageServiceError { @@ -88,64 +87,102 @@ final class MessageService { // MARK: - Message processing func getAndProcess( - message: Message, + identifier: Identifier, folder: String, onlyLocalKeys: Bool, userEmail: String, isUsingKeyManager: Bool ) async throws -> ProcessedMessage { - let message = try await messageProvider.fetchMsg( - id: message.identifier, + let message = try await messageProvider.fetchMessage( + id: identifier, folder: folder ) - if message.isPgp { - return try await decryptAndProcess( - message: message, - sender: message.sender, - onlyLocalKeys: onlyLocalKeys, - userEmail: userEmail, - isUsingKeyManager: isUsingKeyManager - ) - } else { + + guard message.isPgp else { return ProcessedMessage(message: message) } + + return try await decryptAndProcess( + message: message, + onlyLocalKeys: onlyLocalKeys, + userEmail: userEmail, + isUsingKeyManager: isUsingKeyManager + ) } - func decryptAndProcess( - message: Message, - sender: Recipient?, - onlyLocalKeys: Bool, - userEmail: String, - isUsingKeyManager: Bool - ) async throws -> ProcessedMessage { - let keys = try await keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: userEmail) + private func getKeypairs(email: String, isUsingKeyManager: Bool) async throws -> [Keypair] { + let keys = try await keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: email) + guard keys.isNotEmpty else { if isUsingKeyManager { throw MessageServiceError.emptyKeysForEKM } throw MessageServiceError.emptyKeys } - let verificationPubKeys = try await fetchVerificationPubKeys(for: sender, onlyLocal: onlyLocalKeys) + + return keys + } + + private func decrypt( + text: String, + keys: [Keypair], + isMime: Bool = false, + verificationPubKeys: [String] = [] + ) async throws -> CoreRes.ParseDecryptMsg { + let decrypted = try await core.parseDecryptMsg( + encrypted: text.data(), + keys: keys, + msgPwd: nil, + isMime: isMime, + verificationPubKeys: verificationPubKeys + ) + + guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { + let keyPair = keys.first(where: { $0.passphrase == nil }) + throw MessageServiceError.missingPassPhrase(keyPair) + } + + return decrypted + } + + func decrypt( + text: String, + userEmail: String, + isUsingKeyManager: Bool + ) async throws -> String { + let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) + let decrypted = try await decrypt(text: text, keys: keys) + return decrypted.text + } + + func decryptAndProcess( + message: Message, + onlyLocalKeys: Bool, + userEmail: String, + isUsingKeyManager: Bool + ) async throws -> ProcessedMessage { + let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) + + let verificationPubKeys = try await fetchVerificationPubKeys( + for: message.sender, + onlyLocal: onlyLocalKeys + ) var message = message if message.hasSignatureAttachment { // raw data is needed for verification of detached signature - message.raw = try await messageProvider.fetchRawMsg(id: message.identifier) + message.raw = try await messageProvider.fetchRawMessage(id: message.identifier) } let encrypted = message.raw ?? message.body.text - let decrypted = try await core.parseDecryptMsg( - encrypted: encrypted.data(), + + let decrypted = try await decrypt( + text: encrypted, keys: keys, - msgPwd: nil, isMime: message.raw != nil, verificationPubKeys: verificationPubKeys ) - guard !self.hasMsgBlockThatNeedsPassPhrase(decrypted) else { - throw MessageServiceError.missingPassPhrase(message) - } - return try await process( message: message, with: decrypted @@ -174,8 +211,8 @@ final class MessageService { let err = decryptErrBlock.decryptErr?.error let hideContent = err?.type == .badMdc || err?.type == .noMdc let rawMsg = hideContent - ? "content_hidden".localized - : decryptErrBlock.content + ? "content_hidden".localized + : decryptErrBlock.content text = "error_decrypt".localized + "\n\(err?.type.rawValue ?? "unknown".localized): \(err?.message ?? "??")\n\n\n\(rawMsg)" diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index 0d4ee0985..4264d8250 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -6,29 +6,28 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageOperationsProvider { - func markAsUnread(message: Message, folder: String) async throws { - try await update(message: message, labelsToAdd: [.unread]) + func markAsUnread(id: Identifier, folder: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.unread]) } - func markAsRead(message: Message, folder: String) async throws { - try await update(message: message, labelsToRemove: [.unread]) + func markAsRead(id: Identifier, folder: String) async throws { + try await updateMessage(id: id, labelsToRemove: [.unread]) } - func moveMessageToInbox(message: Message, folderPath: String) async throws { - try await update(message: message, labelsToAdd: [.inbox]) + func moveMessageToInbox(id: Identifier, folderPath: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.inbox]) } - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws { - try await update(message: message, labelsToAdd: [.trash]) + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.trash]) } - func delete(message: Message, from folderPath: String?) async throws { + func deleteMessage(id: Identifier, from folderPath: String?) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = message.identifier.stringId else { + guard let identifier = id.stringId else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } @@ -41,24 +40,31 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } func emptyFolder(path: String) async throws { - let messageIdentifiers = try await fetchAllMessageIdentifers(for: path) + let messageIdentifiers = try await fetchAllMessageIdentifiers(for: path) try await batchDeleteMessages(identifiers: messageIdentifiers, from: path) } - private func fetchAllMessageIdentifers(for path: String, token: String? = nil, result: [String] = []) async throws -> [String] { + private func fetchAllMessageIdentifiers( + for path: String, + token: String? = nil, + result: [String] = [] + ) async throws -> [String] { let context = FetchMessageContext(folderPath: path, count: 500, pagination: .byNextPage(token: token)) let list = try await fetchMessagesList(using: context) - var newResult = (list.messages?.compactMap(\.identifier) ?? []) + result + + let newResult = (list.messages?.compactMap(\.identifier) ?? []) + result + if let nextPageToken = list.nextPageToken { - newResult = try await fetchAllMessageIdentifers(for: path, token: nextPageToken, result: newResult) + return try await fetchAllMessageIdentifiers(for: path, token: nextPageToken, result: newResult) + } else { + return newResult } - return newResult } func batchDeleteMessages(identifiers: [String], from folderPath: String?) async throws { @@ -71,32 +77,25 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } - func archiveMessage(message: Message, folderPath: String) async throws { - try await update( - message: message, + func archiveMessage(id: Identifier, folderPath: String) async throws { + try await updateMessage( + id: id, labelsToRemove: [.inbox] ) } - func archiveBatchMessages(messages: [Message]) async throws { - try await batchUpdate( - messages: messages, - labelsToRemove: [.inbox] - ) - } - - private func batchUpdate( - messages: [Message], + func batchUpdate( + messagesIds: [Identifier], labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { let request = GTLRGmail_BatchModifyMessagesRequest() - request.ids = messages.compactMap { $0.identifier.stringId } + request.ids = messagesIds.compactMap(\.stringId) request.addLabelIds = labelsToAdd.map(\.value) request.removeLabelIds = labelsToRemove.map(\.value) let query = GTLRGmailQuery_UsersMessagesBatchModify.query( @@ -104,23 +103,23 @@ extension GmailService: MessageOperationsProvider { userId: .me ) - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in self.gmailService.executeQuery(query) { _, _, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } - private func update( - message: Message, + private func updateMessage( + id: Identifier, labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = message.identifier.stringId else { + guard let identifier = id.stringId else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } let request = GTLRGmail_ModifyMessageRequest() @@ -136,7 +135,7 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift index dc13e5158..0c0f7d3fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift @@ -6,56 +6,47 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap: MessageOperationsProvider { - func markAsUnread(message: Message, folder: String) async throws { - guard let identifier = message.identifier.intId else { + func markAsUnread(id: Identifier, folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } try await executeVoid("markAsUnread", { sess, respond in sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.remove, - flags: [MCOMessageFlag.seen] + kind: .remove, + flags: [.seen] ).start { error in respond(error) } }) } - func markAsRead(message: Message, folder: String) async throws { - guard let identifier = message.identifier.intId else { + func markAsRead(id: Identifier, folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } - var flags: MCOMessageFlag = [] - let imapFlagValues = message.labels.map(\.imapFlagValue) - // keep previous flags - for value in imapFlagValues { - flags.insert(MCOMessageFlag(rawValue: value)) - } - // add seen flag - flags.insert(MCOMessageFlag.seen) try await executeVoid("markAsRead", { sess, respond in sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.add, - flags: flags + kind: .add, + flags: [.seen] ).start { error in respond(error) } }) } - func moveMessageToInbox(message: Message, folderPath: String) async throws { + func moveMessageToInbox(id: Identifier, folderPath: String) async throws { // should be implemented later - guard message.identifier.intId != nil else { + guard id.intId != nil else { throw ImapError.missingMessageInfo("intId") } } - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws { - guard let identifier = message.identifier.intId else { + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } guard let trashPath = trashPath else { @@ -74,8 +65,8 @@ extension Imap: MessageOperationsProvider { }) } - func delete(message: Message, from folderPath: String?) async throws { - guard let identifier = message.identifier.intId else { + func deleteMessage(id: Identifier, from folderPath: String?) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } guard let folderPath = folderPath else { @@ -101,7 +92,7 @@ extension Imap: MessageOperationsProvider { sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.set, + kind: .set, flags: flags ).start { error in respond(error) } }) @@ -115,8 +106,8 @@ extension Imap: MessageOperationsProvider { }) } - func archiveMessage(message: Message, folderPath: String) async throws { - guard let identifier = message.identifier.intId else { + func archiveMessage(id: Identifier, folderPath: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } try await pushUpdatedMsgFlags(with: identifier, folder: folderPath, flags: MCOMessageFlag.deleted) diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift index 40e13b7c6..2d2017274 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift @@ -6,16 +6,15 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol MessageOperationsProvider { - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws - func delete(message: Message, from folderPath: String?) async throws - func moveMessageToInbox(message: Message, folderPath: String) async throws - func archiveMessage(message: Message, folderPath: String) async throws - func markAsUnread(message: Message, folder: String) async throws - func markAsRead(message: Message, folder: String) async throws + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws + func deleteMessage(id: Identifier, from folderPath: String?) async throws + func moveMessageToInbox(id: Identifier, folderPath: String) async throws + func archiveMessage(id: Identifier, folderPath: String) async throws + func markAsUnread(id: Identifier, folder: String) async throws + func markAsRead(id: Identifier, folder: String) async throws func emptyFolder(path: String) async throws func batchDeleteMessages(identifiers: [String], from folderPath: String?) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index a0cb933fe..7b5332ddb 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -13,7 +13,7 @@ import GoogleAPIClientForREST_Gmail // TODO: - https://github.com/FlowCrypt/flowcrypt-ios/issues/669 Remove in scope of the ticket extension GmailService: MessagesListProvider { func fetchMessages(using context: FetchMessageContext) async throws -> MessageContext { - return try await withThrowingTaskGroup(of: Message.self) { [weak self] taskGroup -> MessageContext in + return try await withThrowingTaskGroup(of: Message.self) { [weak self] taskGroup in let list = try await fetchMessagesList(using: context) let messageIdentifiers = list.messages?.compactMap(\.identifier) ?? [] @@ -22,7 +22,7 @@ extension GmailService: MessagesListProvider { if let self = self { for identifier in messageIdentifiers { taskGroup.addTask { - try await self.fetchFullMessage(with: identifier) + try await self.fetchMessage(id: Identifier(stringId: identifier), folder: "") } } @@ -39,59 +39,6 @@ extension GmailService: MessagesListProvider { } } -extension GmailService: DraftsListProvider { - func fetchDrafts(using context: FetchMessageContext) async throws -> MessageContext { - return try await withThrowingTaskGroup(of: Message.self) { taskGroup -> MessageContext in - let list = try await fetchDraftsList(using: context) - - for draft in list.drafts ?? [] { - taskGroup.addTask { - try await self.fetchFullMessage( - with: draft.message?.identifier ?? "", - draftIdentifier: draft.identifier) - } - } - var messages: [Message] = [] - for try await result in taskGroup { - messages.append(result) - } - - return MessageContext( - messages: messages, - pagination: .byNextPage(token: list.nextPageToken) - ) - } - } - - private func fetchDraftsList(using context: FetchMessageContext) async throws -> GTLRGmail_ListDraftsResponse { - let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) - - if let pagination = context.pagination { - guard case let .byNextPage(token) = pagination else { - throw GmailServiceError.paginationError(pagination) - } - query.pageToken = token - } - - if let count = context.count { - query.maxResults = UInt(count) - } - - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - gmailService.executeQuery(query) { _, data, error in - if let error = error { - return continuation.resume(throwing: GmailServiceError.providerError(error)) - } - - guard let messageList = data as? GTLRGmail_ListDraftsResponse else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_ListDraftsResponse")) - } - return continuation.resume(returning: messageList) - } - } - } -} - extension GmailService { func fetchMessagesList(using context: FetchMessageContext) async throws -> GTLRGmail_ListMessagesResponse { let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: .me) @@ -113,7 +60,7 @@ extension GmailService { query.q = searchQuery } - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -127,29 +74,4 @@ extension GmailService { } } } - - private func fetchFullMessage(with identifier: String, draftIdentifier: String? = nil) async throws -> Message { - let query = GTLRGmailQuery_UsersMessagesGet.query(withUserId: .me, identifier: identifier) - query.format = kGTLRGmailFormatFull - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - gmailService.executeQuery(query) { _, data, error in - if let error = error { - return continuation.resume(throwing: GmailServiceError.providerError(error)) - } - - guard let gmailMessage = data as? GTLRGmail_Message else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_Message")) - } - - do { - return continuation.resume(returning: try Message( - gmailMessage: gmailMessage, - draftIdentifier: draftIdentifier) - ) - } catch { - return continuation.resume(throwing: error) - } - } - } - } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift index 2f6feaa3a..132043997 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap: MessagesListProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift index 0754e97dd..9367db490 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift @@ -33,5 +33,6 @@ enum MessagesListPagination { } protocol MessagesListProvider { + func fetchMessage(id: Identifier, folder: String) async throws -> Message func fetchMessages(using context: FetchMessageContext) async throws -> MessageContext } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index e2a316e6b..d39d0769a 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import GoogleAPIClientForREST_Gmail struct Message: Hashable { @@ -23,13 +22,15 @@ struct Message: Hashable { let attachmentIds: [String] var attachments: [MessageAttachment] let threadId: String? - let draftIdentifier: String? + let rfc822MsgId: String? + var draftId: Identifier? var raw: String? let body: MessageBody let inReplyTo: String? + let replyToMsgId: String? private(set) var labels: [MessageLabel] - var isMessageRead: Bool { + var isRead: Bool { // imap if labels.contains(.none) { return false @@ -41,12 +42,14 @@ struct Message: Hashable { return true } + var isDraft: Bool { labels.contains(.draft) } + var isPgp: Bool { - (body.text.contains("-----BEGIN PGP ") && body.text.contains("-----END PGP ")) || hasSignatureAttachment + body.text.isPgp || hasSignatureAttachment } var hasSignatureAttachment: Bool { - attachments.first(where: { $0.type == "application/pgp-signature" }) != nil + attachments.contains(where: { $0.type == "application/pgp-signature" }) } init( @@ -60,13 +63,15 @@ struct Message: Hashable { body: MessageBody, attachments: [MessageAttachment] = [], threadId: String? = nil, - draftIdentifier: String? = nil, + rfc822MsgId: String? = nil, + draftId: Identifier? = nil, raw: String? = nil, to: String? = nil, cc: String? = nil, bcc: String? = nil, replyTo: String? = nil, - inReplyTo: String? = nil + inReplyTo: String? = nil, + replyToMsgId: String? = nil ) { self.identifier = identifier self.date = date @@ -78,13 +83,15 @@ struct Message: Hashable { self.attachments = attachments self.body = body self.threadId = threadId - self.draftIdentifier = draftIdentifier + self.rfc822MsgId = rfc822MsgId + self.draftId = draftId self.raw = raw - self.to = Message.parseRecipients(to) - self.cc = Message.parseRecipients(cc) - self.bcc = Message.parseRecipients(bcc) - self.replyTo = Message.parseRecipients(replyTo) + self.to = Self.parseRecipients(to) + self.cc = Self.parseRecipients(cc) + self.bcc = Self.parseRecipients(bcc) + self.replyTo = Self.parseRecipients(replyTo) self.inReplyTo = inReplyTo + self.replyToMsgId = replyToMsgId } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift new file mode 100644 index 000000000..ee3d41cfc --- /dev/null +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift @@ -0,0 +1,24 @@ +// +// MessageDraft.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 20/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import GoogleAPIClientForREST_Gmail + +struct MessageIdentifier { + var draftId: Identifier? + var threadId: Identifier? + var messageId: Identifier? + var draftMessageId: Identifier? +} + +extension MessageIdentifier { + init(gmailDraft: GTLRGmail_Draft) { + self.draftId = Identifier(stringId: gmailDraft.identifier) + self.threadId = Identifier(stringId: gmailDraft.message?.threadId) + self.messageId = Identifier(stringId: gmailDraft.message?.identifier) + } +} diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift index 4afa725d0..b8d286db3 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore enum MessageLabel: Equatable, Hashable { diff --git a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift index 9ea46087c..696e7ba71 100644 --- a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift +++ b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore // MARK: - MessageSearchProvider diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift index d524bbc23..121c53044 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift @@ -12,31 +12,27 @@ import Foundation extension Imap: MessagesThreadOperationsProvider { private var error: Error { AppErr.general("Doesn't support yet") } - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws { + func delete(id: String?) async throws { throw error } - func delete(thread: MessageThread) async throws { + func moveThreadToTrash(id: String?, labels: Set) async throws { throw error } - func moveThreadToTrash(thread: MessageThread) async throws { + func moveThreadToInbox(id: String?) async throws { throw error } - func moveThreadToInbox(thread: MessageThread) async throws { + func markThreadAsUnread(id: String?, folder: String) async throws { throw error } - func markThreadAsUnread(thread: MessageThread, folder: String) async throws { + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { throw error } - func markThreadAsRead(thread: MessageThread, folder: String) async throws { - throw error - } - - func archive(thread: MessageThread, in folder: String) async throws { + func archive(messagesIds: [Identifier], in folder: String) async throws { throw error } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 3dd74a7d0..45460e219 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -6,63 +6,58 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail protocol MessagesThreadOperationsProvider { - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws - func delete(thread: MessageThread) async throws - func moveThreadToTrash(thread: MessageThread) async throws - func moveThreadToInbox(thread: MessageThread) async throws - func markThreadAsUnread(thread: MessageThread, folder: String) async throws - func markThreadAsRead(thread: MessageThread, folder: String) async throws - func archive(thread: MessageThread, in folder: String) async throws + func delete(id: String?) async throws + func moveThreadToTrash(id: String?, labels: Set) async throws + func moveThreadToInbox(id: String?) async throws + func markThreadAsUnread(id: String?, folder: String) async throws + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws + func archive(messagesIds: [Identifier], in folder: String) async throws } extension GmailService: MessagesThreadOperationsProvider { - func delete(thread: MessageThread) async throws { + func delete(id: String?) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = thread.identifier else { + guard let id = id else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } let query = GTLRGmailQuery_UsersThreadsDelete.query( withUserId: .me, - identifier: identifier + identifier: id ) self.gmailService.executeQuery(query) { _, _, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - continuation.resume(returning: ()) + return continuation.resume() } } } - func moveThreadToTrash(thread: MessageThread) async throws { - try await update(thread: thread, labelsToAdd: [.trash], labelsToRemove: [.inbox, .sent]) + func moveThreadToTrash(id: String?, labels: Set) async throws { + let labelsToRemove = [MessageLabel.inbox, MessageLabel.sent].filter { labels.contains($0) } + try await update(id: id, labelsToAdd: [.trash], labelsToRemove: labelsToRemove) } - func moveThreadToInbox(thread: MessageThread) async throws { - try await update(thread: thread, labelsToAdd: [.inbox], labelsToRemove: [.trash]) + func moveThreadToInbox(id: String?) async throws { + try await update(id: id, labelsToAdd: [.inbox], labelsToRemove: [.trash]) } - func markThreadAsUnread(thread: MessageThread, folder: String) async throws { - try await update(thread: thread, labelsToAdd: [.unread]) + func markThreadAsUnread(id: String?, folder: String) async throws { + try await update(id: id, labelsToAdd: [.unread]) } - func markThreadAsRead(thread: MessageThread, folder: String) async throws { - try await update(thread: thread, labelsToRemove: [.unread]) - } - - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws { + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { try await withThrowingTaskGroup(of: Void.self) { taskGroup in - for message in thread.messages { + for id in messagesIds { taskGroup.addTask { asRead - ? try await self.markAsRead(message: message, folder: folder) - : try await self.markAsUnread(message: message, folder: folder) + ? try await self.markAsRead(id: id, folder: folder) + : try await self.markAsUnread(id: id, folder: folder) } } @@ -70,19 +65,22 @@ extension GmailService: MessagesThreadOperationsProvider { } } - func archive(thread: MessageThread, in folder: String) async throws { + func archive(messagesIds: [Identifier], in folder: String) async throws { // manually updated each message rather than using update(thread:...) method // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r932964129 - try await self.archiveBatchMessages(messages: thread.messages) + try await batchUpdate( + messagesIds: messagesIds, + labelsToRemove: [.inbox] + ) } private func update( - thread: MessageThread, + id: String?, labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = thread.identifier else { + guard let id = id else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } @@ -93,14 +91,14 @@ extension GmailService: MessagesThreadOperationsProvider { let query = GTLRGmailQuery_UsersThreadsModify.query( withObject: request, userId: .me, - identifier: identifier + identifier: id ) self.gmailService.executeQuery(query) { _, _, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 4b19dbb37..de0d29de3 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -10,25 +10,20 @@ import FlowCryptCommon import GoogleAPIClientForREST_Gmail protocol MessagesThreadProvider { + func fetchThread(identifier: String, path: String) async throws -> MessageThread func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext } extension GmailService: MessagesThreadProvider { func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext { let threadsList = try await getThreadsList(using: context) - let requests = threadsList.threads? - .compactMap { (thread) -> (String, String?)? in - guard let id = thread.identifier else { - return nil - } - return (id, thread.snippet) - } - ?? [] - return try await withThrowingTaskGroup(of: MessageThread.self) { (taskGroup) in + let identifiers = threadsList.threads?.compactMap(\.identifier) ?? [] + + return try await withThrowingTaskGroup(of: MessageThread.self) { taskGroup in var messageThreadsById: [String: MessageThread] = [:] - for request in requests { + for identifier in identifiers { taskGroup.addTask { - try await self.getThread(with: request.0, snippet: request.1, path: context.folderPath ?? "") + try await self.fetchThread(identifier: identifier, path: context.folderPath ?? "") } } for try await result in taskGroup { @@ -36,7 +31,7 @@ extension GmailService: MessagesThreadProvider { messageThreadsById[id] = result } } - let messageThreads = requests.compactMap { messageThreadsById[$0.0] } + let messageThreads = identifiers.compactMap { messageThreadsById[$0] } return MessageThreadContext( threads: messageThreads, pagination: .byNextPage(token: threadsList.nextPageToken) @@ -47,7 +42,7 @@ extension GmailService: MessagesThreadProvider { private func getThreadsList(using context: FetchMessageContext) async throws -> GTLRGmail_ListThreadsResponse { let query = try makeQuery(using: context) return try await Task.retrying { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { let gmailError = GmailServiceError.convert(from: error as NSError) @@ -63,39 +58,28 @@ extension GmailService: MessagesThreadProvider { }.value } - private func getThread(with identifier: String, snippet: String?, path: String) async throws -> MessageThread { + func fetchThread(identifier: String, path: String) async throws -> MessageThread { return try await Task.retrying { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery( GTLRGmailQuery_UsersThreadsGet.query(withUserId: .me, identifier: identifier) - ) { (_, data, error) in + ) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - guard let thread = data as? GTLRGmail_Thread else { + guard let gmailThread = data as? GTLRGmail_Thread else { return continuation.resume(throwing: AppErr.cast("GTLRGmail_Thread")) } - guard let threadMsg = thread.messages else { - let empty = MessageThread( - identifier: identifier, - snippet: snippet, - path: path, - messages: [] - ) - return continuation.resume(returning: empty) - } - - let messages = threadMsg.compactMap { try? Message(gmailMessage: $0) } + let messages = gmailThread.messages?.compactMap { try? Message(gmailMessage: $0) } ?? [] - let result = MessageThread( - identifier: thread.identifier, - snippet: snippet, - path: path, + let thread = MessageThread( + identifier: gmailThread.identifier, + snippet: gmailThread.snippet, messages: messages ) - return continuation.resume(returning: result) + return continuation.resume(returning: thread) } } }.value diff --git a/FlowCrypt/Functionality/Pgp/KeyMethods.swift b/FlowCrypt/Functionality/Pgp/KeyMethods.swift index 96ce4ce6e..af99f43c0 100644 --- a/FlowCrypt/Functionality/Pgp/KeyMethods.swift +++ b/FlowCrypt/Functionality/Pgp/KeyMethods.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol KeyMethodsType { func filterByPassPhraseMatch(keys: [T], passPhrase: String) async throws -> [T] @@ -56,7 +55,7 @@ final class KeyMethods: KeyMethodsType { guard parsed.isNotEmpty else { throw KeypairError.noAccountKeysAvailable } - let usable = parsed.filter { $0.isKeyUsable } + let usable = parsed.filter(\.isKeyUsable) guard usable.isNotEmpty else { throw MessageValidationError.noUsableAccountKeys } diff --git a/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift b/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift index d11f3e259..a0c6175d3 100644 --- a/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift +++ b/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift @@ -6,8 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation -import UIKit import Photos import PhotosUI diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 280649cc9..e2cbf0d83 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -11,16 +11,16 @@ import UIKit final class BackupService { let backupProvider: BackupProvider let core: Core - let messageSender: MessageGateway + let messageGateway: MessageGateway init( backupProvider: BackupProvider, core: Core = .shared, - messageSender: MessageGateway + messageGateway: MessageGateway ) { self.backupProvider = backupProvider self.core = core - self.messageSender = messageSender + self.messageGateway = messageGateway } } @@ -70,7 +70,7 @@ extension BackupService: BackupServiceType { ) let t = try await core.composeEmail(msg: message, fmt: .plain) - try await messageSender.sendMail( + _ = try await messageGateway.sendMail( input: MessageGatewayInput(mime: t.mimeEncoded, threadId: nil), progressHandler: nil ) diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift index ee19d3db8..dd4dc7be8 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation /// Organisational rules, set domain-wide, and delivered from FlowCrypt Backend /// These either enforce, alter or forbid various behavior to fit customer needs diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift index 73c06e8ec..4b7297583 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol ClientConfigurationServiceType { var configuration: ClientConfiguration { get async throws } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift index f325518a4..38c2f22d9 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalClientConfigurationType { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index c28984f7b..cb82913c4 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -9,13 +9,14 @@ import Foundation struct ComposeMessageContext: Equatable { + var sender: String var message: String? var recipients: [ComposeMessageRecipient] var subject: String? var attachments: [MessageAttachment] var messagePassword: String? { get { - (_messagePassword ?? "").isNotEmpty ? _messagePassword : nil + _messagePassword.isEmptyOrNil ? nil : _messagePassword } set { _messagePassword = newValue } } @@ -24,12 +25,15 @@ struct ComposeMessageContext: Equatable { } extension ComposeMessageContext { - init(message: String? = nil, - recipients: [ComposeMessageRecipient] = [], - subject: String? = nil, - attachments: [MessageAttachment] = [], - messagePassword: String? = nil + init( + sender: String, + message: String? = nil, + recipients: [ComposeMessageRecipient] = [], + subject: String? = nil, + attachments: [MessageAttachment] = [], + messagePassword: String? = nil ) { + self.sender = sender self.message = message self.recipients = recipients self.subject = subject @@ -44,11 +48,15 @@ extension ComposeMessageContext { } var hasCcOrBccRecipients: Bool { - recipients.first(where: { $0.type == .cc || $0.type == .bcc }) != nil + recipients.contains(where: { $0.type == .cc || $0.type == .bcc }) + } + + var hasRecipientsWithActivePubKey: Bool { + recipients.contains(where: { $0.keyState == .active }) } var hasRecipientsWithoutPubKey: Bool { - recipients.first { $0.keyState == .empty } != nil + recipients.contains(where: { $0.keyState == .empty }) } var hasMessagePasswordIfNeeded: Bool { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index 5091c8330..22b3b4cd6 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -57,7 +57,7 @@ enum ComposeMessageError: Error, CustomStringConvertible, Equatable { case passPhraseRequired case passPhraseNoMatch case gatewayError(Error) - case promptUserToEnterPassPhraseForSigningKey(Keypair) + case missingPassPhrase(Keypair) case noKeysFoundForSign(Int, String) var description: String { @@ -68,7 +68,7 @@ enum ComposeMessageError: Error, CustomStringConvertible, Equatable { return "compose_sign_passphrase_required".localized case .passPhraseNoMatch: return "compose_sign_passphrase_no_match".localized - case .noKeysFoundForSign(let count, let sender): + case let .noKeysFoundForSign(count, sender): return "compose_sign_no_keys".localizeWithArguments("\(count)", sender) case .gatewayError(let error): return error.localizedDescription diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index a70ea30f8..18947c544 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -7,10 +7,7 @@ // import FlowCryptUI -import Foundation -import GoogleAPIClientForREST_Gmail import FlowCryptCommon -import UIKit typealias RecipientState = RecipientEmailsCellNode.Input.State @@ -20,6 +17,15 @@ protocol CoreComposeMessageType { func encrypt(file: Data, name: String, pubKeys: [String]?) async throws -> Data } +enum DraftSaveState { + case cancelled, saving(ComposedDraft), success(SendableMsg), error(Error) +} + +struct ComposedDraft: Equatable { + let input: ComposeMessageInput + let contextToSend: ComposeMessageContext +} + final class ComposeMessageService { private let appContext: AppContextWithUser @@ -29,6 +35,8 @@ final class ComposeMessageService { private let draftGateway: DraftGateway? private lazy var logger = Logger.nested(Self.self) + private var saveDraftTask: Task? + private struct ReplyInfo: Encodable { let sender: String let recipient: [String] @@ -50,7 +58,6 @@ final class ComposeMessageService { self.draftGateway = draftGateway self.core = core self.localContactsProvider = localContactsProvider ?? LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) - self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService") } private var onStateChanged: ((State) -> Void)? @@ -65,14 +72,14 @@ final class ComposeMessageService { throw ComposeMessageError.noKeysFoundForSign(keys.count, senderEmail) } if signingKey.passphrase == nil { - throw ComposeMessageError.promptUserToEnterPassPhraseForSigningKey(signingKey) + throw ComposeMessageError.missingPassPhrase(signingKey) } return signingKey } func handlePassPhraseEntry(_ passPhrase: String, for signingKey: Keypair) async throws -> Bool { // since pass phrase was entered (an inconvenient thing for user to do), - // let's find all keys that match and save the pass phrase for all + // let's find all keys that match and save the pass phrase for all let allKeys = try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender) guard allKeys.isNotEmpty else { throw KeypairError.noAccountKeysAvailable @@ -91,89 +98,105 @@ final class ComposeMessageService { // MARK: - Validation func validateAndProduceSendableMsg( - senderEmail: String, input: ComposeMessageInput, contextToSend: ComposeMessageContext, - includeAttachments: Bool = true + isDraft: Bool = false, + withPubKeys: Bool = true ) async throws -> SendableMsg { - onStateChanged?(.validatingMessage) - - let recipients = contextToSend.recipients - guard recipients.isNotEmpty else { - throw MessageValidationError.emptyRecipient - } + let subject = contextToSend.subject ?? "" - let emails = recipients.map(\.email) - let emptyEmails = emails.filter { !$0.hasContent } + if !isDraft { + onStateChanged?(.validatingMessage) - guard emails.isNotEmpty, emptyEmails.isEmpty else { - throw MessageValidationError.emptyRecipient - } - - guard emails.filter({ !$0.isValidEmail }).isEmpty else { - throw MessageValidationError.invalidEmailRecipient - } - - guard input.isQuote || contextToSend.subject?.hasContent ?? false else { - throw MessageValidationError.emptySubject - } - - guard let text = contextToSend.message, text.hasContent else { - throw MessageValidationError.emptyMessage - } - - let subject = contextToSend.subject ?? "(no subject)" + guard contextToSend.recipients.isNotEmpty else { + throw MessageValidationError.emptyRecipient + } - let senderKeys = try await keyMethods.chooseSenderKeys( - for: .encryption, - keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), - senderEmail: senderEmail - ) + let emails = contextToSend.recipients.map(\.email) + let emptyEmails = emails.filter { !$0.hasContent } - guard senderKeys.isNotEmpty else { - throw MessageValidationError.noUsableAccountKeys - } + guard emails.isNotEmpty, emptyEmails.isEmpty else { + throw MessageValidationError.emptyRecipient + } - let sendableAttachments: [SendableMsg.Attachment] = includeAttachments - ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } - : [] + guard !emails.contains(where: { !$0.isValidEmail }) else { + throw MessageValidationError.invalidEmailRecipient + } - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword - ) + guard input.isQuote || subject.hasContent else { + throw MessageValidationError.emptySubject + } - if let password = contextToSend.messagePassword, password.isNotEmpty { - if subject.lowercased().contains(password.lowercased()) { - throw MessageValidationError.subjectContainsPassword + guard let text = contextToSend.message, text.hasContent else { + throw MessageValidationError.emptyMessage } - let allAvailablePassPhrases = try appContext.combinedPassPhraseStorage.getPassPhrases(for: sender).map(\.value) - if allAvailablePassPhrases.contains(password) { - throw MessageValidationError.notUniquePassword + if let password = contextToSend.messagePassword, password.isNotEmpty { + if subject.lowercased().contains(password.lowercased()) { + throw MessageValidationError.subjectContainsPassword + } + + let allAvailablePassPhrases = try appContext.combinedPassPhraseStorage.getPassPhrases(for: sender).map(\.value) + if allAvailablePassPhrases.contains(password) { + throw MessageValidationError.notUniquePassword + } } } - let signingPrv = try await prepareSigningKey(senderEmail: senderEmail) + let pubKeys = withPubKeys ? try await getPubKeys( + senderEmail: contextToSend.sender, + recipients: contextToSend.recipients, + hasMessagePassword: contextToSend.hasMessagePassword, + shouldValidate: !isDraft + ) : [] + + let sendableAttachments: [SendableMsg.Attachment] = isDraft + ? [] + : contextToSend.attachments.map(\.sendableMsgAttachment) + let signingPrv = isDraft ? nil : try await prepareSigningKey(senderEmail: contextToSend.sender) return SendableMsg( - text: text, + text: contextToSend.message ?? "", html: nil, to: contextToSend.recipientEmails(type: .to), cc: contextToSend.recipientEmails(type: .cc), bcc: contextToSend.recipientEmails(type: .bcc), - from: senderEmail, + from: contextToSend.sender, subject: subject, replyToMsgId: input.replyToMsgId, inReplyTo: input.inReplyTo, atts: sendableAttachments, - pubKeys: senderKeys.map(\.public) + validPubKeys, + pubKeys: pubKeys, signingPrv: signingPrv, password: contextToSend.messagePassword ) } + private func getPubKeys( + senderEmail: String, + recipients: [ComposeMessageRecipient], + hasMessagePassword: Bool, + shouldValidate: Bool + ) async throws -> [String] { + let senderKeys = try await keyMethods.chooseSenderKeys( + for: .encryption, + keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), + senderEmail: senderEmail + ).map(\.public) + + if shouldValidate, senderKeys.isEmpty { + throw MessageValidationError.noUsableAccountKeys + } + + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + if shouldValidate { + try validate(recipients: recipientsWithPubKeys, hasMessagePassword: hasMessagePassword) + } + let validPubKeys = recipientsWithPubKeys.flatMap(\.activePubKeys).map(\.armored) + + return senderKeys + validPubKeys + } + private func getRecipientKeys(for composeRecipients: [ComposeMessageRecipient]) async throws -> [RecipientWithSortedPubKeys] { let recipients = composeRecipients.map(Recipient.init) var recipientsWithKeys: [RecipientWithSortedPubKeys] = [] @@ -190,10 +213,12 @@ final class ComposeMessageService { return recipientsWithKeys } - private func validate(recipients: [RecipientWithSortedPubKeys], - hasMessagePassword: Bool) throws -> [String] { + private func validate( + recipients: [RecipientWithSortedPubKeys], + hasMessagePassword: Bool + ) throws { func contains(keyState: PubKeyState) -> Bool { - recipients.first(where: { $0.keyState == keyState }) != nil + recipients.contains(where: { $0.keyState == keyState }) } logger.logDebug("validate recipients: \(recipients)") @@ -209,30 +234,63 @@ final class ComposeMessageService { guard !contains(keyState: .revoked) else { throw MessageValidationError.revokedKeyRecipients } - - return recipients.flatMap(\.activePubKeys).map(\.armored) } // MARK: - Drafts - private var draft: GTLRGmail_Draft? - func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { - do { - let r = try await core.composeEmail( - msg: message, - fmt: .encryptInline - ) - draft = try await draftGateway?.saveDraft(input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), draft: draft) - } catch { - throw ComposeMessageError.gatewayError(error) + var messageIdentifier: MessageIdentifier? + + func fetchMessageIdentifier(info: ComposeMessageInput.MessageQuoteInfo) { + Task { + if let draftId = info.draftId { + messageIdentifier = try await draftGateway?.fetchDraft(id: draftId) + } else if let messageId = info.rfc822MsgId { + let identifier = Identifier(stringId: messageId) + messageIdentifier = try await draftGateway?.fetchDraftIdentifier(for: identifier) + } + } + } + + func saveDraft(message: SendableMsg, threadId: String?, shouldEncrypt: Bool) async throws { + if let saveDraftTask { + _ = try await saveDraftTask.value + } + + saveDraftTask = Task { + do { + let mime = try await self.core.composeEmail( + msg: message, + fmt: shouldEncrypt ? .encryptInline : .plain + ).mimeEncoded + + let threadId = self.messageIdentifier?.threadId?.stringId ?? threadId + + return try await self.draftGateway?.saveDraft( + input: MessageGatewayInput( + mime: mime, + threadId: threadId + ), + draftId: self.messageIdentifier?.draftId + ) + } catch { + throw ComposeMessageError.gatewayError(error) + } } + + messageIdentifier = try await saveDraftTask?.value + saveDraftTask = nil + } + + func deleteDraft() async throws { + guard let draftId = messageIdentifier?.draftId else { return } + try await draftGateway?.deleteDraft(with: draftId) } // MARK: - Encrypt and Send - func encryptAndSend(message: SendableMsg, threadId: String?) async throws { + func encryptAndSend(message: SendableMsg, threadId: String?) async throws -> Identifier { do { onStateChanged?(.startComposing) - let hasPassword = (message.password ?? "").isNotEmpty + let hasPassword = !message.password.isEmptyOrNil let composedEmail: CoreRes.ComposeEmail if hasPassword { @@ -249,7 +307,7 @@ final class ComposeMessageService { threadId: threadId ) - try await appContext.getRequiredMailProvider().messageSender.sendMail( + let identifier = try await appContext.getRequiredMailProvider().messageGateway.sendMail( input: input, progressHandler: { [weak self] progress in let progressToShow = hasPassword ? 0.5 + progress / 2 : progress @@ -258,11 +316,13 @@ final class ComposeMessageService { ) // cleaning any draft saved/created/fetched during editing - if let draftId = draft?.identifier { - await draftGateway?.deleteDraft(with: draftId) + if let draftId = messageIdentifier?.draftId { + try await draftGateway?.deleteDraft(with: draftId) } onStateChanged?(.messageSent) + + return identifier } catch { throw ComposeMessageError.gatewayError(error) } @@ -376,7 +436,6 @@ extension ComposeMessageService { return "
" } - // TODO: - Anton - compose_password_link private func createMessageBodyWithPasswordLink(sender: String, url: String) -> SendableMsgBody { let text = "compose_password_link".localizeWithArguments(sender, url) let aStyle = "padding: 2px 6px; background: #2199e8; color: #fff; display: inline-block; text-decoration: none;" diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index a229e10cb..7dcb6d209 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import UIKit protocol EKMVcHelperType { @@ -147,7 +146,7 @@ final class EKMVcHelper: EKMVcHelperType { @MainActor private func requestPassPhraseWithModal(in viewController: UIViewController) async throws -> String { - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in let alert = alertsFactory.makePassPhraseAlert( title: "refresh_key_alert_title".localized, onCancel: { @@ -160,7 +159,7 @@ final class EKMVcHelper: EKMVcHelperType { viewController.presentedViewController?.dismiss(animated: true) - Task { + Task { do { let matched = try await self.handlePassPhraseEntry( appContext: self.appContext, diff --git a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift index b4aa2c57c..2723e9550 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol TrashFolderProviderType { var trashFolderPath: String? { get async throws } diff --git a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift index 7caf2aa76..eb2cdeb70 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalFoldersProviderType { diff --git a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift index 3ebea2ab1..c8fdf441e 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: RemoteFoldersProviderType { diff --git a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift index 225816d11..63d2e595d 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift @@ -2,12 +2,11 @@ // Imap+folders.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 9/11/19. +// Created by Anton Kharchevskyi on 9/11/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // import FlowCryptCommon -import Foundation import MailCore // MARK: - RemoteFoldersProviderType diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift index 51a0e84c7..0ee635578 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift @@ -64,7 +64,7 @@ extension GoogleUserService { return contactsScope.allSatisfy(currentScope.contains) } - internal func runWarmupQuery() { + func runWarmupQuery() { Task { // Warmup query for google contacts cache _ = await searchContacts(query: "") diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index d4447d41b..cacada275 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -9,7 +9,6 @@ import AppAuth import Combine import FlowCryptCommon -import Foundation import GTMAppAuth import RealmSwift import GoogleAPIClientForREST_Oauth2 @@ -30,7 +29,7 @@ enum GoogleUserServiceError: Error, CustomStringConvertible { switch self { case .cancelledAuthorization: return "google_user_service_error_auth_cancelled".localized - case .wrongAccount(let signedAccount, let currentAccount): + case let .wrongAccount(signedAccount, currentAccount): return "google_user_service_error_wrong_account".localizeWithArguments( signedAccount, currentAccount, currentAccount ) @@ -105,7 +104,7 @@ final class GoogleUserService: NSObject, GoogleUserServiceType { static let index = "GTMAppAuthAuthorizerIndex" } - internal lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) + lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) private var tokenResponse: OIDTokenResponse? { authorization?.authState.lastTokenResponse @@ -155,7 +154,7 @@ extension GoogleUserService: UserServiceType { return continuation.resume(throwing: error) } } - Task { + Task { do { return continuation.resume( returning: try await self.handleGoogleAuthStateResult( @@ -275,7 +274,7 @@ extension GoogleUserService { private func fetchGoogleUser( with authorization: GTMAppAuthFetcherAuthorization ) async throws -> GTLROauth2_Userinfo { - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in let query = GTLROauth2Query_UserinfoGet.query() let authService = GTLROauth2Service() if Bundle.shouldUseMockGmailApi { @@ -326,7 +325,7 @@ extension GoogleUserService { .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") - while decodedString.utf16.count % 4 != 0 { + while !decodedString.utf16.count.isMultiple(of: 4) { decodedString += "=" } diff --git a/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift index f24d400f7..e79dd935a 100644 --- a/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift import FlowCryptCommon diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift index d734ab891..b7380e41b 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift @@ -74,7 +74,7 @@ protocol InMemoryPassPhraseProviderType { /// - Warning: - should be shared instance final class InMemoryPassPhraseProvider: InMemoryPassPhraseProviderType { - static let shared: InMemoryPassPhraseProvider = InMemoryPassPhraseProvider() + static let shared = InMemoryPassPhraseProvider() private(set) var passPhrases: Set = [] diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift index 7a3aafe0b..d8c697bcc 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol KeyAndPassPhraseStorageType { @@ -31,8 +30,7 @@ final class KeyAndPassPhraseStorage: KeyAndPassPhraseStorageType { var keypairs = try encryptedStorage.getKeypairs(by: email) for i in keypairs.indices { keypairs[i].passphrase = keypairs[i].passphrase ?? storedPassPhrases - .filter { $0.value.isNotEmpty } - .first(where: { $0.primaryFingerprintOfAssociatedKey == keypairs[i].primaryFingerprint })? + .first(where: { $0.value.isNotEmpty && $0.primaryFingerprintOfAssociatedKey == keypairs[i].primaryFingerprint })? .value } return keypairs diff --git a/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift b/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift index db343237b..8aa04e125 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalSendAsProviderType { diff --git a/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift b/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift index 48a2b707e..d7887705d 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift @@ -14,8 +14,8 @@ struct SendAsModel { let isDefault: Bool let verificationStatus: SendAsVerificationStatus - var descriptoin: String { - if displayName == "" { + var description: String { + if displayName.isEmpty { return sendAsEmail } return "\(sendAsEmail) (\(displayName))" diff --git a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift index a29fe33ef..c502aae8e 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: RemoteSendAsProviderType { diff --git a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift index 556b967e7..00f3641ac 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift @@ -2,7 +2,7 @@ // RemoteSendAsProviderType.swift // FlowCrypt // -// Created by Ioan Moldovan on 06/13/22. +// Created by Ioan Moldovan on 06/13/22 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift index ccb1d9ff7..a442c554f 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol SendAsServiceType { func fetchList(isForceReload: Bool, for user: User) async throws -> [SendAsModel] @@ -49,9 +48,9 @@ final class SendAsService: SendAsServiceType { try self.localSendAsProvider.save(list: fetchedList, for: user) // return list - continuation.resume(returning: fetchedList) + return continuation.resume(returning: fetchedList) } catch { - continuation.resume(throwing: error) + return continuation.resume(throwing: error) } } } diff --git a/FlowCrypt/Models/Common/Keypair.swift b/FlowCrypt/Models/Common/Keypair.swift index 3baf2cfeb..83865cc85 100644 --- a/FlowCrypt/Models/Common/Keypair.swift +++ b/FlowCrypt/Models/Common/Keypair.swift @@ -58,8 +58,8 @@ extension Keypair { self.public = object.public self.passphrase = object.passphrase self.source = object.source - self.allFingerprints = object.allFingerprints.map { $0 } - self.allLongids = object.allLongids.map { $0 } + self.allFingerprints = Array(object.allFingerprints) + self.allLongids = Array(object.allLongids) self.lastModified = object.lastModified self.isRevoked = object.isRevoked } diff --git a/FlowCrypt/Models/Common/Recipient.swift b/FlowCrypt/Models/Common/Recipient.swift index b90ac418e..919f56201 100644 --- a/FlowCrypt/Models/Common/Recipient.swift +++ b/FlowCrypt/Models/Common/Recipient.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore import GoogleAPIClientForREST_PeopleService diff --git a/FlowCrypt/Models/Common/RecipientBase.swift b/FlowCrypt/Models/Common/RecipientBase.swift index 5cd823b7f..a4d2429fc 100644 --- a/FlowCrypt/Models/Common/RecipientBase.swift +++ b/FlowCrypt/Models/Common/RecipientBase.swift @@ -15,7 +15,7 @@ protocol RecipientBase { extension RecipientBase { var formatted: String { - guard let name = name else { return email } + guard let name else { return email } return "\(name) <\(email)>" } @@ -26,7 +26,7 @@ extension RecipientBase { } var displayName: String { - if let name = name, name != "" { + if let name, !name.isEmpty { return name } return email diff --git a/FlowCrypt/Models/Contact Models/PubKey.swift b/FlowCrypt/Models/Contact Models/PubKey.swift index 79de16a69..8eac2c752 100644 --- a/FlowCrypt/Models/Contact Models/PubKey.swift +++ b/FlowCrypt/Models/Contact Models/PubKey.swift @@ -77,8 +77,8 @@ extension PubKey { self.lastSig = object.lastSig self.lastChecked = object.lastChecked self.expiresOn = object.expiresOn - self.longids = object.longids.map { $0 } - self.fingerprints = object.fingerprints.map { $0 } + self.longids = Array(object.longids) + self.fingerprints = Array(object.fingerprints) self.created = object.created self.algo = nil diff --git a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift index 1e154c1a2..d09c8042e 100644 --- a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift +++ b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift @@ -14,15 +14,7 @@ struct InboxViewModel { init(folderName: String, path: String) { self.folderName = folderName - if folderName.isEmpty { - self.path = "Inbox" - } else { - self.path = path - } - } - - var isDrafts: Bool { - return folderName == "Draft" + self.path = folderName.isEmpty ? "Inbox" : path } } diff --git a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift index 75227461b..9846654b4 100644 --- a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class ClientConfigurationRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift index 3ef12be67..e8218108b 100644 --- a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class FolderRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift b/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift index 7b7b79af6..d7210e9c8 100644 --- a/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import RealmSwift import CryptoKit diff --git a/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift b/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift index 54e99a9ec..95e1a3542 100644 --- a/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift enum PubKeyObjectError: Error { diff --git a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift index 0d03e500d..71c7b0d9c 100644 --- a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class RecipientRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift b/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift index 5b2e5fb9a..282c68219 100644 --- a/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class SendAsRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/UserRealmObject.swift b/FlowCrypt/Models/Realm Models/UserRealmObject.swift index 100d897b8..61f9690a7 100644 --- a/FlowCrypt/Models/Realm Models/UserRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/UserRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class UserRealmObject: Object { diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 8e6f69438..57d699dea 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -49,7 +49,7 @@ "message_compose_secure" = "Compose Secure Message"; "message_unknown_sender" = "(unknown sender)"; "message_no_recipients" = "(no recipients)"; -"message_missing_subject" = "No subject"; +"message_missing_subject" = "(no subject)"; "message_attachment_saved_successfully_title" = "Attachment Saved"; "message_attachment_saved_successfully_message" = "Your attachment was saved in Files. Would you like to open it?"; "message_attachment_saved_with_error" = "Attachment could not be saved. %@"; @@ -69,6 +69,7 @@ "message_decrypt_error" = "decrypt error"; "message_mark_read_error" = "Could not mark message as read: %@"; "message_reply_all" = "Reply all"; +"message_not_found_in_folder" = "Message not found in folder: "; // ERROR "error_fetch_folders" = "Could not fetch folders"; @@ -129,7 +130,7 @@ "compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)\n\nPassword should include:\n- one uppercase\n- one lowercase\n- one number\n- one special character eg &/#\"-'_%-@,;:!*()\n- min 8 characters length"; "compose_error" = "Could not compose message"; "compose_reply_successful" = "Reply successfully sent"; -"compose_quote_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender +"compose_quote_from" = "On %@, at %@, %@ wrote:"; // Date, time, sender "compose_forward_successful" = "Message forwarded successfully"; "compose_encrypted_sent" = "Encrypted message sent"; "compose_message_sent" = "Message sent"; @@ -152,6 +153,13 @@ "compose_recipient_edit" = "Edit"; "compose_recipient_remove" = "Remove"; +// Drafts +"draft" = "Draft"; +"draft_saving" = "Saving draft..."; +"draft_saved" = "Draft saved"; +"draft_deleted" = "Draft deleted"; +"draft_error" = "Failed to save draft: %@"; + // Folders "folder_all_mail" = "All Mail"; "folder_all_inbox" = "Inbox"; @@ -162,7 +170,7 @@ "folder_SENT" = "Sent"; "folder_IMPORTANT" = "Important"; "folder_TRASH" = "Trash"; -"folder_DRAFT" = "Draft"; +"folder_DRAFT" = "Drafts"; "folder_SPAM" = "Spam"; "folder_STARRED" = "Starred"; "folder_UNREAD" = "Unread"; @@ -402,6 +410,8 @@ "message_permanently_delete" = "You're about to permanently delete a message"; "message_permanently_delete_title" = "Are you sure?"; +"draft_delete_confirmation" = "Delete draft?"; + "contacts_fingerprint" = "Fingerprint:"; "contacts_created" = "Created:"; diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 6bd27082d..48f615e90 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -20636,7 +20636,7 @@ var time_estimates;time_estimates={estimate_attack_times:function(e){var t,n,s,o /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 110: +/***/ 111: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -20644,26 +20644,26 @@ var time_estimates;time_estimates={estimate_attack_times:function(e){var t,n,s,o global.dereq_asn1 = exports; -global.dereq_asn1.bignum = __webpack_require__(111); +global.dereq_asn1.bignum = __webpack_require__(112); -global.dereq_asn1.define = (__webpack_require__(113).define); -global.dereq_asn1.base = __webpack_require__(127); -global.dereq_asn1.constants = __webpack_require__(128); -global.dereq_asn1.decoders = __webpack_require__(124); -global.dereq_asn1.encoders = __webpack_require__(114); +global.dereq_asn1.define = (__webpack_require__(114).define); +global.dereq_asn1.base = __webpack_require__(128); +global.dereq_asn1.constants = __webpack_require__(129); +global.dereq_asn1.decoders = __webpack_require__(125); +global.dereq_asn1.encoders = __webpack_require__(115); /***/ }), -/***/ 113: +/***/ 114: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const encoders = __webpack_require__(114); -const decoders = __webpack_require__(124); -const inherits = __webpack_require__(116); +const encoders = __webpack_require__(115); +const decoders = __webpack_require__(125); +const inherits = __webpack_require__(117); const api = exports; @@ -20720,15 +20720,15 @@ Entity.prototype.encode = function encode(data, enc, /* internal */ reporter) { /***/ }), -/***/ 120: +/***/ 121: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Reporter = (__webpack_require__(119).Reporter); -const Buffer = (__webpack_require__(117).Buffer); +const inherits = __webpack_require__(117); +const Reporter = (__webpack_require__(120).Reporter); +const Buffer = (__webpack_require__(118).Buffer); function DecoderBuffer(base, options) { Reporter.call(this, options); @@ -20881,7 +20881,7 @@ EncoderBuffer.prototype.join = function join(out, offset) { /***/ }), -/***/ 127: +/***/ 128: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -20889,24 +20889,24 @@ EncoderBuffer.prototype.join = function join(out, offset) { const base = exports; -base.Reporter = (__webpack_require__(119).Reporter); -base.DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -base.EncoderBuffer = (__webpack_require__(120).EncoderBuffer); -base.Node = __webpack_require__(118); +base.Reporter = (__webpack_require__(120).Reporter); +base.DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +base.EncoderBuffer = (__webpack_require__(121).EncoderBuffer); +base.Node = __webpack_require__(119); /***/ }), -/***/ 118: +/***/ 119: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const Reporter = (__webpack_require__(119).Reporter); -const EncoderBuffer = (__webpack_require__(120).EncoderBuffer); -const DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -const assert = __webpack_require__(121); +const Reporter = (__webpack_require__(120).Reporter); +const EncoderBuffer = (__webpack_require__(121).EncoderBuffer); +const DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +const assert = __webpack_require__(122); // Supported tags const tags = [ @@ -21543,13 +21543,13 @@ Node.prototype._isPrintstr = function isPrintstr(str) { /***/ }), -/***/ 119: +/***/ 120: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); function Reporter(options) { this._reporterState = { @@ -21674,7 +21674,7 @@ ReporterError.prototype.rethrow = function rethrow(msg) { /***/ }), -/***/ 122: +/***/ 123: /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -21740,7 +21740,7 @@ exports.tagByName = reverse(exports.tag); /***/ }), -/***/ 128: +/***/ 129: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -21764,25 +21764,25 @@ constants._reverse = function reverse(map) { return res; }; -constants.der = __webpack_require__(122); +constants.der = __webpack_require__(123); /***/ }), -/***/ 125: +/***/ 126: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); -const bignum = __webpack_require__(111); -const DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -const Node = __webpack_require__(118); +const bignum = __webpack_require__(112); +const DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +const Node = __webpack_require__(119); // Import DER constants -const der = __webpack_require__(122); +const der = __webpack_require__(123); function DERDecoder(entity) { this.enc = 'der'; @@ -22112,7 +22112,7 @@ function derDecodeLen(buf, primitive, fail) { /***/ }), -/***/ 124: +/***/ 125: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -22120,22 +22120,22 @@ function derDecodeLen(buf, primitive, fail) { const decoders = exports; -decoders.der = __webpack_require__(125); -decoders.pem = __webpack_require__(126); +decoders.der = __webpack_require__(126); +decoders.pem = __webpack_require__(127); /***/ }), -/***/ 126: +/***/ 127: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Buffer = (__webpack_require__(117).Buffer); +const inherits = __webpack_require__(117); +const Buffer = (__webpack_require__(118).Buffer); -const DERDecoder = __webpack_require__(125); +const DERDecoder = __webpack_require__(126); function PEMDecoder(entity) { DERDecoder.call(this, entity); @@ -22185,18 +22185,18 @@ PEMDecoder.prototype.decode = function decode(data, options) { /***/ }), -/***/ 115: +/***/ 116: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Buffer = (__webpack_require__(117).Buffer); -const Node = __webpack_require__(118); +const inherits = __webpack_require__(117); +const Buffer = (__webpack_require__(118).Buffer); +const Node = __webpack_require__(119); // Import DER constants -const der = __webpack_require__(122); +const der = __webpack_require__(123); function DEREncoder(entity) { this.enc = 'der'; @@ -22488,7 +22488,7 @@ function encodeTag(tag, primitive, cls, reporter) { /***/ }), -/***/ 114: +/***/ 115: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -22496,21 +22496,21 @@ function encodeTag(tag, primitive, cls, reporter) { const encoders = exports; -encoders.der = __webpack_require__(115); -encoders.pem = __webpack_require__(123); +encoders.der = __webpack_require__(116); +encoders.pem = __webpack_require__(124); /***/ }), -/***/ 123: +/***/ 124: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); -const DEREncoder = __webpack_require__(115); +const DEREncoder = __webpack_require__(116); function PEMEncoder(entity) { DEREncoder.call(this, entity); @@ -22691,7 +22691,7 @@ function fromByteArray (uint8) { /***/ }), -/***/ 111: +/***/ 112: /***/ (function(module, __unused_webpack_exports, __webpack_require__) { /* module decorator */ module = __webpack_require__.nmd(module); @@ -22750,7 +22750,7 @@ function fromByteArray (uint8) { if (typeof window !== 'undefined' && typeof window.Buffer !== 'undefined') { Buffer = window.Buffer; } else { - Buffer = (__webpack_require__(112).Buffer); + Buffer = (__webpack_require__(113).Buffer); } } catch (e) { } @@ -28351,7 +28351,7 @@ exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { /***/ }), -/***/ 116: +/***/ 117: /***/ ((module) => { if (typeof Object.create === 'function') { @@ -28385,7 +28385,7 @@ if (typeof Object.create === 'function') { /***/ }), -/***/ 121: +/***/ 122: /***/ ((module) => { module.exports = assert; @@ -28403,7 +28403,7 @@ assert.equal = function assertEqual(l, r, msg) { /***/ }), -/***/ 117: +/***/ 118: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; @@ -28488,7 +28488,7 @@ module.exports = safer /***/ }), -/***/ 112: +/***/ 113: /***/ (() => { /* (ignored) */ @@ -28539,7 +28539,7 @@ module.exports = safer /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __webpack_require__(110); +/******/ var __webpack_exports__ = __webpack_require__(111); /******/ module.exports = __webpack_exports__; /******/ /******/ })() @@ -28555,20 +28555,20 @@ module.exports = safer Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getSigningPrv = exports.Endpoints = void 0; const format_output_1 = __webpack_require__(2); -const pgp_msg_1 = __webpack_require__(107); -const pgp_key_1 = __webpack_require__(102); +const pgp_msg_1 = __webpack_require__(108); +const pgp_key_1 = __webpack_require__(103); const mime_1 = __webpack_require__(22); -const att_1 = __webpack_require__(32); +const att_1 = __webpack_require__(33); const buf_1 = __webpack_require__(4); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_password_1 = __webpack_require__(108); -const store_1 = __webpack_require__(103); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_password_1 = __webpack_require__(109); +const store_1 = __webpack_require__(104); const common_1 = __webpack_require__(23); -const const_1 = __webpack_require__(106); -const validate_input_1 = __webpack_require__(109); -const xss_1 = __webpack_require__(35); -const const_2 = __webpack_require__(106); -const openpgp_1 = __webpack_require__(101); +const const_1 = __webpack_require__(107); +const validate_input_1 = __webpack_require__(110); +const xss_1 = __webpack_require__(36); +const const_2 = __webpack_require__(107); +const openpgp_1 = __webpack_require__(102); class Endpoints { constructor() { this.version = async () => { @@ -28595,7 +28595,11 @@ class Endpoints { } if (req.format === 'plain') { const atts = (req.atts || []).map(({ name, type, base64 }) => new att_1.Att({ name, type, data: buf_1.Buf.fromBase64Str(base64) })); - return (0, format_output_1.fmtRes)({}, buf_1.Buf.fromUtfStr(await mime_1.Mime.encode({ 'text/plain': req.text, 'text/html': req.html }, mimeHeaders, atts))); + const body = { 'text/plain': req.text }; + if (req.html) { + body['text/html'] = req.html; + } + return (0, format_output_1.fmtRes)({}, buf_1.Buf.fromUtfStr(await mime_1.Mime.encode(body, mimeHeaders, atts))); } else if (req.format === 'encrypt-inline') { const encryptedAtts = []; @@ -28921,7 +28925,7 @@ const msg_block_1 = __webpack_require__(3); const buf_1 = __webpack_require__(4); const mime_1 = __webpack_require__(22); const common_1 = __webpack_require__(23); -const xss_1 = __webpack_require__(35); +const xss_1 = __webpack_require__(36); const isContentBlock = (t) => { return t === 'plainText' || t === 'decryptedText' || t === 'plainHtml' || t === 'decryptedHtml' || t === 'signedMsg' || t === 'verifiedMsg'; @@ -37915,12 +37919,12 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Mime = void 0; const common_1 = __webpack_require__(23); const require_1 = __webpack_require__(24); -const att_1 = __webpack_require__(32); +const att_1 = __webpack_require__(33); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); +const catch_1 = __webpack_require__(34); const msg_block_1 = __webpack_require__(3); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); const util_1 = __webpack_require__(5); const MimeParser = (0, require_1.requireMimeParser)(); const MimeBuilder = (0, require_1.requireMimeBuilder)(); @@ -38557,7 +38561,7 @@ const requireStreamReadToEnd = async () => { const runtime = globalThis.process?.release?.name || 'not node'; return runtime === 'not node' ? (await Promise.resolve().then(() => __webpack_require__(25))).readToEnd - : (__webpack_require__(31).readToEnd); + : (__webpack_require__(32).readToEnd); }; exports.requireStreamReadToEnd = requireStreamReadToEnd; const requireMimeParser = () => { @@ -38582,9 +38586,6 @@ exports.requireIso88592 = requireIso88592; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "ArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_3__.ArrayStream), -/* harmony export */ "ReadableStream": () => (/* binding */ ReadableStream), -/* harmony export */ "TransformStream": () => (/* binding */ TransformStream), -/* harmony export */ "WritableStream": () => (/* binding */ WritableStream), /* harmony export */ "cancel": () => (/* binding */ cancel), /* harmony export */ "clone": () => (/* binding */ clone), /* harmony export */ "concat": () => (/* binding */ concat), @@ -38596,15 +38597,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ "isArrayStream": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream), /* harmony export */ "isStream": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isStream), /* harmony export */ "isUint8Array": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isUint8Array), -/* harmony export */ "loadStreamsPonyfill": () => (/* binding */ loadStreamsPonyfill), /* harmony export */ "nodeToWeb": () => (/* reexport safe */ _node_conversions__WEBPACK_IMPORTED_MODULE_1__.nodeToWeb), /* harmony export */ "parse": () => (/* binding */ parse), /* harmony export */ "passiveClone": () => (/* binding */ passiveClone), /* harmony export */ "pipe": () => (/* binding */ pipe), /* harmony export */ "readToEnd": () => (/* binding */ readToEnd), /* harmony export */ "slice": () => (/* binding */ slice), -/* harmony export */ "toNativeReadable": () => (/* binding */ toNativeReadable), -/* harmony export */ "toPonyfillReadable": () => (/* binding */ toPonyfillReadable), /* harmony export */ "toStream": () => (/* binding */ toStream), /* harmony export */ "transform": () => (/* binding */ transform), /* harmony export */ "transformPair": () => (/* binding */ transformPair), @@ -38613,38 +38611,14 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ }); /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(26); /* harmony import */ var _node_conversions__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(29); -/* harmony import */ var _reader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(30); +/* harmony import */ var _reader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31); /* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27); -let { ReadableStream, WritableStream, TransformStream } = globalThis; - -let toPonyfillReadable, toNativeReadable; - -async function loadStreamsPonyfill() { - if (TransformStream) { - return; - } - - const [ponyfill, adapter] = await Promise.all([ - __webpack_require__.e(/* import() */ 3).then(__webpack_require__.bind(__webpack_require__, 129)), - __webpack_require__.e(/* import() */ 4).then(__webpack_require__.bind(__webpack_require__, 130)) - ]); - - ({ ReadableStream, WritableStream, TransformStream } = ponyfill); - - const { createReadableStreamWrapper } = adapter; - - if (globalThis.ReadableStream && ReadableStream !== globalThis.ReadableStream) { - toPonyfillReadable = createReadableStreamWrapper(ReadableStream); - toNativeReadable = createReadableStreamWrapper(globalThis.ReadableStream); - } -} - -const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(6).Buffer); +const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(30).Buffer); /** * Convert data to Stream @@ -38656,9 +38630,6 @@ function toStream(input) { if (streamType === 'node') { return (0,_node_conversions__WEBPACK_IMPORTED_MODULE_1__.nodeToWeb)(input); } - if (streamType === 'web' && toPonyfillReadable) { - return toPonyfillReadable(input); - } if (streamType) { return input; } @@ -38717,7 +38688,7 @@ function concat(list) { */ function concatStream(list) { list = list.map(toStream); - const transform = transformWithCancel(async function (reason) { + const transform = transformWithCancel(async function(reason) { await Promise.all(transforms.map(stream => cancel(stream, reason))); }); let prev = Promise.resolve(); @@ -38794,7 +38765,7 @@ async function pipe(input, target, { preventAbort, preventCancel }); - } catch (e) { } + } catch(e) {} return; } input = toArrayStream(input); @@ -38852,9 +38823,9 @@ function transformWithCancel(cancel) { } }, cancel - }, { highWaterMark: 0 }), + }, {highWaterMark: 0}), writable: new WritableStream({ - write: async function (chunk) { + write: async function(chunk) { outputController.enqueue(chunk); if (!pulled) { await new Promise(resolve => { @@ -38904,7 +38875,7 @@ function transform(input, process = () => undefined, finish = () => undefined) { try { const result = await process(value); if (result !== undefined) controller.enqueue(result); - } catch (e) { + } catch(e) { controller.error(e); } }, @@ -38912,7 +38883,7 @@ function transform(input, process = () => undefined, finish = () => undefined) { try { const result = await finish(); if (result !== undefined) controller.enqueue(result); - } catch (e) { + } catch(e) { controller.error(e); } } @@ -38944,7 +38915,7 @@ function transformPair(input, fn) { const pipeDonePromise = pipe(input, incoming.writable); - const outgoing = transformWithCancel(async function (reason) { + const outgoing = transformWithCancel(async function(reason) { incomingTransformController.error(reason); await pipeDonePromise; await new Promise(setTimeout); @@ -39042,14 +39013,14 @@ function passiveClone(input) { await writer.ready; const { done, value } = await reader.read(); if (done) { - try { controller.close(); } catch (e) { } + try { controller.close(); } catch(e) {} await writer.close(); return; } - try { controller.enqueue(value); } catch (e) { } + try { controller.enqueue(value); } catch(e) {} await writer.write(value); } - } catch (e) { + } catch(e) { controller.error(e); await writer.abort(e); } @@ -39087,7 +39058,7 @@ function overwrite(input, clone) { * @param {ReadableStream|Uint8array|String} input * @returns {ReadableStream|Uint8array|String} clone */ -function slice(input, begin = 0, end = Infinity) { +function slice(input, begin=0, end=Infinity) { if ((0,_util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { throw new Error('Not implemented'); } @@ -39146,7 +39117,7 @@ function slice(input, begin = 0, end = Infinity) { * @returns {Promise} the return value of join() * @async */ -async function readToEnd(input, join = concat) { +async function readToEnd(input, join=concat) { if ((0,_util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { return input.readToEnd(join); } @@ -39207,14 +39178,12 @@ function fromAsync(fn) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "concatUint8Array": () => (/* binding */ concatUint8Array), -/* harmony export */ "isArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_1__.isArrayStream), +/* harmony export */ "isArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_0__.isArrayStream), /* harmony export */ "isNode": () => (/* binding */ isNode), /* harmony export */ "isStream": () => (/* binding */ isStream), /* harmony export */ "isUint8Array": () => (/* binding */ isUint8Array) /* harmony export */ }); -/* harmony import */ var _streams__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25); -/* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27); - +/* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); const isNode = typeof globalThis.process === 'object' && @@ -39225,18 +39194,15 @@ const NodeReadableStream = isNode && (__webpack_require__(28).Readable); /** * Check whether data is a Stream, and if so of which type * @param {Any} input data to check - * @returns {'web'|'ponyfill'|'node'|'array'|'web-like'|false} + * @returns {'web'|'node'|'array'|'web-like'|false} */ function isStream(input) { - if ((0,_writer__WEBPACK_IMPORTED_MODULE_1__.isArrayStream)(input)) { + if ((0,_writer__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { return 'array'; } if (globalThis.ReadableStream && globalThis.ReadableStream.prototype.isPrototypeOf(input)) { return 'web'; } - if (_streams__WEBPACK_IMPORTED_MODULE_0__.ReadableStream && _streams__WEBPACK_IMPORTED_MODULE_0__.ReadableStream.prototype.isPrototypeOf(input)) { - return 'ponyfill'; - } if (NodeReadableStream && NodeReadableStream.prototype.isPrototypeOf(input)) { return 'node'; } @@ -39431,7 +39397,7 @@ __webpack_require__.r(__webpack_exports__); -const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(6).Buffer); +const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(30).Buffer); const NodeReadableStream = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(28).Readable); /** @@ -39451,7 +39417,7 @@ if (NodeReadableStream) { */ nodeToWeb = function(nodeStream) { let canceled = false; - return new _streams__WEBPACK_IMPORTED_MODULE_1__.ReadableStream({ + return new ReadableStream({ start(controller) { nodeStream.pause(); nodeStream.on('data', chunk => { @@ -39529,6 +39495,12 @@ if (NodeReadableStream) { /***/ }), /* 30 */ +/***/ (() => { + +/* (ignored) */ + +/***/ }), +/* 31 */ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -39745,14 +39717,14 @@ Reader.prototype.readToEnd = async function(join=_streams__WEBPACK_IMPORTED_MODU /***/ }), -/* 31 */ +/* 32 */ /***/ ((module) => { "use strict"; module.exports = require("../../bundles/raw/web-stream-tools"); /***/ }), -/* 32 */ +/* 33 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -39858,7 +39830,7 @@ Att.keyinfoAsPubkeyAtt = (ki) => { /***/ }), -/* 33 */ +/* 34 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -39877,7 +39849,7 @@ Catch.report = (name, details) => { /***/ }), -/* 34 */ +/* 35 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -39886,13 +39858,13 @@ var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.MsgBlockParser = void 0; const msg_block_1 = __webpack_require__(3); -const xss_1 = __webpack_require__(35); +const xss_1 = __webpack_require__(36); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); +const catch_1 = __webpack_require__(34); const mime_1 = __webpack_require__(22); -const pgp_armor_1 = __webpack_require__(100); -const pgp_key_1 = __webpack_require__(102); -const pgp_msg_1 = __webpack_require__(107); +const pgp_armor_1 = __webpack_require__(101); +const pgp_key_1 = __webpack_require__(103); +const pgp_msg_1 = __webpack_require__(108); const common_1 = __webpack_require__(23); class MsgBlockParser { } @@ -40040,11 +40012,11 @@ MsgBlockParser.pushArmoredPubkeysToBlocks = async (armoredPubkeys, blocks) => { /***/ }), -/* 35 */ +/* 36 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -/* provided dependency */ var dereq_sanitize_html = __webpack_require__(36); +/* provided dependency */ var dereq_sanitize_html = __webpack_require__(37); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Xss = void 0; @@ -40173,15 +40145,15 @@ Xss.htmlUnescape = (str) => { /***/ }), -/* 36 */ +/* 37 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { -const htmlparser = __webpack_require__(37); -const escapeStringRegexp = __webpack_require__(62); -const { isPlainObject } = __webpack_require__(63); -const deepmerge = __webpack_require__(64); -const parseSrcset = __webpack_require__(65); -const { parse: postcssParse } = __webpack_require__(66); +const htmlparser = __webpack_require__(38); +const escapeStringRegexp = __webpack_require__(63); +const { isPlainObject } = __webpack_require__(64); +const deepmerge = __webpack_require__(65); +const parseSrcset = __webpack_require__(66); +const { parse: postcssParse } = __webpack_require__(67); // Tags that can conceivably represent stand-alone media. const mediaTags = [ 'img', 'audio', 'video', 'picture', 'svg', @@ -40699,6 +40671,14 @@ function sanitizeHtml(html, options, _recursing) { // Do not crash on bad markup return; } + + if (frame.tag !== name) { + // Another case of bad markup. + // Push to stack, so that it will be used in future closing tags. + stack.push(frame); + return; + } + skipText = options.enforceHtmlBoundary ? name === 'html' : false; depth--; const skip = skipMap[depth]; @@ -41011,7 +40991,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) { /***/ }), -/* 37 */ +/* 38 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41043,9 +41023,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RssHandler = exports.DefaultHandler = exports.DomUtils = exports.ElementType = exports.Tokenizer = exports.createDomStream = exports.parseDOM = exports.parseDocument = exports.DomHandler = exports.Parser = void 0; -var Parser_1 = __webpack_require__(38); +var Parser_1 = __webpack_require__(39); Object.defineProperty(exports, "Parser", ({ enumerable: true, get: function () { return Parser_1.Parser; } })); -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); Object.defineProperty(exports, "DomHandler", ({ enumerable: true, get: function () { return domhandler_1.DomHandler; } })); Object.defineProperty(exports, "DefaultHandler", ({ enumerable: true, get: function () { return domhandler_1.DomHandler; } })); // Helper methods @@ -41087,22 +41067,22 @@ function createDomStream(cb, options, elementCb) { return new Parser_1.Parser(handler, options); } exports.createDomStream = createDomStream; -var Tokenizer_1 = __webpack_require__(39); +var Tokenizer_1 = __webpack_require__(40); Object.defineProperty(exports, "Tokenizer", ({ enumerable: true, get: function () { return __importDefault(Tokenizer_1).default; } })); -var ElementType = __importStar(__webpack_require__(46)); +var ElementType = __importStar(__webpack_require__(47)); exports.ElementType = ElementType; /* * All of the following exports exist for backwards-compatibility. * They should probably be removed eventually. */ -__exportStar(__webpack_require__(48), exports); -exports.DomUtils = __importStar(__webpack_require__(49)); -var FeedHandler_1 = __webpack_require__(48); +__exportStar(__webpack_require__(49), exports); +exports.DomUtils = __importStar(__webpack_require__(50)); +var FeedHandler_1 = __webpack_require__(49); Object.defineProperty(exports, "RssHandler", ({ enumerable: true, get: function () { return FeedHandler_1.FeedHandler; } })); /***/ }), -/* 38 */ +/* 39 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41112,7 +41092,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Parser = void 0; -var Tokenizer_1 = __importDefault(__webpack_require__(39)); +var Tokenizer_1 = __importDefault(__webpack_require__(40)); var formTags = new Set([ "input", "option", @@ -41490,7 +41470,7 @@ exports.Parser = Parser; /***/ }), -/* 39 */ +/* 40 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41499,10 +41479,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -var decode_codepoint_1 = __importDefault(__webpack_require__(40)); -var entities_json_1 = __importDefault(__webpack_require__(42)); -var legacy_json_1 = __importDefault(__webpack_require__(43)); -var xml_json_1 = __importDefault(__webpack_require__(44)); +var decode_codepoint_1 = __importDefault(__webpack_require__(41)); +var entities_json_1 = __importDefault(__webpack_require__(43)); +var legacy_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); function whitespace(c) { return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r"; } @@ -42406,7 +42386,7 @@ exports["default"] = Tokenizer; /***/ }), -/* 40 */ +/* 41 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42415,7 +42395,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -var decode_json_1 = __importDefault(__webpack_require__(41)); +var decode_json_1 = __importDefault(__webpack_require__(42)); // Adapted from https://github.com/mathiasbynens/he/blob/master/src/he.js#L94-L119 var fromCodePoint = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -42443,35 +42423,35 @@ exports["default"] = decodeCodePoint; /***/ }), -/* 41 */ +/* 42 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"0":65533,"128":8364,"130":8218,"131":402,"132":8222,"133":8230,"134":8224,"135":8225,"136":710,"137":8240,"138":352,"139":8249,"140":338,"142":381,"145":8216,"146":8217,"147":8220,"148":8221,"149":8226,"150":8211,"151":8212,"152":732,"153":8482,"154":353,"155":8250,"156":339,"158":382,"159":376}'); /***/ }), -/* 42 */ +/* 43 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"Aacute":"Á","aacute":"á","Abreve":"Ă","abreve":"ă","ac":"∾","acd":"∿","acE":"∾̳","Acirc":"Â","acirc":"â","acute":"´","Acy":"А","acy":"а","AElig":"Æ","aelig":"æ","af":"⁡","Afr":"𝔄","afr":"𝔞","Agrave":"À","agrave":"à","alefsym":"ℵ","aleph":"ℵ","Alpha":"Α","alpha":"α","Amacr":"Ā","amacr":"ā","amalg":"⨿","amp":"&","AMP":"&","andand":"⩕","And":"⩓","and":"∧","andd":"⩜","andslope":"⩘","andv":"⩚","ang":"∠","ange":"⦤","angle":"∠","angmsdaa":"⦨","angmsdab":"⦩","angmsdac":"⦪","angmsdad":"⦫","angmsdae":"⦬","angmsdaf":"⦭","angmsdag":"⦮","angmsdah":"⦯","angmsd":"∡","angrt":"∟","angrtvb":"⊾","angrtvbd":"⦝","angsph":"∢","angst":"Å","angzarr":"⍼","Aogon":"Ą","aogon":"ą","Aopf":"𝔸","aopf":"𝕒","apacir":"⩯","ap":"≈","apE":"⩰","ape":"≊","apid":"≋","apos":"\'","ApplyFunction":"⁡","approx":"≈","approxeq":"≊","Aring":"Å","aring":"å","Ascr":"𝒜","ascr":"𝒶","Assign":"≔","ast":"*","asymp":"≈","asympeq":"≍","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","awconint":"∳","awint":"⨑","backcong":"≌","backepsilon":"϶","backprime":"‵","backsim":"∽","backsimeq":"⋍","Backslash":"∖","Barv":"⫧","barvee":"⊽","barwed":"⌅","Barwed":"⌆","barwedge":"⌅","bbrk":"⎵","bbrktbrk":"⎶","bcong":"≌","Bcy":"Б","bcy":"б","bdquo":"„","becaus":"∵","because":"∵","Because":"∵","bemptyv":"⦰","bepsi":"϶","bernou":"ℬ","Bernoullis":"ℬ","Beta":"Β","beta":"β","beth":"ℶ","between":"≬","Bfr":"𝔅","bfr":"𝔟","bigcap":"⋂","bigcirc":"◯","bigcup":"⋃","bigodot":"⨀","bigoplus":"⨁","bigotimes":"⨂","bigsqcup":"⨆","bigstar":"★","bigtriangledown":"▽","bigtriangleup":"△","biguplus":"⨄","bigvee":"⋁","bigwedge":"⋀","bkarow":"⤍","blacklozenge":"⧫","blacksquare":"▪","blacktriangle":"▴","blacktriangledown":"▾","blacktriangleleft":"◂","blacktriangleright":"▸","blank":"␣","blk12":"▒","blk14":"░","blk34":"▓","block":"█","bne":"=⃥","bnequiv":"≡⃥","bNot":"⫭","bnot":"⌐","Bopf":"𝔹","bopf":"𝕓","bot":"⊥","bottom":"⊥","bowtie":"⋈","boxbox":"⧉","boxdl":"┐","boxdL":"╕","boxDl":"╖","boxDL":"╗","boxdr":"┌","boxdR":"╒","boxDr":"╓","boxDR":"╔","boxh":"─","boxH":"═","boxhd":"┬","boxHd":"╤","boxhD":"╥","boxHD":"╦","boxhu":"┴","boxHu":"╧","boxhU":"╨","boxHU":"╩","boxminus":"⊟","boxplus":"⊞","boxtimes":"⊠","boxul":"┘","boxuL":"╛","boxUl":"╜","boxUL":"╝","boxur":"└","boxuR":"╘","boxUr":"╙","boxUR":"╚","boxv":"│","boxV":"║","boxvh":"┼","boxvH":"╪","boxVh":"╫","boxVH":"╬","boxvl":"┤","boxvL":"╡","boxVl":"╢","boxVL":"╣","boxvr":"├","boxvR":"╞","boxVr":"╟","boxVR":"╠","bprime":"‵","breve":"˘","Breve":"˘","brvbar":"¦","bscr":"𝒷","Bscr":"ℬ","bsemi":"⁏","bsim":"∽","bsime":"⋍","bsolb":"⧅","bsol":"\\\\","bsolhsub":"⟈","bull":"•","bullet":"•","bump":"≎","bumpE":"⪮","bumpe":"≏","Bumpeq":"≎","bumpeq":"≏","Cacute":"Ć","cacute":"ć","capand":"⩄","capbrcup":"⩉","capcap":"⩋","cap":"∩","Cap":"⋒","capcup":"⩇","capdot":"⩀","CapitalDifferentialD":"ⅅ","caps":"∩︀","caret":"⁁","caron":"ˇ","Cayleys":"ℭ","ccaps":"⩍","Ccaron":"Č","ccaron":"č","Ccedil":"Ç","ccedil":"ç","Ccirc":"Ĉ","ccirc":"ĉ","Cconint":"∰","ccups":"⩌","ccupssm":"⩐","Cdot":"Ċ","cdot":"ċ","cedil":"¸","Cedilla":"¸","cemptyv":"⦲","cent":"¢","centerdot":"·","CenterDot":"·","cfr":"𝔠","Cfr":"ℭ","CHcy":"Ч","chcy":"ч","check":"✓","checkmark":"✓","Chi":"Χ","chi":"χ","circ":"ˆ","circeq":"≗","circlearrowleft":"↺","circlearrowright":"↻","circledast":"⊛","circledcirc":"⊚","circleddash":"⊝","CircleDot":"⊙","circledR":"®","circledS":"Ⓢ","CircleMinus":"⊖","CirclePlus":"⊕","CircleTimes":"⊗","cir":"○","cirE":"⧃","cire":"≗","cirfnint":"⨐","cirmid":"⫯","cirscir":"⧂","ClockwiseContourIntegral":"∲","CloseCurlyDoubleQuote":"”","CloseCurlyQuote":"’","clubs":"♣","clubsuit":"♣","colon":":","Colon":"∷","Colone":"⩴","colone":"≔","coloneq":"≔","comma":",","commat":"@","comp":"∁","compfn":"∘","complement":"∁","complexes":"ℂ","cong":"≅","congdot":"⩭","Congruent":"≡","conint":"∮","Conint":"∯","ContourIntegral":"∮","copf":"𝕔","Copf":"ℂ","coprod":"∐","Coproduct":"∐","copy":"©","COPY":"©","copysr":"℗","CounterClockwiseContourIntegral":"∳","crarr":"↵","cross":"✗","Cross":"⨯","Cscr":"𝒞","cscr":"𝒸","csub":"⫏","csube":"⫑","csup":"⫐","csupe":"⫒","ctdot":"⋯","cudarrl":"⤸","cudarrr":"⤵","cuepr":"⋞","cuesc":"⋟","cularr":"↶","cularrp":"⤽","cupbrcap":"⩈","cupcap":"⩆","CupCap":"≍","cup":"∪","Cup":"⋓","cupcup":"⩊","cupdot":"⊍","cupor":"⩅","cups":"∪︀","curarr":"↷","curarrm":"⤼","curlyeqprec":"⋞","curlyeqsucc":"⋟","curlyvee":"⋎","curlywedge":"⋏","curren":"¤","curvearrowleft":"↶","curvearrowright":"↷","cuvee":"⋎","cuwed":"⋏","cwconint":"∲","cwint":"∱","cylcty":"⌭","dagger":"†","Dagger":"‡","daleth":"ℸ","darr":"↓","Darr":"↡","dArr":"⇓","dash":"‐","Dashv":"⫤","dashv":"⊣","dbkarow":"⤏","dblac":"˝","Dcaron":"Ď","dcaron":"ď","Dcy":"Д","dcy":"д","ddagger":"‡","ddarr":"⇊","DD":"ⅅ","dd":"ⅆ","DDotrahd":"⤑","ddotseq":"⩷","deg":"°","Del":"∇","Delta":"Δ","delta":"δ","demptyv":"⦱","dfisht":"⥿","Dfr":"𝔇","dfr":"𝔡","dHar":"⥥","dharl":"⇃","dharr":"⇂","DiacriticalAcute":"´","DiacriticalDot":"˙","DiacriticalDoubleAcute":"˝","DiacriticalGrave":"`","DiacriticalTilde":"˜","diam":"⋄","diamond":"⋄","Diamond":"⋄","diamondsuit":"♦","diams":"♦","die":"¨","DifferentialD":"ⅆ","digamma":"ϝ","disin":"⋲","div":"÷","divide":"÷","divideontimes":"⋇","divonx":"⋇","DJcy":"Ђ","djcy":"ђ","dlcorn":"⌞","dlcrop":"⌍","dollar":"$","Dopf":"𝔻","dopf":"𝕕","Dot":"¨","dot":"˙","DotDot":"⃜","doteq":"≐","doteqdot":"≑","DotEqual":"≐","dotminus":"∸","dotplus":"∔","dotsquare":"⊡","doublebarwedge":"⌆","DoubleContourIntegral":"∯","DoubleDot":"¨","DoubleDownArrow":"⇓","DoubleLeftArrow":"⇐","DoubleLeftRightArrow":"⇔","DoubleLeftTee":"⫤","DoubleLongLeftArrow":"⟸","DoubleLongLeftRightArrow":"⟺","DoubleLongRightArrow":"⟹","DoubleRightArrow":"⇒","DoubleRightTee":"⊨","DoubleUpArrow":"⇑","DoubleUpDownArrow":"⇕","DoubleVerticalBar":"∥","DownArrowBar":"⤓","downarrow":"↓","DownArrow":"↓","Downarrow":"⇓","DownArrowUpArrow":"⇵","DownBreve":"̑","downdownarrows":"⇊","downharpoonleft":"⇃","downharpoonright":"⇂","DownLeftRightVector":"⥐","DownLeftTeeVector":"⥞","DownLeftVectorBar":"⥖","DownLeftVector":"↽","DownRightTeeVector":"⥟","DownRightVectorBar":"⥗","DownRightVector":"⇁","DownTeeArrow":"↧","DownTee":"⊤","drbkarow":"⤐","drcorn":"⌟","drcrop":"⌌","Dscr":"𝒟","dscr":"𝒹","DScy":"Ѕ","dscy":"ѕ","dsol":"⧶","Dstrok":"Đ","dstrok":"đ","dtdot":"⋱","dtri":"▿","dtrif":"▾","duarr":"⇵","duhar":"⥯","dwangle":"⦦","DZcy":"Џ","dzcy":"џ","dzigrarr":"⟿","Eacute":"É","eacute":"é","easter":"⩮","Ecaron":"Ě","ecaron":"ě","Ecirc":"Ê","ecirc":"ê","ecir":"≖","ecolon":"≕","Ecy":"Э","ecy":"э","eDDot":"⩷","Edot":"Ė","edot":"ė","eDot":"≑","ee":"ⅇ","efDot":"≒","Efr":"𝔈","efr":"𝔢","eg":"⪚","Egrave":"È","egrave":"è","egs":"⪖","egsdot":"⪘","el":"⪙","Element":"∈","elinters":"⏧","ell":"ℓ","els":"⪕","elsdot":"⪗","Emacr":"Ē","emacr":"ē","empty":"∅","emptyset":"∅","EmptySmallSquare":"◻","emptyv":"∅","EmptyVerySmallSquare":"▫","emsp13":" ","emsp14":" ","emsp":" ","ENG":"Ŋ","eng":"ŋ","ensp":" ","Eogon":"Ę","eogon":"ę","Eopf":"𝔼","eopf":"𝕖","epar":"⋕","eparsl":"⧣","eplus":"⩱","epsi":"ε","Epsilon":"Ε","epsilon":"ε","epsiv":"ϵ","eqcirc":"≖","eqcolon":"≕","eqsim":"≂","eqslantgtr":"⪖","eqslantless":"⪕","Equal":"⩵","equals":"=","EqualTilde":"≂","equest":"≟","Equilibrium":"⇌","equiv":"≡","equivDD":"⩸","eqvparsl":"⧥","erarr":"⥱","erDot":"≓","escr":"ℯ","Escr":"ℰ","esdot":"≐","Esim":"⩳","esim":"≂","Eta":"Η","eta":"η","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","euro":"€","excl":"!","exist":"∃","Exists":"∃","expectation":"ℰ","exponentiale":"ⅇ","ExponentialE":"ⅇ","fallingdotseq":"≒","Fcy":"Ф","fcy":"ф","female":"♀","ffilig":"ffi","fflig":"ff","ffllig":"ffl","Ffr":"𝔉","ffr":"𝔣","filig":"fi","FilledSmallSquare":"◼","FilledVerySmallSquare":"▪","fjlig":"fj","flat":"♭","fllig":"fl","fltns":"▱","fnof":"ƒ","Fopf":"𝔽","fopf":"𝕗","forall":"∀","ForAll":"∀","fork":"⋔","forkv":"⫙","Fouriertrf":"ℱ","fpartint":"⨍","frac12":"½","frac13":"⅓","frac14":"¼","frac15":"⅕","frac16":"⅙","frac18":"⅛","frac23":"⅔","frac25":"⅖","frac34":"¾","frac35":"⅗","frac38":"⅜","frac45":"⅘","frac56":"⅚","frac58":"⅝","frac78":"⅞","frasl":"⁄","frown":"⌢","fscr":"𝒻","Fscr":"ℱ","gacute":"ǵ","Gamma":"Γ","gamma":"γ","Gammad":"Ϝ","gammad":"ϝ","gap":"⪆","Gbreve":"Ğ","gbreve":"ğ","Gcedil":"Ģ","Gcirc":"Ĝ","gcirc":"ĝ","Gcy":"Г","gcy":"г","Gdot":"Ġ","gdot":"ġ","ge":"≥","gE":"≧","gEl":"⪌","gel":"⋛","geq":"≥","geqq":"≧","geqslant":"⩾","gescc":"⪩","ges":"⩾","gesdot":"⪀","gesdoto":"⪂","gesdotol":"⪄","gesl":"⋛︀","gesles":"⪔","Gfr":"𝔊","gfr":"𝔤","gg":"≫","Gg":"⋙","ggg":"⋙","gimel":"ℷ","GJcy":"Ѓ","gjcy":"ѓ","gla":"⪥","gl":"≷","glE":"⪒","glj":"⪤","gnap":"⪊","gnapprox":"⪊","gne":"⪈","gnE":"≩","gneq":"⪈","gneqq":"≩","gnsim":"⋧","Gopf":"𝔾","gopf":"𝕘","grave":"`","GreaterEqual":"≥","GreaterEqualLess":"⋛","GreaterFullEqual":"≧","GreaterGreater":"⪢","GreaterLess":"≷","GreaterSlantEqual":"⩾","GreaterTilde":"≳","Gscr":"𝒢","gscr":"ℊ","gsim":"≳","gsime":"⪎","gsiml":"⪐","gtcc":"⪧","gtcir":"⩺","gt":">","GT":">","Gt":"≫","gtdot":"⋗","gtlPar":"⦕","gtquest":"⩼","gtrapprox":"⪆","gtrarr":"⥸","gtrdot":"⋗","gtreqless":"⋛","gtreqqless":"⪌","gtrless":"≷","gtrsim":"≳","gvertneqq":"≩︀","gvnE":"≩︀","Hacek":"ˇ","hairsp":" ","half":"½","hamilt":"ℋ","HARDcy":"Ъ","hardcy":"ъ","harrcir":"⥈","harr":"↔","hArr":"⇔","harrw":"↭","Hat":"^","hbar":"ℏ","Hcirc":"Ĥ","hcirc":"ĥ","hearts":"♥","heartsuit":"♥","hellip":"…","hercon":"⊹","hfr":"𝔥","Hfr":"ℌ","HilbertSpace":"ℋ","hksearow":"⤥","hkswarow":"⤦","hoarr":"⇿","homtht":"∻","hookleftarrow":"↩","hookrightarrow":"↪","hopf":"𝕙","Hopf":"ℍ","horbar":"―","HorizontalLine":"─","hscr":"𝒽","Hscr":"ℋ","hslash":"ℏ","Hstrok":"Ħ","hstrok":"ħ","HumpDownHump":"≎","HumpEqual":"≏","hybull":"⁃","hyphen":"‐","Iacute":"Í","iacute":"í","ic":"⁣","Icirc":"Î","icirc":"î","Icy":"И","icy":"и","Idot":"İ","IEcy":"Е","iecy":"е","iexcl":"¡","iff":"⇔","ifr":"𝔦","Ifr":"ℑ","Igrave":"Ì","igrave":"ì","ii":"ⅈ","iiiint":"⨌","iiint":"∭","iinfin":"⧜","iiota":"℩","IJlig":"IJ","ijlig":"ij","Imacr":"Ī","imacr":"ī","image":"ℑ","ImaginaryI":"ⅈ","imagline":"ℐ","imagpart":"ℑ","imath":"ı","Im":"ℑ","imof":"⊷","imped":"Ƶ","Implies":"⇒","incare":"℅","in":"∈","infin":"∞","infintie":"⧝","inodot":"ı","intcal":"⊺","int":"∫","Int":"∬","integers":"ℤ","Integral":"∫","intercal":"⊺","Intersection":"⋂","intlarhk":"⨗","intprod":"⨼","InvisibleComma":"⁣","InvisibleTimes":"⁢","IOcy":"Ё","iocy":"ё","Iogon":"Į","iogon":"į","Iopf":"𝕀","iopf":"𝕚","Iota":"Ι","iota":"ι","iprod":"⨼","iquest":"¿","iscr":"𝒾","Iscr":"ℐ","isin":"∈","isindot":"⋵","isinE":"⋹","isins":"⋴","isinsv":"⋳","isinv":"∈","it":"⁢","Itilde":"Ĩ","itilde":"ĩ","Iukcy":"І","iukcy":"і","Iuml":"Ï","iuml":"ï","Jcirc":"Ĵ","jcirc":"ĵ","Jcy":"Й","jcy":"й","Jfr":"𝔍","jfr":"𝔧","jmath":"ȷ","Jopf":"𝕁","jopf":"𝕛","Jscr":"𝒥","jscr":"𝒿","Jsercy":"Ј","jsercy":"ј","Jukcy":"Є","jukcy":"є","Kappa":"Κ","kappa":"κ","kappav":"ϰ","Kcedil":"Ķ","kcedil":"ķ","Kcy":"К","kcy":"к","Kfr":"𝔎","kfr":"𝔨","kgreen":"ĸ","KHcy":"Х","khcy":"х","KJcy":"Ќ","kjcy":"ќ","Kopf":"𝕂","kopf":"𝕜","Kscr":"𝒦","kscr":"𝓀","lAarr":"⇚","Lacute":"Ĺ","lacute":"ĺ","laemptyv":"⦴","lagran":"ℒ","Lambda":"Λ","lambda":"λ","lang":"⟨","Lang":"⟪","langd":"⦑","langle":"⟨","lap":"⪅","Laplacetrf":"ℒ","laquo":"«","larrb":"⇤","larrbfs":"⤟","larr":"←","Larr":"↞","lArr":"⇐","larrfs":"⤝","larrhk":"↩","larrlp":"↫","larrpl":"⤹","larrsim":"⥳","larrtl":"↢","latail":"⤙","lAtail":"⤛","lat":"⪫","late":"⪭","lates":"⪭︀","lbarr":"⤌","lBarr":"⤎","lbbrk":"❲","lbrace":"{","lbrack":"[","lbrke":"⦋","lbrksld":"⦏","lbrkslu":"⦍","Lcaron":"Ľ","lcaron":"ľ","Lcedil":"Ļ","lcedil":"ļ","lceil":"⌈","lcub":"{","Lcy":"Л","lcy":"л","ldca":"⤶","ldquo":"“","ldquor":"„","ldrdhar":"⥧","ldrushar":"⥋","ldsh":"↲","le":"≤","lE":"≦","LeftAngleBracket":"⟨","LeftArrowBar":"⇤","leftarrow":"←","LeftArrow":"←","Leftarrow":"⇐","LeftArrowRightArrow":"⇆","leftarrowtail":"↢","LeftCeiling":"⌈","LeftDoubleBracket":"⟦","LeftDownTeeVector":"⥡","LeftDownVectorBar":"⥙","LeftDownVector":"⇃","LeftFloor":"⌊","leftharpoondown":"↽","leftharpoonup":"↼","leftleftarrows":"⇇","leftrightarrow":"↔","LeftRightArrow":"↔","Leftrightarrow":"⇔","leftrightarrows":"⇆","leftrightharpoons":"⇋","leftrightsquigarrow":"↭","LeftRightVector":"⥎","LeftTeeArrow":"↤","LeftTee":"⊣","LeftTeeVector":"⥚","leftthreetimes":"⋋","LeftTriangleBar":"⧏","LeftTriangle":"⊲","LeftTriangleEqual":"⊴","LeftUpDownVector":"⥑","LeftUpTeeVector":"⥠","LeftUpVectorBar":"⥘","LeftUpVector":"↿","LeftVectorBar":"⥒","LeftVector":"↼","lEg":"⪋","leg":"⋚","leq":"≤","leqq":"≦","leqslant":"⩽","lescc":"⪨","les":"⩽","lesdot":"⩿","lesdoto":"⪁","lesdotor":"⪃","lesg":"⋚︀","lesges":"⪓","lessapprox":"⪅","lessdot":"⋖","lesseqgtr":"⋚","lesseqqgtr":"⪋","LessEqualGreater":"⋚","LessFullEqual":"≦","LessGreater":"≶","lessgtr":"≶","LessLess":"⪡","lesssim":"≲","LessSlantEqual":"⩽","LessTilde":"≲","lfisht":"⥼","lfloor":"⌊","Lfr":"𝔏","lfr":"𝔩","lg":"≶","lgE":"⪑","lHar":"⥢","lhard":"↽","lharu":"↼","lharul":"⥪","lhblk":"▄","LJcy":"Љ","ljcy":"љ","llarr":"⇇","ll":"≪","Ll":"⋘","llcorner":"⌞","Lleftarrow":"⇚","llhard":"⥫","lltri":"◺","Lmidot":"Ŀ","lmidot":"ŀ","lmoustache":"⎰","lmoust":"⎰","lnap":"⪉","lnapprox":"⪉","lne":"⪇","lnE":"≨","lneq":"⪇","lneqq":"≨","lnsim":"⋦","loang":"⟬","loarr":"⇽","lobrk":"⟦","longleftarrow":"⟵","LongLeftArrow":"⟵","Longleftarrow":"⟸","longleftrightarrow":"⟷","LongLeftRightArrow":"⟷","Longleftrightarrow":"⟺","longmapsto":"⟼","longrightarrow":"⟶","LongRightArrow":"⟶","Longrightarrow":"⟹","looparrowleft":"↫","looparrowright":"↬","lopar":"⦅","Lopf":"𝕃","lopf":"𝕝","loplus":"⨭","lotimes":"⨴","lowast":"∗","lowbar":"_","LowerLeftArrow":"↙","LowerRightArrow":"↘","loz":"◊","lozenge":"◊","lozf":"⧫","lpar":"(","lparlt":"⦓","lrarr":"⇆","lrcorner":"⌟","lrhar":"⇋","lrhard":"⥭","lrm":"‎","lrtri":"⊿","lsaquo":"‹","lscr":"𝓁","Lscr":"ℒ","lsh":"↰","Lsh":"↰","lsim":"≲","lsime":"⪍","lsimg":"⪏","lsqb":"[","lsquo":"‘","lsquor":"‚","Lstrok":"Ł","lstrok":"ł","ltcc":"⪦","ltcir":"⩹","lt":"<","LT":"<","Lt":"≪","ltdot":"⋖","lthree":"⋋","ltimes":"⋉","ltlarr":"⥶","ltquest":"⩻","ltri":"◃","ltrie":"⊴","ltrif":"◂","ltrPar":"⦖","lurdshar":"⥊","luruhar":"⥦","lvertneqq":"≨︀","lvnE":"≨︀","macr":"¯","male":"♂","malt":"✠","maltese":"✠","Map":"⤅","map":"↦","mapsto":"↦","mapstodown":"↧","mapstoleft":"↤","mapstoup":"↥","marker":"▮","mcomma":"⨩","Mcy":"М","mcy":"м","mdash":"—","mDDot":"∺","measuredangle":"∡","MediumSpace":" ","Mellintrf":"ℳ","Mfr":"𝔐","mfr":"𝔪","mho":"℧","micro":"µ","midast":"*","midcir":"⫰","mid":"∣","middot":"·","minusb":"⊟","minus":"−","minusd":"∸","minusdu":"⨪","MinusPlus":"∓","mlcp":"⫛","mldr":"…","mnplus":"∓","models":"⊧","Mopf":"𝕄","mopf":"𝕞","mp":"∓","mscr":"𝓂","Mscr":"ℳ","mstpos":"∾","Mu":"Μ","mu":"μ","multimap":"⊸","mumap":"⊸","nabla":"∇","Nacute":"Ń","nacute":"ń","nang":"∠⃒","nap":"≉","napE":"⩰̸","napid":"≋̸","napos":"ʼn","napprox":"≉","natural":"♮","naturals":"ℕ","natur":"♮","nbsp":" ","nbump":"≎̸","nbumpe":"≏̸","ncap":"⩃","Ncaron":"Ň","ncaron":"ň","Ncedil":"Ņ","ncedil":"ņ","ncong":"≇","ncongdot":"⩭̸","ncup":"⩂","Ncy":"Н","ncy":"н","ndash":"–","nearhk":"⤤","nearr":"↗","neArr":"⇗","nearrow":"↗","ne":"≠","nedot":"≐̸","NegativeMediumSpace":"​","NegativeThickSpace":"​","NegativeThinSpace":"​","NegativeVeryThinSpace":"​","nequiv":"≢","nesear":"⤨","nesim":"≂̸","NestedGreaterGreater":"≫","NestedLessLess":"≪","NewLine":"\\n","nexist":"∄","nexists":"∄","Nfr":"𝔑","nfr":"𝔫","ngE":"≧̸","nge":"≱","ngeq":"≱","ngeqq":"≧̸","ngeqslant":"⩾̸","nges":"⩾̸","nGg":"⋙̸","ngsim":"≵","nGt":"≫⃒","ngt":"≯","ngtr":"≯","nGtv":"≫̸","nharr":"↮","nhArr":"⇎","nhpar":"⫲","ni":"∋","nis":"⋼","nisd":"⋺","niv":"∋","NJcy":"Њ","njcy":"њ","nlarr":"↚","nlArr":"⇍","nldr":"‥","nlE":"≦̸","nle":"≰","nleftarrow":"↚","nLeftarrow":"⇍","nleftrightarrow":"↮","nLeftrightarrow":"⇎","nleq":"≰","nleqq":"≦̸","nleqslant":"⩽̸","nles":"⩽̸","nless":"≮","nLl":"⋘̸","nlsim":"≴","nLt":"≪⃒","nlt":"≮","nltri":"⋪","nltrie":"⋬","nLtv":"≪̸","nmid":"∤","NoBreak":"⁠","NonBreakingSpace":" ","nopf":"𝕟","Nopf":"ℕ","Not":"⫬","not":"¬","NotCongruent":"≢","NotCupCap":"≭","NotDoubleVerticalBar":"∦","NotElement":"∉","NotEqual":"≠","NotEqualTilde":"≂̸","NotExists":"∄","NotGreater":"≯","NotGreaterEqual":"≱","NotGreaterFullEqual":"≧̸","NotGreaterGreater":"≫̸","NotGreaterLess":"≹","NotGreaterSlantEqual":"⩾̸","NotGreaterTilde":"≵","NotHumpDownHump":"≎̸","NotHumpEqual":"≏̸","notin":"∉","notindot":"⋵̸","notinE":"⋹̸","notinva":"∉","notinvb":"⋷","notinvc":"⋶","NotLeftTriangleBar":"⧏̸","NotLeftTriangle":"⋪","NotLeftTriangleEqual":"⋬","NotLess":"≮","NotLessEqual":"≰","NotLessGreater":"≸","NotLessLess":"≪̸","NotLessSlantEqual":"⩽̸","NotLessTilde":"≴","NotNestedGreaterGreater":"⪢̸","NotNestedLessLess":"⪡̸","notni":"∌","notniva":"∌","notnivb":"⋾","notnivc":"⋽","NotPrecedes":"⊀","NotPrecedesEqual":"⪯̸","NotPrecedesSlantEqual":"⋠","NotReverseElement":"∌","NotRightTriangleBar":"⧐̸","NotRightTriangle":"⋫","NotRightTriangleEqual":"⋭","NotSquareSubset":"⊏̸","NotSquareSubsetEqual":"⋢","NotSquareSuperset":"⊐̸","NotSquareSupersetEqual":"⋣","NotSubset":"⊂⃒","NotSubsetEqual":"⊈","NotSucceeds":"⊁","NotSucceedsEqual":"⪰̸","NotSucceedsSlantEqual":"⋡","NotSucceedsTilde":"≿̸","NotSuperset":"⊃⃒","NotSupersetEqual":"⊉","NotTilde":"≁","NotTildeEqual":"≄","NotTildeFullEqual":"≇","NotTildeTilde":"≉","NotVerticalBar":"∤","nparallel":"∦","npar":"∦","nparsl":"⫽⃥","npart":"∂̸","npolint":"⨔","npr":"⊀","nprcue":"⋠","nprec":"⊀","npreceq":"⪯̸","npre":"⪯̸","nrarrc":"⤳̸","nrarr":"↛","nrArr":"⇏","nrarrw":"↝̸","nrightarrow":"↛","nRightarrow":"⇏","nrtri":"⋫","nrtrie":"⋭","nsc":"⊁","nsccue":"⋡","nsce":"⪰̸","Nscr":"𝒩","nscr":"𝓃","nshortmid":"∤","nshortparallel":"∦","nsim":"≁","nsime":"≄","nsimeq":"≄","nsmid":"∤","nspar":"∦","nsqsube":"⋢","nsqsupe":"⋣","nsub":"⊄","nsubE":"⫅̸","nsube":"⊈","nsubset":"⊂⃒","nsubseteq":"⊈","nsubseteqq":"⫅̸","nsucc":"⊁","nsucceq":"⪰̸","nsup":"⊅","nsupE":"⫆̸","nsupe":"⊉","nsupset":"⊃⃒","nsupseteq":"⊉","nsupseteqq":"⫆̸","ntgl":"≹","Ntilde":"Ñ","ntilde":"ñ","ntlg":"≸","ntriangleleft":"⋪","ntrianglelefteq":"⋬","ntriangleright":"⋫","ntrianglerighteq":"⋭","Nu":"Ν","nu":"ν","num":"#","numero":"№","numsp":" ","nvap":"≍⃒","nvdash":"⊬","nvDash":"⊭","nVdash":"⊮","nVDash":"⊯","nvge":"≥⃒","nvgt":">⃒","nvHarr":"⤄","nvinfin":"⧞","nvlArr":"⤂","nvle":"≤⃒","nvlt":"<⃒","nvltrie":"⊴⃒","nvrArr":"⤃","nvrtrie":"⊵⃒","nvsim":"∼⃒","nwarhk":"⤣","nwarr":"↖","nwArr":"⇖","nwarrow":"↖","nwnear":"⤧","Oacute":"Ó","oacute":"ó","oast":"⊛","Ocirc":"Ô","ocirc":"ô","ocir":"⊚","Ocy":"О","ocy":"о","odash":"⊝","Odblac":"Ő","odblac":"ő","odiv":"⨸","odot":"⊙","odsold":"⦼","OElig":"Œ","oelig":"œ","ofcir":"⦿","Ofr":"𝔒","ofr":"𝔬","ogon":"˛","Ograve":"Ò","ograve":"ò","ogt":"⧁","ohbar":"⦵","ohm":"Ω","oint":"∮","olarr":"↺","olcir":"⦾","olcross":"⦻","oline":"‾","olt":"⧀","Omacr":"Ō","omacr":"ō","Omega":"Ω","omega":"ω","Omicron":"Ο","omicron":"ο","omid":"⦶","ominus":"⊖","Oopf":"𝕆","oopf":"𝕠","opar":"⦷","OpenCurlyDoubleQuote":"“","OpenCurlyQuote":"‘","operp":"⦹","oplus":"⊕","orarr":"↻","Or":"⩔","or":"∨","ord":"⩝","order":"ℴ","orderof":"ℴ","ordf":"ª","ordm":"º","origof":"⊶","oror":"⩖","orslope":"⩗","orv":"⩛","oS":"Ⓢ","Oscr":"𝒪","oscr":"ℴ","Oslash":"Ø","oslash":"ø","osol":"⊘","Otilde":"Õ","otilde":"õ","otimesas":"⨶","Otimes":"⨷","otimes":"⊗","Ouml":"Ö","ouml":"ö","ovbar":"⌽","OverBar":"‾","OverBrace":"⏞","OverBracket":"⎴","OverParenthesis":"⏜","para":"¶","parallel":"∥","par":"∥","parsim":"⫳","parsl":"⫽","part":"∂","PartialD":"∂","Pcy":"П","pcy":"п","percnt":"%","period":".","permil":"‰","perp":"⊥","pertenk":"‱","Pfr":"𝔓","pfr":"𝔭","Phi":"Φ","phi":"φ","phiv":"ϕ","phmmat":"ℳ","phone":"☎","Pi":"Π","pi":"π","pitchfork":"⋔","piv":"ϖ","planck":"ℏ","planckh":"ℎ","plankv":"ℏ","plusacir":"⨣","plusb":"⊞","pluscir":"⨢","plus":"+","plusdo":"∔","plusdu":"⨥","pluse":"⩲","PlusMinus":"±","plusmn":"±","plussim":"⨦","plustwo":"⨧","pm":"±","Poincareplane":"ℌ","pointint":"⨕","popf":"𝕡","Popf":"ℙ","pound":"£","prap":"⪷","Pr":"⪻","pr":"≺","prcue":"≼","precapprox":"⪷","prec":"≺","preccurlyeq":"≼","Precedes":"≺","PrecedesEqual":"⪯","PrecedesSlantEqual":"≼","PrecedesTilde":"≾","preceq":"⪯","precnapprox":"⪹","precneqq":"⪵","precnsim":"⋨","pre":"⪯","prE":"⪳","precsim":"≾","prime":"′","Prime":"″","primes":"ℙ","prnap":"⪹","prnE":"⪵","prnsim":"⋨","prod":"∏","Product":"∏","profalar":"⌮","profline":"⌒","profsurf":"⌓","prop":"∝","Proportional":"∝","Proportion":"∷","propto":"∝","prsim":"≾","prurel":"⊰","Pscr":"𝒫","pscr":"𝓅","Psi":"Ψ","psi":"ψ","puncsp":" ","Qfr":"𝔔","qfr":"𝔮","qint":"⨌","qopf":"𝕢","Qopf":"ℚ","qprime":"⁗","Qscr":"𝒬","qscr":"𝓆","quaternions":"ℍ","quatint":"⨖","quest":"?","questeq":"≟","quot":"\\"","QUOT":"\\"","rAarr":"⇛","race":"∽̱","Racute":"Ŕ","racute":"ŕ","radic":"√","raemptyv":"⦳","rang":"⟩","Rang":"⟫","rangd":"⦒","range":"⦥","rangle":"⟩","raquo":"»","rarrap":"⥵","rarrb":"⇥","rarrbfs":"⤠","rarrc":"⤳","rarr":"→","Rarr":"↠","rArr":"⇒","rarrfs":"⤞","rarrhk":"↪","rarrlp":"↬","rarrpl":"⥅","rarrsim":"⥴","Rarrtl":"⤖","rarrtl":"↣","rarrw":"↝","ratail":"⤚","rAtail":"⤜","ratio":"∶","rationals":"ℚ","rbarr":"⤍","rBarr":"⤏","RBarr":"⤐","rbbrk":"❳","rbrace":"}","rbrack":"]","rbrke":"⦌","rbrksld":"⦎","rbrkslu":"⦐","Rcaron":"Ř","rcaron":"ř","Rcedil":"Ŗ","rcedil":"ŗ","rceil":"⌉","rcub":"}","Rcy":"Р","rcy":"р","rdca":"⤷","rdldhar":"⥩","rdquo":"”","rdquor":"”","rdsh":"↳","real":"ℜ","realine":"ℛ","realpart":"ℜ","reals":"ℝ","Re":"ℜ","rect":"▭","reg":"®","REG":"®","ReverseElement":"∋","ReverseEquilibrium":"⇋","ReverseUpEquilibrium":"⥯","rfisht":"⥽","rfloor":"⌋","rfr":"𝔯","Rfr":"ℜ","rHar":"⥤","rhard":"⇁","rharu":"⇀","rharul":"⥬","Rho":"Ρ","rho":"ρ","rhov":"ϱ","RightAngleBracket":"⟩","RightArrowBar":"⇥","rightarrow":"→","RightArrow":"→","Rightarrow":"⇒","RightArrowLeftArrow":"⇄","rightarrowtail":"↣","RightCeiling":"⌉","RightDoubleBracket":"⟧","RightDownTeeVector":"⥝","RightDownVectorBar":"⥕","RightDownVector":"⇂","RightFloor":"⌋","rightharpoondown":"⇁","rightharpoonup":"⇀","rightleftarrows":"⇄","rightleftharpoons":"⇌","rightrightarrows":"⇉","rightsquigarrow":"↝","RightTeeArrow":"↦","RightTee":"⊢","RightTeeVector":"⥛","rightthreetimes":"⋌","RightTriangleBar":"⧐","RightTriangle":"⊳","RightTriangleEqual":"⊵","RightUpDownVector":"⥏","RightUpTeeVector":"⥜","RightUpVectorBar":"⥔","RightUpVector":"↾","RightVectorBar":"⥓","RightVector":"⇀","ring":"˚","risingdotseq":"≓","rlarr":"⇄","rlhar":"⇌","rlm":"‏","rmoustache":"⎱","rmoust":"⎱","rnmid":"⫮","roang":"⟭","roarr":"⇾","robrk":"⟧","ropar":"⦆","ropf":"𝕣","Ropf":"ℝ","roplus":"⨮","rotimes":"⨵","RoundImplies":"⥰","rpar":")","rpargt":"⦔","rppolint":"⨒","rrarr":"⇉","Rrightarrow":"⇛","rsaquo":"›","rscr":"𝓇","Rscr":"ℛ","rsh":"↱","Rsh":"↱","rsqb":"]","rsquo":"’","rsquor":"’","rthree":"⋌","rtimes":"⋊","rtri":"▹","rtrie":"⊵","rtrif":"▸","rtriltri":"⧎","RuleDelayed":"⧴","ruluhar":"⥨","rx":"℞","Sacute":"Ś","sacute":"ś","sbquo":"‚","scap":"⪸","Scaron":"Š","scaron":"š","Sc":"⪼","sc":"≻","sccue":"≽","sce":"⪰","scE":"⪴","Scedil":"Ş","scedil":"ş","Scirc":"Ŝ","scirc":"ŝ","scnap":"⪺","scnE":"⪶","scnsim":"⋩","scpolint":"⨓","scsim":"≿","Scy":"С","scy":"с","sdotb":"⊡","sdot":"⋅","sdote":"⩦","searhk":"⤥","searr":"↘","seArr":"⇘","searrow":"↘","sect":"§","semi":";","seswar":"⤩","setminus":"∖","setmn":"∖","sext":"✶","Sfr":"𝔖","sfr":"𝔰","sfrown":"⌢","sharp":"♯","SHCHcy":"Щ","shchcy":"щ","SHcy":"Ш","shcy":"ш","ShortDownArrow":"↓","ShortLeftArrow":"←","shortmid":"∣","shortparallel":"∥","ShortRightArrow":"→","ShortUpArrow":"↑","shy":"­","Sigma":"Σ","sigma":"σ","sigmaf":"ς","sigmav":"ς","sim":"∼","simdot":"⩪","sime":"≃","simeq":"≃","simg":"⪞","simgE":"⪠","siml":"⪝","simlE":"⪟","simne":"≆","simplus":"⨤","simrarr":"⥲","slarr":"←","SmallCircle":"∘","smallsetminus":"∖","smashp":"⨳","smeparsl":"⧤","smid":"∣","smile":"⌣","smt":"⪪","smte":"⪬","smtes":"⪬︀","SOFTcy":"Ь","softcy":"ь","solbar":"⌿","solb":"⧄","sol":"/","Sopf":"𝕊","sopf":"𝕤","spades":"♠","spadesuit":"♠","spar":"∥","sqcap":"⊓","sqcaps":"⊓︀","sqcup":"⊔","sqcups":"⊔︀","Sqrt":"√","sqsub":"⊏","sqsube":"⊑","sqsubset":"⊏","sqsubseteq":"⊑","sqsup":"⊐","sqsupe":"⊒","sqsupset":"⊐","sqsupseteq":"⊒","square":"□","Square":"□","SquareIntersection":"⊓","SquareSubset":"⊏","SquareSubsetEqual":"⊑","SquareSuperset":"⊐","SquareSupersetEqual":"⊒","SquareUnion":"⊔","squarf":"▪","squ":"□","squf":"▪","srarr":"→","Sscr":"𝒮","sscr":"𝓈","ssetmn":"∖","ssmile":"⌣","sstarf":"⋆","Star":"⋆","star":"☆","starf":"★","straightepsilon":"ϵ","straightphi":"ϕ","strns":"¯","sub":"⊂","Sub":"⋐","subdot":"⪽","subE":"⫅","sube":"⊆","subedot":"⫃","submult":"⫁","subnE":"⫋","subne":"⊊","subplus":"⪿","subrarr":"⥹","subset":"⊂","Subset":"⋐","subseteq":"⊆","subseteqq":"⫅","SubsetEqual":"⊆","subsetneq":"⊊","subsetneqq":"⫋","subsim":"⫇","subsub":"⫕","subsup":"⫓","succapprox":"⪸","succ":"≻","succcurlyeq":"≽","Succeeds":"≻","SucceedsEqual":"⪰","SucceedsSlantEqual":"≽","SucceedsTilde":"≿","succeq":"⪰","succnapprox":"⪺","succneqq":"⪶","succnsim":"⋩","succsim":"≿","SuchThat":"∋","sum":"∑","Sum":"∑","sung":"♪","sup1":"¹","sup2":"²","sup3":"³","sup":"⊃","Sup":"⋑","supdot":"⪾","supdsub":"⫘","supE":"⫆","supe":"⊇","supedot":"⫄","Superset":"⊃","SupersetEqual":"⊇","suphsol":"⟉","suphsub":"⫗","suplarr":"⥻","supmult":"⫂","supnE":"⫌","supne":"⊋","supplus":"⫀","supset":"⊃","Supset":"⋑","supseteq":"⊇","supseteqq":"⫆","supsetneq":"⊋","supsetneqq":"⫌","supsim":"⫈","supsub":"⫔","supsup":"⫖","swarhk":"⤦","swarr":"↙","swArr":"⇙","swarrow":"↙","swnwar":"⤪","szlig":"ß","Tab":"\\t","target":"⌖","Tau":"Τ","tau":"τ","tbrk":"⎴","Tcaron":"Ť","tcaron":"ť","Tcedil":"Ţ","tcedil":"ţ","Tcy":"Т","tcy":"т","tdot":"⃛","telrec":"⌕","Tfr":"𝔗","tfr":"𝔱","there4":"∴","therefore":"∴","Therefore":"∴","Theta":"Θ","theta":"θ","thetasym":"ϑ","thetav":"ϑ","thickapprox":"≈","thicksim":"∼","ThickSpace":"  ","ThinSpace":" ","thinsp":" ","thkap":"≈","thksim":"∼","THORN":"Þ","thorn":"þ","tilde":"˜","Tilde":"∼","TildeEqual":"≃","TildeFullEqual":"≅","TildeTilde":"≈","timesbar":"⨱","timesb":"⊠","times":"×","timesd":"⨰","tint":"∭","toea":"⤨","topbot":"⌶","topcir":"⫱","top":"⊤","Topf":"𝕋","topf":"𝕥","topfork":"⫚","tosa":"⤩","tprime":"‴","trade":"™","TRADE":"™","triangle":"▵","triangledown":"▿","triangleleft":"◃","trianglelefteq":"⊴","triangleq":"≜","triangleright":"▹","trianglerighteq":"⊵","tridot":"◬","trie":"≜","triminus":"⨺","TripleDot":"⃛","triplus":"⨹","trisb":"⧍","tritime":"⨻","trpezium":"⏢","Tscr":"𝒯","tscr":"𝓉","TScy":"Ц","tscy":"ц","TSHcy":"Ћ","tshcy":"ћ","Tstrok":"Ŧ","tstrok":"ŧ","twixt":"≬","twoheadleftarrow":"↞","twoheadrightarrow":"↠","Uacute":"Ú","uacute":"ú","uarr":"↑","Uarr":"↟","uArr":"⇑","Uarrocir":"⥉","Ubrcy":"Ў","ubrcy":"ў","Ubreve":"Ŭ","ubreve":"ŭ","Ucirc":"Û","ucirc":"û","Ucy":"У","ucy":"у","udarr":"⇅","Udblac":"Ű","udblac":"ű","udhar":"⥮","ufisht":"⥾","Ufr":"𝔘","ufr":"𝔲","Ugrave":"Ù","ugrave":"ù","uHar":"⥣","uharl":"↿","uharr":"↾","uhblk":"▀","ulcorn":"⌜","ulcorner":"⌜","ulcrop":"⌏","ultri":"◸","Umacr":"Ū","umacr":"ū","uml":"¨","UnderBar":"_","UnderBrace":"⏟","UnderBracket":"⎵","UnderParenthesis":"⏝","Union":"⋃","UnionPlus":"⊎","Uogon":"Ų","uogon":"ų","Uopf":"𝕌","uopf":"𝕦","UpArrowBar":"⤒","uparrow":"↑","UpArrow":"↑","Uparrow":"⇑","UpArrowDownArrow":"⇅","updownarrow":"↕","UpDownArrow":"↕","Updownarrow":"⇕","UpEquilibrium":"⥮","upharpoonleft":"↿","upharpoonright":"↾","uplus":"⊎","UpperLeftArrow":"↖","UpperRightArrow":"↗","upsi":"υ","Upsi":"ϒ","upsih":"ϒ","Upsilon":"Υ","upsilon":"υ","UpTeeArrow":"↥","UpTee":"⊥","upuparrows":"⇈","urcorn":"⌝","urcorner":"⌝","urcrop":"⌎","Uring":"Ů","uring":"ů","urtri":"◹","Uscr":"𝒰","uscr":"𝓊","utdot":"⋰","Utilde":"Ũ","utilde":"ũ","utri":"▵","utrif":"▴","uuarr":"⇈","Uuml":"Ü","uuml":"ü","uwangle":"⦧","vangrt":"⦜","varepsilon":"ϵ","varkappa":"ϰ","varnothing":"∅","varphi":"ϕ","varpi":"ϖ","varpropto":"∝","varr":"↕","vArr":"⇕","varrho":"ϱ","varsigma":"ς","varsubsetneq":"⊊︀","varsubsetneqq":"⫋︀","varsupsetneq":"⊋︀","varsupsetneqq":"⫌︀","vartheta":"ϑ","vartriangleleft":"⊲","vartriangleright":"⊳","vBar":"⫨","Vbar":"⫫","vBarv":"⫩","Vcy":"В","vcy":"в","vdash":"⊢","vDash":"⊨","Vdash":"⊩","VDash":"⊫","Vdashl":"⫦","veebar":"⊻","vee":"∨","Vee":"⋁","veeeq":"≚","vellip":"⋮","verbar":"|","Verbar":"‖","vert":"|","Vert":"‖","VerticalBar":"∣","VerticalLine":"|","VerticalSeparator":"❘","VerticalTilde":"≀","VeryThinSpace":" ","Vfr":"𝔙","vfr":"𝔳","vltri":"⊲","vnsub":"⊂⃒","vnsup":"⊃⃒","Vopf":"𝕍","vopf":"𝕧","vprop":"∝","vrtri":"⊳","Vscr":"𝒱","vscr":"𝓋","vsubnE":"⫋︀","vsubne":"⊊︀","vsupnE":"⫌︀","vsupne":"⊋︀","Vvdash":"⊪","vzigzag":"⦚","Wcirc":"Ŵ","wcirc":"ŵ","wedbar":"⩟","wedge":"∧","Wedge":"⋀","wedgeq":"≙","weierp":"℘","Wfr":"𝔚","wfr":"𝔴","Wopf":"𝕎","wopf":"𝕨","wp":"℘","wr":"≀","wreath":"≀","Wscr":"𝒲","wscr":"𝓌","xcap":"⋂","xcirc":"◯","xcup":"⋃","xdtri":"▽","Xfr":"𝔛","xfr":"𝔵","xharr":"⟷","xhArr":"⟺","Xi":"Ξ","xi":"ξ","xlarr":"⟵","xlArr":"⟸","xmap":"⟼","xnis":"⋻","xodot":"⨀","Xopf":"𝕏","xopf":"𝕩","xoplus":"⨁","xotime":"⨂","xrarr":"⟶","xrArr":"⟹","Xscr":"𝒳","xscr":"𝓍","xsqcup":"⨆","xuplus":"⨄","xutri":"△","xvee":"⋁","xwedge":"⋀","Yacute":"Ý","yacute":"ý","YAcy":"Я","yacy":"я","Ycirc":"Ŷ","ycirc":"ŷ","Ycy":"Ы","ycy":"ы","yen":"¥","Yfr":"𝔜","yfr":"𝔶","YIcy":"Ї","yicy":"ї","Yopf":"𝕐","yopf":"𝕪","Yscr":"𝒴","yscr":"𝓎","YUcy":"Ю","yucy":"ю","yuml":"ÿ","Yuml":"Ÿ","Zacute":"Ź","zacute":"ź","Zcaron":"Ž","zcaron":"ž","Zcy":"З","zcy":"з","Zdot":"Ż","zdot":"ż","zeetrf":"ℨ","ZeroWidthSpace":"​","Zeta":"Ζ","zeta":"ζ","zfr":"𝔷","Zfr":"ℨ","ZHcy":"Ж","zhcy":"ж","zigrarr":"⇝","zopf":"𝕫","Zopf":"ℤ","Zscr":"𝒵","zscr":"𝓏","zwj":"‍","zwnj":"‌"}'); /***/ }), -/* 43 */ +/* 44 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"Aacute":"Á","aacute":"á","Acirc":"Â","acirc":"â","acute":"´","AElig":"Æ","aelig":"æ","Agrave":"À","agrave":"à","amp":"&","AMP":"&","Aring":"Å","aring":"å","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","brvbar":"¦","Ccedil":"Ç","ccedil":"ç","cedil":"¸","cent":"¢","copy":"©","COPY":"©","curren":"¤","deg":"°","divide":"÷","Eacute":"É","eacute":"é","Ecirc":"Ê","ecirc":"ê","Egrave":"È","egrave":"è","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","frac12":"½","frac14":"¼","frac34":"¾","gt":">","GT":">","Iacute":"Í","iacute":"í","Icirc":"Î","icirc":"î","iexcl":"¡","Igrave":"Ì","igrave":"ì","iquest":"¿","Iuml":"Ï","iuml":"ï","laquo":"«","lt":"<","LT":"<","macr":"¯","micro":"µ","middot":"·","nbsp":" ","not":"¬","Ntilde":"Ñ","ntilde":"ñ","Oacute":"Ó","oacute":"ó","Ocirc":"Ô","ocirc":"ô","Ograve":"Ò","ograve":"ò","ordf":"ª","ordm":"º","Oslash":"Ø","oslash":"ø","Otilde":"Õ","otilde":"õ","Ouml":"Ö","ouml":"ö","para":"¶","plusmn":"±","pound":"£","quot":"\\"","QUOT":"\\"","raquo":"»","reg":"®","REG":"®","sect":"§","shy":"­","sup1":"¹","sup2":"²","sup3":"³","szlig":"ß","THORN":"Þ","thorn":"þ","times":"×","Uacute":"Ú","uacute":"ú","Ucirc":"Û","ucirc":"û","Ugrave":"Ù","ugrave":"ù","uml":"¨","Uuml":"Ü","uuml":"ü","Yacute":"Ý","yacute":"ý","yen":"¥","yuml":"ÿ"}'); /***/ }), -/* 44 */ +/* 45 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"amp":"&","apos":"\'","gt":">","lt":"<","quot":"\\""}'); /***/ }), -/* 45 */ +/* 46 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42492,9 +42472,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DomHandler = void 0; -var domelementtype_1 = __webpack_require__(46); -var node_1 = __webpack_require__(47); -__exportStar(__webpack_require__(47), exports); +var domelementtype_1 = __webpack_require__(47); +var node_1 = __webpack_require__(48); +__exportStar(__webpack_require__(48), exports); var reWhitespace = /\s+/g; // Default options var defaultOpts = { @@ -42654,7 +42634,7 @@ exports["default"] = DomHandler; /***/ }), -/* 46 */ +/* 47 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -42716,7 +42696,7 @@ exports.Doctype = ElementType.Doctype; /***/ }), -/* 47 */ +/* 48 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42749,7 +42729,7 @@ var __assign = (this && this.__assign) || function () { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.cloneNode = exports.hasChildren = exports.isDocument = exports.isDirective = exports.isComment = exports.isText = exports.isCDATA = exports.isTag = exports.Element = exports.Document = exports.NodeWithChildren = exports.ProcessingInstruction = exports.Comment = exports.Text = exports.DataNode = exports.Node = void 0; -var domelementtype_1 = __webpack_require__(46); +var domelementtype_1 = __webpack_require__(47); var nodeTypes = new Map([ [domelementtype_1.ElementType.Tag, 1], [domelementtype_1.ElementType.Script, 1], @@ -43167,7 +43147,7 @@ function cloneChildren(childs) { /***/ }), -/* 48 */ +/* 49 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43211,9 +43191,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseFeed = exports.FeedHandler = void 0; -var domhandler_1 = __importDefault(__webpack_require__(45)); -var DomUtils = __importStar(__webpack_require__(49)); -var Parser_1 = __webpack_require__(38); +var domhandler_1 = __importDefault(__webpack_require__(46)); +var DomUtils = __importStar(__webpack_require__(50)); +var Parser_1 = __webpack_require__(39); var FeedItemMediaMedium; (function (FeedItemMediaMedium) { FeedItemMediaMedium[FeedItemMediaMedium["image"] = 0] = "image"; @@ -43409,7 +43389,7 @@ exports.parseFeed = parseFeed; /***/ }), -/* 49 */ +/* 50 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43426,15 +43406,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.hasChildren = exports.isDocument = exports.isComment = exports.isText = exports.isCDATA = exports.isTag = void 0; -__exportStar(__webpack_require__(50), exports); -__exportStar(__webpack_require__(56), exports); +__exportStar(__webpack_require__(51), exports); __exportStar(__webpack_require__(57), exports); __exportStar(__webpack_require__(58), exports); __exportStar(__webpack_require__(59), exports); __exportStar(__webpack_require__(60), exports); __exportStar(__webpack_require__(61), exports); +__exportStar(__webpack_require__(62), exports); /** @deprecated Use these methods from `domhandler` directly. */ -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); Object.defineProperty(exports, "isTag", ({ enumerable: true, get: function () { return domhandler_1.isTag; } })); Object.defineProperty(exports, "isCDATA", ({ enumerable: true, get: function () { return domhandler_1.isCDATA; } })); Object.defineProperty(exports, "isText", ({ enumerable: true, get: function () { return domhandler_1.isText; } })); @@ -43444,7 +43424,7 @@ Object.defineProperty(exports, "hasChildren", ({ enumerable: true, get: function /***/ }), -/* 50 */ +/* 51 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43454,9 +43434,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.innerText = exports.textContent = exports.getText = exports.getInnerHTML = exports.getOuterHTML = void 0; -var domhandler_1 = __webpack_require__(45); -var dom_serializer_1 = __importDefault(__webpack_require__(51)); -var domelementtype_1 = __webpack_require__(46); +var domhandler_1 = __webpack_require__(46); +var dom_serializer_1 = __importDefault(__webpack_require__(52)); +var domelementtype_1 = __webpack_require__(47); /** * @param node Node to get the outer HTML of. * @param options Options for serialization. @@ -43537,7 +43517,7 @@ exports.innerText = innerText; /***/ }), -/* 51 */ +/* 52 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43576,15 +43556,15 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); /* * Module dependencies */ -var ElementType = __importStar(__webpack_require__(46)); -var entities_1 = __webpack_require__(52); +var ElementType = __importStar(__webpack_require__(47)); +var entities_1 = __webpack_require__(53); /** * Mixed-case SVG and MathML tags & attributes * recognized by the HTML parser. * * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign */ -var foreignNames_1 = __webpack_require__(55); +var foreignNames_1 = __webpack_require__(56); var unencodedElements = new Set([ "style", "script", @@ -43755,15 +43735,15 @@ function renderComment(elem) { /***/ }), -/* 52 */ +/* 53 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.decodeXMLStrict = exports.decodeHTML5Strict = exports.decodeHTML4Strict = exports.decodeHTML5 = exports.decodeHTML4 = exports.decodeHTMLStrict = exports.decodeHTML = exports.decodeXML = exports.encodeHTML5 = exports.encodeHTML4 = exports.escapeUTF8 = exports.escape = exports.encodeNonAsciiHTML = exports.encodeHTML = exports.encodeXML = exports.encode = exports.decodeStrict = exports.decode = void 0; -var decode_1 = __webpack_require__(53); -var encode_1 = __webpack_require__(54); +var decode_1 = __webpack_require__(54); +var encode_1 = __webpack_require__(55); /** * Decodes a string with entities. * @@ -43797,7 +43777,7 @@ function encode(data, level) { return (!level || level <= 0 ? encode_1.encodeXML : encode_1.encodeHTML)(data); } exports.encode = encode; -var encode_2 = __webpack_require__(54); +var encode_2 = __webpack_require__(55); Object.defineProperty(exports, "encodeXML", ({ enumerable: true, get: function () { return encode_2.encodeXML; } })); Object.defineProperty(exports, "encodeHTML", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); Object.defineProperty(exports, "encodeNonAsciiHTML", ({ enumerable: true, get: function () { return encode_2.encodeNonAsciiHTML; } })); @@ -43806,7 +43786,7 @@ Object.defineProperty(exports, "escapeUTF8", ({ enumerable: true, get: function // Legacy aliases (deprecated) Object.defineProperty(exports, "encodeHTML4", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); Object.defineProperty(exports, "encodeHTML5", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); -var decode_2 = __webpack_require__(53); +var decode_2 = __webpack_require__(54); Object.defineProperty(exports, "decodeXML", ({ enumerable: true, get: function () { return decode_2.decodeXML; } })); Object.defineProperty(exports, "decodeHTML", ({ enumerable: true, get: function () { return decode_2.decodeHTML; } })); Object.defineProperty(exports, "decodeHTMLStrict", ({ enumerable: true, get: function () { return decode_2.decodeHTMLStrict; } })); @@ -43819,7 +43799,7 @@ Object.defineProperty(exports, "decodeXMLStrict", ({ enumerable: true, get: func /***/ }), -/* 53 */ +/* 54 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43829,10 +43809,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.decodeHTML = exports.decodeHTMLStrict = exports.decodeXML = void 0; -var entities_json_1 = __importDefault(__webpack_require__(42)); -var legacy_json_1 = __importDefault(__webpack_require__(43)); -var xml_json_1 = __importDefault(__webpack_require__(44)); -var decode_codepoint_1 = __importDefault(__webpack_require__(40)); +var entities_json_1 = __importDefault(__webpack_require__(43)); +var legacy_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); +var decode_codepoint_1 = __importDefault(__webpack_require__(41)); var strictEntityRe = /&(?:[a-zA-Z0-9]+|#[xX][\da-fA-F]+|#\d+);/g; exports.decodeXML = getStrictDecoder(xml_json_1.default); exports.decodeHTMLStrict = getStrictDecoder(entities_json_1.default); @@ -43879,7 +43859,7 @@ function getReplacer(map) { /***/ }), -/* 54 */ +/* 55 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43889,7 +43869,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.escapeUTF8 = exports.escape = exports.encodeNonAsciiHTML = exports.encodeHTML = exports.encodeXML = void 0; -var xml_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); var inverseXML = getInverseObj(xml_json_1.default); var xmlReplacer = getInverseReplacer(inverseXML); /** @@ -43900,7 +43880,7 @@ var xmlReplacer = getInverseReplacer(inverseXML); * numeric hexadecimal reference (eg. `ü`) will be used. */ exports.encodeXML = getASCIIEncoder(inverseXML); -var entities_json_1 = __importDefault(__webpack_require__(42)); +var entities_json_1 = __importDefault(__webpack_require__(43)); var inverseHTML = getInverseObj(entities_json_1.default); var htmlReplacer = getInverseReplacer(inverseHTML); /** @@ -44022,7 +44002,7 @@ function getASCIIEncoder(obj) { /***/ }), -/* 55 */ +/* 56 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -44132,14 +44112,14 @@ exports.attributeNames = new Map([ /***/ }), -/* 56 */ +/* 57 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prevElementSibling = exports.nextElementSibling = exports.getName = exports.hasAttrib = exports.getAttributeValue = exports.getSiblings = exports.getParent = exports.getChildren = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); var emptyArray = []; /** * Get a node's children. @@ -44256,7 +44236,7 @@ exports.prevElementSibling = prevElementSibling; /***/ }), -/* 57 */ +/* 58 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -44392,14 +44372,14 @@ exports.prepend = prepend; /***/ }), -/* 58 */ +/* 59 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.findAll = exports.existsOne = exports.findOne = exports.findOneChild = exports.find = exports.filter = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); /** * Search a node and its children for nodes passing a test function. * @@ -44525,15 +44505,15 @@ exports.findAll = findAll; /***/ }), -/* 59 */ +/* 60 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getElementsByTagType = exports.getElementsByTagName = exports.getElementById = exports.getElements = exports.testElement = void 0; -var domhandler_1 = __webpack_require__(45); -var querying_1 = __webpack_require__(58); +var domhandler_1 = __webpack_require__(46); +var querying_1 = __webpack_require__(59); var Checks = { tag_name: function (name) { if (typeof name === "function") { @@ -44656,14 +44636,14 @@ exports.getElementsByTagType = getElementsByTagType; /***/ }), -/* 60 */ +/* 61 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.uniqueSort = exports.compareDocumentPosition = exports.removeSubsets = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); /** * Given an array of nodes, remove any member that is contained by another. * @@ -44788,15 +44768,15 @@ exports.uniqueSort = uniqueSort; /***/ }), -/* 61 */ +/* 62 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getFeed = void 0; -var stringify_1 = __webpack_require__(50); -var legacy_1 = __webpack_require__(59); +var stringify_1 = __webpack_require__(51); +var legacy_1 = __webpack_require__(60); /** * Get the feed object from the root of a DOM tree. * @@ -44985,7 +44965,7 @@ function isValidFeed(value) { /***/ }), -/* 62 */ +/* 63 */ /***/ ((module) => { "use strict"; @@ -45005,7 +44985,7 @@ module.exports = string => { /***/ }), -/* 63 */ +/* 64 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -45050,7 +45030,7 @@ exports.isPlainObject = isPlainObject; /***/ }), -/* 64 */ +/* 65 */ /***/ ((module) => { "use strict"; @@ -45190,7 +45170,7 @@ module.exports = deepmerge_1; /***/ }), -/* 65 */ +/* 66 */ /***/ (function(module, exports) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/** @@ -45521,30 +45501,30 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 66 */ +/* 67 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let CssSyntaxError = __webpack_require__(67) -let Declaration = __webpack_require__(70) -let LazyResult = __webpack_require__(75) -let Container = __webpack_require__(84) -let Processor = __webpack_require__(97) -let stringify = __webpack_require__(74) -let fromJSON = __webpack_require__(99) -let Document = __webpack_require__(86) -let Warning = __webpack_require__(89) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Result = __webpack_require__(88) -let Input = __webpack_require__(80) -let parse = __webpack_require__(90) -let list = __webpack_require__(96) -let Rule = __webpack_require__(95) -let Root = __webpack_require__(94) -let Node = __webpack_require__(71) +let CssSyntaxError = __webpack_require__(68) +let Declaration = __webpack_require__(71) +let LazyResult = __webpack_require__(76) +let Container = __webpack_require__(85) +let Processor = __webpack_require__(98) +let stringify = __webpack_require__(75) +let fromJSON = __webpack_require__(100) +let Document = __webpack_require__(87) +let Warning = __webpack_require__(90) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Result = __webpack_require__(89) +let Input = __webpack_require__(81) +let parse = __webpack_require__(91) +let list = __webpack_require__(97) +let Rule = __webpack_require__(96) +let Root = __webpack_require__(95) +let Node = __webpack_require__(72) function postcss(...plugins) { if (plugins.length === 1 && Array.isArray(plugins[0])) { @@ -45629,15 +45609,15 @@ postcss.default = postcss /***/ }), -/* 67 */ +/* 68 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let pico = __webpack_require__(68) +let pico = __webpack_require__(69) -let terminalHighlight = __webpack_require__(69) +let terminalHighlight = __webpack_require__(70) class CssSyntaxError extends Error { constructor(message, line, column, source, file, plugin) { @@ -45736,7 +45716,7 @@ CssSyntaxError.default = CssSyntaxError /***/ }), -/* 68 */ +/* 69 */ /***/ ((module) => { var x=String; @@ -45746,19 +45726,19 @@ module.exports.createColors = create; /***/ }), -/* 69 */ +/* 70 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 70 */ +/* 71 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Node = __webpack_require__(71) +let Node = __webpack_require__(72) class Declaration extends Node { constructor(defaults) { @@ -45783,16 +45763,16 @@ Declaration.default = Declaration /***/ }), -/* 71 */ +/* 72 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let CssSyntaxError = __webpack_require__(67) -let Stringifier = __webpack_require__(73) -let stringify = __webpack_require__(74) +let { isClean, my } = __webpack_require__(73) +let CssSyntaxError = __webpack_require__(68) +let Stringifier = __webpack_require__(74) +let stringify = __webpack_require__(75) function cloneNode(obj, parent) { let cloned = new obj.constructor() @@ -46169,7 +46149,7 @@ Node.default = Node /***/ }), -/* 72 */ +/* 73 */ /***/ ((module) => { "use strict"; @@ -46181,7 +46161,7 @@ module.exports.my = Symbol('my') /***/ }), -/* 73 */ +/* 74 */ /***/ ((module) => { "use strict"; @@ -46541,13 +46521,13 @@ Stringifier.default = Stringifier /***/ }), -/* 74 */ +/* 75 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Stringifier = __webpack_require__(73) +let Stringifier = __webpack_require__(74) function stringify(node, builder) { let str = new Stringifier(builder) @@ -46559,21 +46539,21 @@ stringify.default = stringify /***/ }), -/* 75 */ +/* 76 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let MapGenerator = __webpack_require__(76) -let stringify = __webpack_require__(74) -let Container = __webpack_require__(84) -let Document = __webpack_require__(86) -let warnOnce = __webpack_require__(87) -let Result = __webpack_require__(88) -let parse = __webpack_require__(90) -let Root = __webpack_require__(94) +let { isClean, my } = __webpack_require__(73) +let MapGenerator = __webpack_require__(77) +let stringify = __webpack_require__(75) +let Container = __webpack_require__(85) +let Document = __webpack_require__(87) +let warnOnce = __webpack_require__(88) +let Result = __webpack_require__(89) +let parse = __webpack_require__(91) +let Root = __webpack_require__(95) const TYPE_TO_CLASS_NAME = { document: 'Document', @@ -47116,17 +47096,17 @@ Document.registerLazyResult(LazyResult) /***/ }), -/* 76 */ +/* 77 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { dirname, resolve, relative, sep } = __webpack_require__(78) -let { pathToFileURL } = __webpack_require__(79) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { dirname, resolve, relative, sep } = __webpack_require__(79) +let { pathToFileURL } = __webpack_require__(80) -let Input = __webpack_require__(80) +let Input = __webpack_require__(81) let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) let pathAvailable = Boolean(dirname && resolve && relative && sep) @@ -47454,38 +47434,38 @@ module.exports = MapGenerator /***/ }), -/* 77 */ +/* 78 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 78 */ +/* 79 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 79 */ +/* 80 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 80 */ +/* 81 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { fileURLToPath, pathToFileURL } = __webpack_require__(79) -let { resolve, isAbsolute } = __webpack_require__(78) -let { nanoid } = __webpack_require__(81) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { fileURLToPath, pathToFileURL } = __webpack_require__(80) +let { resolve, isAbsolute } = __webpack_require__(79) +let { nanoid } = __webpack_require__(82) -let terminalHighlight = __webpack_require__(69) -let CssSyntaxError = __webpack_require__(67) -let PreviousMap = __webpack_require__(82) +let terminalHighlight = __webpack_require__(70) +let CssSyntaxError = __webpack_require__(68) +let PreviousMap = __webpack_require__(83) let fromOffsetCache = Symbol('fromOffsetCache') @@ -47727,7 +47707,7 @@ if (terminalHighlight && terminalHighlight.registerInput) { /***/ }), -/* 81 */ +/* 82 */ /***/ ((module) => { let urlAlphabet = @@ -47754,15 +47734,15 @@ module.exports = { nanoid, customAlphabet } /***/ }), -/* 82 */ +/* 83 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { existsSync, readFileSync } = __webpack_require__(83) -let { dirname, join } = __webpack_require__(78) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { existsSync, readFileSync } = __webpack_require__(84) +let { dirname, join } = __webpack_require__(79) function fromBase64(str) { if (Buffer) { @@ -47903,22 +47883,22 @@ PreviousMap.default = PreviousMap /***/ }), -/* 83 */ +/* 84 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 84 */ +/* 85 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let Declaration = __webpack_require__(70) -let Comment = __webpack_require__(85) -let Node = __webpack_require__(71) +let { isClean, my } = __webpack_require__(73) +let Declaration = __webpack_require__(71) +let Comment = __webpack_require__(86) +let Node = __webpack_require__(72) let parse, Rule, AtRule, Root @@ -48091,16 +48071,16 @@ class Container extends Node { } insertBefore(exist, add) { - exist = this.index(exist) - + let existIndex = this.index(exist) let type = exist === 0 ? 'prepend' : false - let nodes = this.normalize(add, this.proxyOf.nodes[exist], type).reverse() - for (let node of nodes) this.proxyOf.nodes.splice(exist, 0, node) + let nodes = this.normalize(add, this.proxyOf.nodes[existIndex], type).reverse() + existIndex = this.index(exist) + for (let node of nodes) this.proxyOf.nodes.splice(existIndex, 0, node) let index for (let id in this.indexes) { index = this.indexes[id] - if (exist <= index) { + if (existIndex <= index) { this.indexes[id] = index + nodes.length } } @@ -48111,15 +48091,15 @@ class Container extends Node { } insertAfter(exist, add) { - exist = this.index(exist) - - let nodes = this.normalize(add, this.proxyOf.nodes[exist]).reverse() - for (let node of nodes) this.proxyOf.nodes.splice(exist + 1, 0, node) + let existIndex = this.index(exist) + let nodes = this.normalize(add, this.proxyOf.nodes[existIndex]).reverse() + existIndex = this.index(exist) + for (let node of nodes) this.proxyOf.nodes.splice(existIndex + 1, 0, node) let index for (let id in this.indexes) { index = this.indexes[id] - if (exist < index) { + if (existIndex < index) { this.indexes[id] = index + nodes.length } } @@ -48355,13 +48335,13 @@ Container.rebuild = node => { /***/ }), -/* 85 */ +/* 86 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Node = __webpack_require__(71) +let Node = __webpack_require__(72) class Comment extends Node { constructor(defaults) { @@ -48375,13 +48355,13 @@ Comment.default = Comment /***/ }), -/* 86 */ +/* 87 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) let LazyResult, Processor @@ -48415,7 +48395,7 @@ Document.default = Document /***/ }), -/* 87 */ +/* 88 */ /***/ ((module) => { "use strict"; @@ -48435,13 +48415,13 @@ module.exports = function warnOnce(message) { /***/ }), -/* 88 */ +/* 89 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Warning = __webpack_require__(89) +let Warning = __webpack_require__(90) class Result { constructor(processor, root, opts) { @@ -48484,7 +48464,7 @@ Result.default = Result /***/ }), -/* 89 */ +/* 90 */ /***/ ((module) => { "use strict"; @@ -48528,15 +48508,15 @@ Warning.default = Warning /***/ }), -/* 90 */ +/* 91 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) -let Parser = __webpack_require__(91) -let Input = __webpack_require__(80) +let Container = __webpack_require__(85) +let Parser = __webpack_require__(92) +let Input = __webpack_require__(81) function parse(css, opts) { let input = new Input(css, opts) @@ -48577,18 +48557,18 @@ Container.registerParse(parse) /***/ }), -/* 91 */ +/* 92 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Declaration = __webpack_require__(70) -let tokenizer = __webpack_require__(92) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Root = __webpack_require__(94) -let Rule = __webpack_require__(95) +let Declaration = __webpack_require__(71) +let tokenizer = __webpack_require__(93) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Root = __webpack_require__(95) +let Rule = __webpack_require__(96) const SAFE_COMMENT_NEIGHBOR = { empty: true, @@ -49187,7 +49167,7 @@ module.exports = Parser /***/ }), -/* 92 */ +/* 93 */ /***/ ((module) => { "use strict"; @@ -49460,13 +49440,13 @@ module.exports = function tokenizer(input, options = {}) { /***/ }), -/* 93 */ +/* 94 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) class AtRule extends Container { constructor(defaults) { @@ -49492,13 +49472,13 @@ Container.registerAtRule(AtRule) /***/ }), -/* 94 */ +/* 95 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) let LazyResult, Processor @@ -49560,14 +49540,14 @@ Container.registerRoot(Root) /***/ }), -/* 95 */ +/* 96 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) -let list = __webpack_require__(96) +let Container = __webpack_require__(85) +let list = __webpack_require__(97) class Rule extends Container { constructor(defaults) { @@ -49594,7 +49574,7 @@ Container.registerRule(Rule) /***/ }), -/* 96 */ +/* 97 */ /***/ ((module) => { "use strict"; @@ -49659,20 +49639,20 @@ list.default = list /***/ }), -/* 97 */ +/* 98 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let NoWorkResult = __webpack_require__(98) -let LazyResult = __webpack_require__(75) -let Document = __webpack_require__(86) -let Root = __webpack_require__(94) +let NoWorkResult = __webpack_require__(99) +let LazyResult = __webpack_require__(76) +let Document = __webpack_require__(87) +let Root = __webpack_require__(95) class Processor { constructor(plugins = []) { - this.version = '8.4.16' + this.version = '8.4.17' this.plugins = this.normalize(plugins) } @@ -49733,17 +49713,17 @@ Document.registerProcessor(Processor) /***/ }), -/* 98 */ +/* 99 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let MapGenerator = __webpack_require__(76) -let stringify = __webpack_require__(74) -let warnOnce = __webpack_require__(87) -let parse = __webpack_require__(90) -const Result = __webpack_require__(88) +let MapGenerator = __webpack_require__(77) +let stringify = __webpack_require__(75) +let warnOnce = __webpack_require__(88) +let parse = __webpack_require__(91) +const Result = __webpack_require__(89) class NoWorkResult { constructor(processor, css, opts) { @@ -49875,19 +49855,19 @@ NoWorkResult.default = NoWorkResult /***/ }), -/* 99 */ +/* 100 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Declaration = __webpack_require__(70) -let PreviousMap = __webpack_require__(82) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Input = __webpack_require__(80) -let Root = __webpack_require__(94) -let Rule = __webpack_require__(95) +let Declaration = __webpack_require__(71) +let PreviousMap = __webpack_require__(83) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Input = __webpack_require__(81) +let Root = __webpack_require__(95) +let Rule = __webpack_require__(96) function fromJSON(json, inputs) { if (Array.isArray(json)) return json.map(n => fromJSON(n)) @@ -49936,7 +49916,7 @@ fromJSON.default = fromJSON /***/ }), -/* 100 */ +/* 101 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -49946,7 +49926,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpArmor = void 0; const buf_1 = __webpack_require__(4); const common_1 = __webpack_require__(23); -const openpgp_1 = __webpack_require__(101); +const openpgp_1 = __webpack_require__(102); class PgpArmor { } exports.PgpArmor = PgpArmor; @@ -50056,7 +50036,7 @@ PgpArmor.cryptoMsgPrepareForDecrypt = async (encrypted) => { /***/ }), -/* 101 */ +/* 102 */ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -93628,7 +93608,7 @@ var elliptic$1 = /*#__PURE__*/Object.freeze({ /***/ }), -/* 102 */ +/* 103 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -93637,14 +93617,14 @@ var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpKey = void 0; const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); -const store_1 = __webpack_require__(103); -const mnemonic_1 = __webpack_require__(104); +const catch_1 = __webpack_require__(34); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); +const store_1 = __webpack_require__(104); +const mnemonic_1 = __webpack_require__(105); const util_1 = __webpack_require__(5); -const openpgp_1 = __webpack_require__(101); -const pgp_1 = __webpack_require__(105); +const openpgp_1 = __webpack_require__(102); +const pgp_1 = __webpack_require__(106); const require_1 = __webpack_require__(24); const common_1 = __webpack_require__(23); class PgpKey { @@ -93980,7 +93960,7 @@ PgpKey.revoke = async (key) => { /***/ }), -/* 103 */ +/* 104 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -94022,7 +94002,7 @@ Store.keyCacheRenewExpiry = () => { /***/ }), -/* 104 */ +/* 105 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96102,16 +96082,16 @@ exports.mnemonic = mnemonic; /***/ }), -/* 105 */ +/* 106 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isPacketDecrypted = exports.isFullyEncrypted = exports.isFullyDecrypted = void 0; -const pgp_key_1 = __webpack_require__(102); -const const_1 = __webpack_require__(106); -const openpgp_1 = __webpack_require__(101); +const pgp_key_1 = __webpack_require__(103); +const const_1 = __webpack_require__(107); +const openpgp_1 = __webpack_require__(102); openpgp_1.config.versionString = `FlowCrypt ${const_1.VERSION} Gmail Encryption`; openpgp_1.config.commentString = 'Seamlessly send and receive encrypted email'; openpgp_1.config.allowUnauthenticatedMessages = true; @@ -96155,7 +96135,7 @@ exports.isPacketDecrypted = isPacketDecrypted; /***/ }), -/* 106 */ +/* 107 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96185,7 +96165,7 @@ exports.gmailBackupSearchQuery = gmailBackupSearchQuery; /***/ }), -/* 107 */ +/* 108 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -96193,16 +96173,16 @@ exports.gmailBackupSearchQuery = gmailBackupSearchQuery; var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpMsg = exports.FormatError = exports.DecryptErrTypes = void 0; -const pgp_key_1 = __webpack_require__(102); +const pgp_key_1 = __webpack_require__(103); const msg_block_1 = __webpack_require__(3); const common_1 = __webpack_require__(23); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); -const store_1 = __webpack_require__(103); -const openpgp_1 = __webpack_require__(101); -const pgp_1 = __webpack_require__(105); +const catch_1 = __webpack_require__(34); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); +const store_1 = __webpack_require__(104); +const openpgp_1 = __webpack_require__(102); +const pgp_1 = __webpack_require__(106); const require_1 = __webpack_require__(24); var DecryptErrTypes; (function (DecryptErrTypes) { @@ -96575,7 +96555,7 @@ PgpMsg.cryptoMsgDecryptCategorizeErr = (decryptErr, msgPwd) => { /***/ }), -/* 108 */ +/* 109 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96670,14 +96650,14 @@ PgpPwd.readableCrackTime = (totalSeconds) => { /***/ }), -/* 109 */ +/* 110 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readArmoredKeyOrThrow = exports.ValidateInput = void 0; -const openpgp_1 = __webpack_require__(101); +const openpgp_1 = __webpack_require__(102); class ValidateInput { } exports.ValidateInput = ValidateInput; @@ -96868,9 +96848,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ return module.exports; /******/ } /******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = __webpack_modules__; -/******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { @@ -96884,28 +96861,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ }; /******/ })(); /******/ -/******/ /* webpack/runtime/ensure chunk */ -/******/ (() => { -/******/ __webpack_require__.f = {}; -/******/ // This file contains only the entry chunk. -/******/ // The chunk loading function for additional chunks -/******/ __webpack_require__.e = (chunkId) => { -/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { -/******/ __webpack_require__.f[key](chunkId, promises); -/******/ return promises; -/******/ }, [])); -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/get javascript chunk filename */ -/******/ (() => { -/******/ // This function allow to reference async chunks -/******/ __webpack_require__.u = (chunkId) => { -/******/ // return url for filenames based on template -/******/ return "" + chunkId + ".js"; -/******/ }; -/******/ })(); -/******/ /******/ /* webpack/runtime/global */ /******/ (() => { /******/ __webpack_require__.g = (function() { @@ -96923,52 +96878,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ -/******/ /* webpack/runtime/load script */ -/******/ (() => { -/******/ var inProgress = {}; -/******/ var dataWebpackPrefix = "flowcrypt-mobile-core:"; -/******/ // loadScript function to load a script via script tag -/******/ __webpack_require__.l = (url, done, key, chunkId) => { -/******/ if(inProgress[url]) { inProgress[url].push(done); return; } -/******/ var script, needAttach; -/******/ if(key !== undefined) { -/******/ var scripts = document.getElementsByTagName("script"); -/******/ for(var i = 0; i < scripts.length; i++) { -/******/ var s = scripts[i]; -/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; } -/******/ } -/******/ } -/******/ if(!script) { -/******/ needAttach = true; -/******/ script = document.createElement('script'); -/******/ -/******/ script.charset = 'utf-8'; -/******/ script.timeout = 120; -/******/ if (__webpack_require__.nc) { -/******/ script.setAttribute("nonce", __webpack_require__.nc); -/******/ } -/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key); -/******/ script.src = url; -/******/ } -/******/ inProgress[url] = [done]; -/******/ var onScriptComplete = (prev, event) => { -/******/ // avoid mem leaks in IE. -/******/ script.onerror = script.onload = null; -/******/ clearTimeout(timeout); -/******/ var doneFns = inProgress[url]; -/******/ delete inProgress[url]; -/******/ script.parentNode && script.parentNode.removeChild(script); -/******/ doneFns && doneFns.forEach((fn) => (fn(event))); -/******/ if(prev) return prev(event); -/******/ } -/******/ ; -/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); -/******/ script.onerror = onScriptComplete.bind(null, script.onerror); -/******/ script.onload = onScriptComplete.bind(null, script.onload); -/******/ needAttach && document.head.appendChild(script); -/******/ }; -/******/ })(); -/******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports @@ -96980,101 +96889,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ }; /******/ })(); /******/ -/******/ /* webpack/runtime/publicPath */ -/******/ (() => { -/******/ __webpack_require__.p = ""; -/******/ })(); -/******/ -/******/ /* webpack/runtime/jsonp chunk loading */ -/******/ (() => { -/******/ // no baseURI -/******/ -/******/ // object to store loaded and loading chunks -/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched -/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded -/******/ var installedChunks = { -/******/ 1: 0 -/******/ }; -/******/ -/******/ __webpack_require__.f.j = (chunkId, promises) => { -/******/ // JSONP chunk loading for javascript -/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; -/******/ if(installedChunkData !== 0) { // 0 means "already installed". -/******/ -/******/ // a Promise means "currently loading". -/******/ if(installedChunkData) { -/******/ promises.push(installedChunkData[2]); -/******/ } else { -/******/ if(true) { // all chunks have JS -/******/ // setup Promise in chunk cache -/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); -/******/ promises.push(installedChunkData[2] = promise); -/******/ -/******/ // start chunk loading -/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId); -/******/ // create error before stack unwound to get useful stacktrace later -/******/ var error = new Error(); -/******/ var loadingEnded = (event) => { -/******/ if(__webpack_require__.o(installedChunks, chunkId)) { -/******/ installedChunkData = installedChunks[chunkId]; -/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined; -/******/ if(installedChunkData) { -/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); -/******/ var realSrc = event && event.target && event.target.src; -/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; -/******/ error.name = 'ChunkLoadError'; -/******/ error.type = errorType; -/******/ error.request = realSrc; -/******/ installedChunkData[1](error); -/******/ } -/******/ } -/******/ }; -/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); -/******/ } else installedChunks[chunkId] = 0; -/******/ } -/******/ } -/******/ }; -/******/ -/******/ // no prefetching -/******/ -/******/ // no preloaded -/******/ -/******/ // no HMR -/******/ -/******/ // no HMR manifest -/******/ -/******/ // no on chunks loaded -/******/ -/******/ // install a JSONP callback for chunk loading -/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { -/******/ var [chunkIds, moreModules, runtime] = data; -/******/ // add "moreModules" to the modules object, -/******/ // then flag all "chunkIds" as loaded and fire callback -/******/ var moduleId, chunkId, i = 0; -/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { -/******/ for(moduleId in moreModules) { -/******/ if(__webpack_require__.o(moreModules, moduleId)) { -/******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; -/******/ } -/******/ } -/******/ if(runtime) var result = runtime(__webpack_require__); -/******/ } -/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); -/******/ for(;i < chunkIds.length; i++) { -/******/ chunkId = chunkIds[i]; -/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { -/******/ installedChunks[chunkId][0](); -/******/ } -/******/ installedChunks[chunkId] = 0; -/******/ } -/******/ -/******/ } -/******/ -/******/ var chunkLoadingGlobal = this["webpackChunkflowcrypt_mobile_core"] = this["webpackChunkflowcrypt_mobile_core"] || []; -/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); -/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); -/******/ })(); -/******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode. diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift index 626881204..59c7a8a7a 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation import XCTest class ClientConfigurationTests: XCTestCase { diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift index 03f67769e..88eb8fbe2 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class LocalClientConfigurationMock: LocalClientConfigurationType { var raw: RawClientConfiguration? diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift index 557d7bcd1..7deb33312 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class EnterpriseServerApiMock: EnterpriseServerApiType { var email = "example@flowcrypt.test" diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift index 8de13c104..fc3987fed 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class OrganisationalRulesServiceMock: ClientConfigurationServiceType { var fetchOrganisationalRulesForCurrentUserResult: Result = .failure(MockError()) diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift index 84131162d..e47e901c9 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class PassPhraseStorageMock: PassPhraseStorageType { diff --git a/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift index 21919444c..46589da0b 100644 --- a/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift +++ b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class CoreComposeMessageMock: CoreComposeMessageType, KeyParser { var composeEmailResult: ((SendableMsg, MsgFmt) -> (CoreRes.ComposeEmail))! diff --git a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift index 6bdd985a0..4d6d466c8 100644 --- a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift @@ -7,13 +7,20 @@ // @testable import FlowCrypt -import Foundation import GoogleAPIClientForREST_Gmail class DraftGatewayMock: DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft { - return GTLRGmail_Draft() + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? { + return nil } - func deleteDraft(with identifier: String) async {} + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { + return nil + } + + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier { + return MessageIdentifier(draftId: draftId ?? .random, threadId: nil, messageId: nil) + } + + func deleteDraft(with identifier: Identifier) async {} } diff --git a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift index d33722b38..f085e91b1 100644 --- a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift +++ b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class LocalContactsProviderMock: LocalContactsProviderType { diff --git a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift index 68d258941..c328a0986 100644 --- a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift @@ -7,13 +7,13 @@ // @testable import FlowCrypt -import Foundation class MessageGatewayMock: MessageGateway { - var sendMailResult: ((Data) -> (Result))! - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { + var sendMailResult: ((Data) -> (Result))! + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { if case .failure(let error) = sendMailResult(input.mime) { throw error } + return .random } } diff --git a/FlowCryptAppTests/TestData.swift b/FlowCryptAppTests/TestData.swift index c58e4c5f8..dc3ec7be1 100644 --- a/FlowCryptAppTests/TestData.swift +++ b/FlowCryptAppTests/TestData.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation @testable import FlowCrypt struct TestData { diff --git a/FlowCryptCommon/Extensions/LocalizationExtensions.swift b/FlowCryptCommon/Extensions/LocalizationExtensions.swift index 7f2ad8c64..fd3168082 100644 --- a/FlowCryptCommon/Extensions/LocalizationExtensions.swift +++ b/FlowCryptCommon/Extensions/LocalizationExtensions.swift @@ -8,11 +8,13 @@ import Foundation -@inline(__always) private func localize(_ key: String) -> String { +@inline(__always) +private func localize(_ key: String) -> String { return NSLocalizedString(key, comment: "") } -@inline(__always) private func LocalizedString(_ key: String) -> String { +@inline(__always) +private func LocalizedString(_ key: String) -> String { return localize(key) } @@ -21,12 +23,14 @@ public extension String { return LocalizedString(self) } - @inline(__always) func localizeWithArguments(_ arguments: String...) -> String { + @inline(__always) + func localizeWithArguments(_ arguments: String...) -> String { String(format: localize(self), arguments: arguments) } /// use to localize plurals with Localizable.stringsdict - @inline(__always) func localizePluralsWithArguments(_ arguments: Int...) -> String { + @inline(__always) + func localizePluralsWithArguments(_ arguments: Int...) -> String { String(format: localize(self), arguments: arguments) } } diff --git a/FlowCryptCommon/Extensions/OptionalExtensions.swift b/FlowCryptCommon/Extensions/OptionalExtensions.swift index c31159abf..37253e9ac 100644 --- a/FlowCryptCommon/Extensions/OptionalExtensions.swift +++ b/FlowCryptCommon/Extensions/OptionalExtensions.swift @@ -8,8 +8,8 @@ import Foundation -extension Optional { - public func ifNotNil(_ transform: (Wrapped) throws -> U) rethrows -> U? { +public extension Optional { + func ifNotNil(_ transform: (Wrapped) throws -> U) rethrows -> U? { switch self { case .some(let value): return .some(try transform(value)) @@ -19,8 +19,8 @@ extension Optional { } } -extension Optional where Wrapped: Collection { - public var isEmptyOrNil: Bool { +public extension Optional where Wrapped: Collection { + var isEmptyOrNil: Bool { self?.isEmpty ?? true } } diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index cfd253fa2..14bbf509f 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -9,6 +9,10 @@ public extension String { !trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + var isPgp: Bool { + contains("-----BEGIN PGP ") && contains("-----END PGP ") + } + var trimLeadingSlash: String { if isNotEmpty, self[startIndex] == "/" { return String(dropFirst()) @@ -33,7 +37,7 @@ public extension String { ) -> String { String( self.enumerated() - .map { $0 > 0 && $0 % stride == 0 ? [separator, $1] : [$1] } + .map { $0 > 0 && $0.isMultiple(of: stride) ? [separator, $1] : [$1] } .joined() ) } @@ -84,12 +88,21 @@ public extension String { } func removingHtmlTags() -> String? { - return try? NSAttributedString( + try? NSAttributedString( data: self.data(using: .utf8)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil ).string } + + func removingMailThreadQuote() -> String { + guard let range = range( + of: "On [a-zA-Z0-9, ]*, at [a-zA-Z0-9: ]*, .* wrote:", + options: [.regularExpression] + ) else { return self } + + return self[startIndex.. Void - public extension UIViewController { /// Showing toast on root controller /// @@ -20,13 +20,15 @@ public extension UIViewController { /// - title: Title for the toast /// - duration: Toast presented duration. Default is 3.0 /// - position: Bottom by default. Can be top, center, bottom. - /// - completion: Notify when toast dissapeared + /// - shouldHideKeyboard: True by default. Hide keyboard when toast is presented + /// - completion: Notify when toast dissappeared @MainActor func showToast( _ message: String, title: String? = nil, duration: TimeInterval = 3.0, position: ToastPosition = .bottom, + shouldHideKeyboard: Bool = true, completion: ShowToastCompletion? = nil ) { guard let view = UIApplication.shared.keyWindow?.rootViewController?.view else { @@ -34,7 +36,10 @@ public extension UIViewController { return } view.hideAllToasts() - view.endEditing(true) + + if shouldHideKeyboard { + view.endEditing(true) + } view.makeToast( message, @@ -48,10 +53,11 @@ public extension UIViewController { } } +// MARK: - Alerts public extension UIViewController { @MainActor func showAlert(title: String? = "error".localized, message: String, onOk: (() -> Void)? = nil) { - self.view.hideAllToasts() + view.hideAllToasts() hideSpinner() let alert = UIAlertController( title: title, @@ -63,76 +69,80 @@ public extension UIViewController { style: .destructive ) { _ in onOk?() } alert.addAction(ok) - self.present(alert, animated: true, completion: nil) + present(alert, animated: true, completion: nil) } @MainActor func showAsyncAlert(title: String? = "error".localized, message: String) async throws { - return try await withCheckedThrowingContinuation { (continuation) in - showAlert(title: title, message: message, onOk: { + try await withCheckedThrowingContinuation { continuation in + showAlert(title: title, message: message) { return continuation.resume() - }) + } } } @MainActor - func showRetryAlert( - title: String? = "error".localized, - message: String, - cancelActionTitle: String = "cancel".localized, - onRetry: ((UIAlertAction) -> Void)?, + func showAlertWithAction( + title: String?, + message: String?, + cancelButtonTitle: String = "cancel".localized, + actionButtonTitle: String, + actionAccessibilityIdentifier: String? = nil, + actionStyle: UIAlertAction.Style = .default, + onAction: ((UIAlertAction) -> Void)?, onCancel: ((UIAlertAction) -> Void)? = nil ) { - self.view.hideAllToasts() + view.hideAllToasts() hideSpinner() let alert = UIAlertController( title: title, message: message, preferredStyle: .alert ) - let retry = UIAlertAction( - title: "retry_title".localized, - style: .cancel, - handler: onRetry + let action = UIAlertAction( + title: actionButtonTitle, + style: actionStyle, + handler: onAction ) + action.accessibilityIdentifier = actionAccessibilityIdentifier let cancel = UIAlertAction( - title: cancelActionTitle, - style: .default, - handler: onCancel) - alert.addAction(retry) + title: cancelButtonTitle, + style: .cancel, + handler: onCancel + ) + cancel.accessibilityIdentifier = "aid-cancel-button" + alert.addAction(action) alert.addAction(cancel) present(alert, animated: true, completion: nil) } @MainActor - func showConfirmAlert( - title: String? = "warning".localized, + func showRetryAlert( + title: String? = "error".localized, message: String, - confirmActionTitle: String = "confirm".localized, - cancelActionTitle: String = "cancel".localized, - onConfirm: ((UIAlertAction) -> Void)?, + cancelButtonTitle: String = "cancel".localized, + onRetry: ((UIAlertAction) -> Void)?, onCancel: ((UIAlertAction) -> Void)? = nil ) { - self.view.hideAllToasts() - hideSpinner() - let alert = UIAlertController( + showAlertWithAction( title: title, message: message, - preferredStyle: .alert + cancelButtonTitle: cancelButtonTitle, + actionButtonTitle: "retry_title".localized, + onAction: onRetry, + onCancel: onCancel ) - let confirm = UIAlertAction( - title: confirmActionTitle, - style: .cancel, - handler: onConfirm + } + + @MainActor + func showConfirmAlert(message: String, onConfirm: ((UIAlertAction) -> Void)?) { + showAlertWithAction( + title: "warning".localized, + message: message, + actionButtonTitle: "confirm".localized, + actionAccessibilityIdentifier: "aid-confirm-button", + onAction: onConfirm ) - let cancel = UIAlertAction( - title: cancelActionTitle, - style: .default, - handler: onCancel) - confirm.accessibilityIdentifier = "aid-confirm-button" - alert.addAction(confirm) - alert.addAction(cancel) - present(alert, animated: true, completion: nil) } func keyboardHeight(from notification: Notification) -> CGFloat { @@ -140,6 +150,7 @@ public extension UIViewController { } } +// MARK: - Navigation public extension UINavigationController { func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { pushViewController(viewController, animated: animated) @@ -174,13 +185,13 @@ public extension UIViewController { @MainActor func showSpinner(_ message: String = "loading_title".localized, isUserInteractionEnabled: Bool = false) { - guard self.view.subviews.first(where: { $0 is MBProgressHUD }) == nil else { + guard !view.subviews.contains(where: { $0 is MBProgressHUD }) else { // hud is already shown return } - self.view.isUserInteractionEnabled = isUserInteractionEnabled + view.isUserInteractionEnabled = isUserInteractionEnabled - let spinner = MBProgressHUD.showAdded(to: self.view, animated: true) + let spinner = MBProgressHUD.showAdded(to: view, animated: true) spinner.label.text = message spinner.isUserInteractionEnabled = isUserInteractionEnabled spinner.accessibilityIdentifier = "loadingSpinner" @@ -192,26 +203,28 @@ public extension UIViewController { progress: Float? = nil, systemImageName: String? = nil ) { - if let progress = progress { - if progress >= 1, let imageName = systemImageName { - self.updateSpinner( - label: "compose_sent".localized, - systemImageName: imageName) - } else { - self.showProgressHUD(progress: progress, label: label) - } - } else { + guard let progress = progress else { showIndeterminateHUD(with: label) + return + } + + if progress >= 1, let imageName = systemImageName { + updateSpinner( + label: "compose_sent".localized, + systemImageName: imageName + ) + } else { + showProgressHUD(progress: progress, label: label) } } @MainActor func hideSpinner() { - let subviews = self.view.subviews.compactMap { $0 as? MBProgressHUD } + let subviews = view.subviews.compactMap { $0 as? MBProgressHUD } for subview in subviews { subview.hide(animated: true) } - self.view.isUserInteractionEnabled = true + view.isUserInteractionEnabled = true } @MainActor @@ -234,7 +247,7 @@ public extension UIViewController { @MainActor func showIndeterminateHUD(with title: String) { - self.currentProgressHUD.mode = .indeterminate - self.currentProgressHUD.label.text = title + currentProgressHUD.mode = .indeterminate + currentProgressHUD.label.text = title } } diff --git a/FlowCryptCommon/Extensions/UIVIewExtensions.swift b/FlowCryptCommon/Extensions/UIViewExtensions.swift similarity index 100% rename from FlowCryptCommon/Extensions/UIVIewExtensions.swift rename to FlowCryptCommon/Extensions/UIViewExtensions.swift diff --git a/FlowCryptCommon/Trace.swift b/FlowCryptCommon/Trace.swift index 0820317e8..018ad20a7 100644 --- a/FlowCryptCommon/Trace.swift +++ b/FlowCryptCommon/Trace.swift @@ -29,7 +29,7 @@ public final class Trace { public func finish(roundedTo: Int = 3) -> String { let resultValue = result() - let timeValue: String = resultValue <= 1 ? " ms" : " sec" + let timeValue = resultValue <= 1 ? " ms" : " sec" return resultValue.roundedString(toPlace: roundedTo) + timeValue } diff --git a/FlowCryptUI/Cell Nodes/BackupCellNode.swift b/FlowCryptUI/Cell Nodes/BackupCellNode.swift index cf52b94b5..446bbe3b4 100644 --- a/FlowCryptUI/Cell Nodes/BackupCellNode.swift +++ b/FlowCryptUI/Cell Nodes/BackupCellNode.swift @@ -17,7 +17,7 @@ public final class BackupCellNode: CellNode { self.insets = insets } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { return ASCenterLayoutSpec( centeringOptions: .XY, sizingOptions: .minimumXY, diff --git a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift index 794fefdad..ac90bc811 100644 --- a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift @@ -33,7 +33,7 @@ public final class ButtonCellNode: CellNode { private let insets: UIEdgeInsets private let buttonColor: UIColor? - public var isButtonEnabled: Bool = true { + public var isButtonEnabled = true { didSet { button.isEnabled = isButtonEnabled let alpha: CGFloat = isButtonEnabled ? 1 : 0.5 @@ -53,7 +53,7 @@ public final class ButtonCellNode: CellNode { button.setAttributedTitle(input.title, for: .normal) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: button diff --git a/FlowCryptUI/Cell Nodes/CellNode.swift b/FlowCryptUI/Cell Nodes/CellNode.swift index 682c6fd71..ec62c033e 100644 --- a/FlowCryptUI/Cell Nodes/CellNode.swift +++ b/FlowCryptUI/Cell Nodes/CellNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit open class CellNode: ASCellNode { - public override init() { + override public init() { super.init() automaticallyManagesSubnodes = true selectionStyle = .none diff --git a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift index ec82fa089..632ebf154 100644 --- a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift +++ b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift @@ -51,7 +51,7 @@ public final class CheckBoxTextNode: CellNode { } } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { checkBox.style.preferredSize = input.preferredSize if input.subtitle != nil { diff --git a/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift b/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift index 4f832f612..47c1e88fc 100644 --- a/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift @@ -38,14 +38,14 @@ public final class ComposeRecipientCellNode: CellNode { darkStyle: .white, lightStyle: .black ) - recipientNode.attributedText = input.recipients.map { (recipient) -> NSAttributedString in + recipientNode.attributedText = input.recipients.map { recipient -> NSAttributedString in // Use black text color for gray bubbles var textColor = recipient.state.backgroundColor if textColor == titleNodeBackgroundColorSelected { textColor = grayBubbleTextColor } return recipient.email.string.attributed(.regular(17), color: textColor, alignment: .left) - }.reduce(NSMutableAttributedString()) { (r, e) in + }.reduce(NSMutableAttributedString()) { r, e in if r.length > 0 { r.append(", ".attributed(color: grayBubbleTextColor)) } @@ -54,7 +54,7 @@ public final class ComposeRecipientCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), child: recipientNode diff --git a/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift b/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift index 5f10daa99..ae814f19a 100644 --- a/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift +++ b/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift @@ -37,7 +37,7 @@ public final class ComposeRecipientPopupNameNode: CellNode { super.init() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let stack = ASStackLayoutSpec.vertical() if name != nil { stack.children = [nameNode, emailNode] diff --git a/FlowCryptUI/Cell Nodes/ContactCellNode.swift b/FlowCryptUI/Cell Nodes/ContactCellNode.swift index 8487fe2d9..8501e6adb 100644 --- a/FlowCryptUI/Cell Nodes/ContactCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactCellNode.swift @@ -54,7 +54,7 @@ public final class ContactCellNode: CellNode { action?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let children: [ASLayoutElement] if input.name == nil { emailNode.style.flexGrow = 1 diff --git a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift index 2e0d31973..1a03e91ae 100644 --- a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import Foundation public final class ContactKeyCellNode: CellNode { public struct Input { @@ -68,7 +67,7 @@ public final class ContactKeyCellNode: CellNode { borderNode.isUserInteractionEnabled = false } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let specs = [ [fingerprintTitleNode, fingerprintNode], [createdAtTitleNode, createdAtNode], diff --git a/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift index 19b54c104..c99fc6d07 100644 --- a/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift @@ -31,7 +31,7 @@ public final class ContactUserCellNode: CellNode { userNode.accessibilityIdentifier = "aid-user-email" } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), child: ASStackLayoutSpec( diff --git a/FlowCryptUI/Cell Nodes/DividerCellNode.swift b/FlowCryptUI/Cell Nodes/DividerCellNode.swift index 57c247a8f..28e22f68c 100644 --- a/FlowCryptUI/Cell Nodes/DividerCellNode.swift +++ b/FlowCryptUI/Cell Nodes/DividerCellNode.swift @@ -24,7 +24,7 @@ public final class DividerCellNode: CellNode { backgroundColor = .clear } - public override func layoutSpecThatFits(_ range: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ range: ASSizeRange) -> ASLayoutSpec { let expectedWidth = range.max.width - inset.width line.style.preferredSize.width = expectedWidth > 0 ? expectedWidth : range.max.width return ASInsetLayoutSpec(insets: inset, child: line) diff --git a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift index 4fb6e4b65..e27309fa8 100644 --- a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift @@ -60,7 +60,7 @@ public final class EmptyCellNode: CellNode { accessibilityIdentifier = input.accessibilityIdentifier } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let spec = ASStackLayoutSpec( direction: .vertical, spacing: 16, @@ -70,7 +70,7 @@ public final class EmptyCellNode: CellNode { ) spec.style.preferredSize = size return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16), + insets: UIEdgeInsets.side(16), child: ASCenterLayoutSpec(child: spec) ) } diff --git a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift index 618b4a2d5..7e23d6dc5 100644 --- a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift @@ -35,8 +35,7 @@ public final class EmptyFolderCellNode: CellNode { return buttonNode }() - public init(path: String, - emptyFolder: (() -> Void)?) { + public init(path: String, emptyFolder: (() -> Void)?) { self.path = path self.emptyFolder = emptyFolder super.init() @@ -46,7 +45,7 @@ public final class EmptyFolderCellNode: CellNode { emptyFolder?() } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let stack = ASStackLayoutSpec.horizontal() textNode.style.maxWidth = ASDimensionMake(constrainedSize.max.width - 60) stack.children = [ diff --git a/FlowCryptUI/Cell Nodes/InboxCellNode.swift b/FlowCryptUI/Cell Nodes/InboxCellNode.swift index 8e431f275..0c65dd027 100644 --- a/FlowCryptUI/Cell Nodes/InboxCellNode.swift +++ b/FlowCryptUI/Cell Nodes/InboxCellNode.swift @@ -76,7 +76,7 @@ public final class InboxCellNode: CellNode { accessibilityIdentifier = "aid-inbox-item" } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let emailElement: ASLayoutElement = { guard let countNode = countNode else { return emailNode } emailNode.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Cell Nodes/InfoCellNode.swift b/FlowCryptUI/Cell Nodes/InfoCellNode.swift index b2a6ae33c..b7fbd3adf 100644 --- a/FlowCryptUI/Cell Nodes/InfoCellNode.swift +++ b/FlowCryptUI/Cell Nodes/InfoCellNode.swift @@ -52,7 +52,7 @@ public final class InfoCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { guard imageNode.image != nil else { return ASInsetLayoutSpec( insets: input?.insets ?? .zero, diff --git a/FlowCryptUI/Cell Nodes/LabelCellNode.swift b/FlowCryptUI/Cell Nodes/LabelCellNode.swift index b9d4bc914..2a91d080d 100644 --- a/FlowCryptUI/Cell Nodes/LabelCellNode.swift +++ b/FlowCryptUI/Cell Nodes/LabelCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import Foundation public final class LabelCellNode: CellNode { public struct Input { @@ -17,6 +16,9 @@ public final class LabelCellNode: CellNode { let spacing: CGFloat let accessibilityIdentifier: String? let labelAccessibilityIdentifier: String? + let buttonAccessibilityIdentifier: String? + let actionButtonImageName: String? + let action: (() -> Void)? public init( title: NSAttributedString, @@ -24,7 +26,10 @@ public final class LabelCellNode: CellNode { insets: UIEdgeInsets = .deviceSpecificTextInsets(top: 8, bottom: 8), spacing: CGFloat = 4, accessibilityIdentifier: String? = nil, - labelAccessibilityIdentifier: String? = nil + labelAccessibilityIdentifier: String? = nil, + buttonAccessibilityIdentifier: String? = nil, + actionButtonImageName: String? = nil, + action: (() -> Void)? = nil ) { self.title = title self.text = text @@ -32,12 +37,17 @@ public final class LabelCellNode: CellNode { self.spacing = spacing self.accessibilityIdentifier = accessibilityIdentifier self.labelAccessibilityIdentifier = labelAccessibilityIdentifier + self.buttonAccessibilityIdentifier = buttonAccessibilityIdentifier + self.actionButtonImageName = actionButtonImageName + self.action = action } } private let titleNode = ASTextNode2() private let textNode = ASTextNode2() + private let actionButtonNode = ASButtonNode() private let input: Input + private var action: (() -> Void)? public init(input: Input) { self.input = input @@ -47,18 +57,48 @@ public final class LabelCellNode: CellNode { titleNode.accessibilityIdentifier = input.labelAccessibilityIdentifier textNode.attributedText = input.text textNode.accessibilityIdentifier = input.accessibilityIdentifier + + action = input.action + actionButtonNode.addTarget(self, action: #selector(onActionButtonTap), forControlEvents: .touchUpInside) + + if let imageName = input.actionButtonImageName { + actionButtonNode.accessibilityIdentifier = input.buttonAccessibilityIdentifier + actionButtonNode.setImage(UIImage(systemName: imageName)?.tinted(.secondaryLabel), for: .normal) + } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - ASInsetLayoutSpec( - insets: input.insets, - child: ASStackLayoutSpec( - direction: .vertical, + @objc private func onActionButtonTap() { + action?() + } + + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let labelSpec = ASStackLayoutSpec( + direction: .vertical, + spacing: input.spacing, + justifyContent: .start, + alignItems: .start, + children: [titleNode, textNode] + ) + if action != nil { + actionButtonNode.style.preferredSize = CGSize(width: 36, height: 44) + + let spec = ASStackLayoutSpec( + direction: .horizontal, spacing: input.spacing, - justifyContent: .start, - alignItems: .start, - children: [titleNode, textNode] + justifyContent: .spaceBetween, + alignItems: .stretch, + children: [labelSpec, actionButtonNode] ) - ) + labelSpec.style.flexShrink = 1 + return ASInsetLayoutSpec( + insets: input.insets, + child: spec + ) + } else { + return ASInsetLayoutSpec( + insets: input.insets, + child: labelSpec + ) + } } } diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift similarity index 58% rename from FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift rename to FlowCryptUI/Cell Nodes/MessageActionCellNode.swift index 599eec670..f439ec62b 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift @@ -1,5 +1,5 @@ // -// MessagePasswordCellNode.swift +// MessageActionCellNode.swift // FlowCryptUI // // Created by Roma Sosnovsky on 15/12/21 @@ -9,30 +9,34 @@ import AsyncDisplayKit import UIKit -public final class MessagePasswordCellNode: CellNode { +public final class MessageActionCellNode: CellNode { public struct Input { let text: NSAttributedString? let color: UIColor let image: UIImage? - - public init(text: NSAttributedString?, - color: UIColor, - image: UIImage?) { + let accessibilityIdentifier: String? + + public init( + text: NSAttributedString?, + color: UIColor, + image: UIImage?, + accessibilityIdentifier: String? + ) { self.text = text self.color = color self.image = image + self.accessibilityIdentifier = accessibilityIdentifier } } private let input: Input private let buttonNode = ASButtonNode() - private let setMessagePassword: (() -> Void)? + private let action: (() -> Void)? - public init(input: Input, - setMessagePassword: (() -> Void)?) { + public init(input: Input, action: (() -> Void)?) { self.input = input - self.setMessagePassword = setMessagePassword + self.action = action super.init() @@ -47,34 +51,23 @@ public final class MessagePasswordCellNode: CellNode { buttonNode.borderWidth = 1 buttonNode.cornerRadius = 6 buttonNode.contentHorizontalAlignment = .left - buttonNode.accessibilityIdentifier = "aid-message-password-cell" + buttonNode.accessibilityIdentifier = input.accessibilityIdentifier buttonNode.setAttributedTitle(input.text, for: .normal) buttonNode.setImage(input.image, for: .normal) buttonNode.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - buttonNode.style.flexShrink = 1.0 - - let spacer = ASLayoutSpec() - spacer.style.flexGrow = 1.0 - - let spec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 4, - justifyContent: .start, - alignItems: .start, - children: [buttonNode, spacer] - ) + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + buttonNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - child: spec + child: buttonNode ) } @objc private func onButtonTap() { - setMessagePassword?() + action?() } } diff --git a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift index ee8397adf..d957f49a0 100644 --- a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift @@ -21,7 +21,7 @@ public final class MessageSubjectNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { subjectNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 16, bottom: 4), diff --git a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift index f74ff8c6a..3c38f0fd5 100644 --- a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift @@ -24,7 +24,7 @@ public final class MessageTextSubjectNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { textNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 7f52e8c0e..1299a22f8 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -29,7 +29,7 @@ final class RecipientEmailNode: CellNode { let input: Input let imageNode = ASImageNode() - public var onTap: ((Tap) -> Void)? + var onTap: ((Tap) -> Void)? init(input: Input, index: Int) { self.input = input @@ -47,7 +47,7 @@ final class RecipientEmailNode: CellNode { titleNode.clipsToBounds = true titleNode.borderWidth = 1 titleNode.borderColor = input.recipient.state.borderColor.cgColor - titleNode.textContainerInset = RecipientEmailNode.Constants.titleInsets + titleNode.textContainerInset = Self.Constants.titleInsets imageNode.image = input.recipient.state.stateImage imageNode.alpha = 0 diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 9a3d20b7d..dbbf89db4 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -10,7 +10,7 @@ import AsyncDisplayKit public final class RecipientEmailTextFieldNode: TextFieldCellNode { - public override init( + override public init( input: TextFieldCellNode.Input, action: TextFieldAction? = nil ) { @@ -20,12 +20,12 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { textField.becomeFirstResponder() return true } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { textField.style.preferredSize.height = input.height return ASInsetLayoutSpec(insets: input.insets, child: textField) diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index a324b1985..7435bda5b 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -7,9 +7,8 @@ // import AsyncDisplayKit -import FlowCryptCommon -final public class RecipientEmailsCellNode: CellNode { +public final class RecipientEmailsCellNode: CellNode { public typealias RecipientTap = (RecipientEmailTapAction) -> Void public enum RecipientEmailTapAction { @@ -91,7 +90,7 @@ final public class RecipientEmailsCellNode: CellNode { self.toggleButtonAction = toggleButtonAction } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let collectionNodeSize = CGSize(width: constrainedSize.max.width, height: collectionLayoutHeight) let buttonSize = CGSize(width: 40, height: 32) @@ -118,13 +117,13 @@ final public class RecipientEmailsCellNode: CellNode { } } -extension RecipientEmailsCellNode { - public func onItemSelect(_ action: RecipientTap?) -> Self { +public extension RecipientEmailsCellNode { + func onItemSelect(_ action: RecipientTap?) -> Self { self.onAction = action return self } - public func onLayoutHeightChanged(_ completion: @escaping (CGFloat) -> Void) -> Self { + func onLayoutHeightChanged(_ completion: @escaping (CGFloat) -> Void) -> Self { self.layout.onHeightChanged = completion return self } @@ -139,7 +138,7 @@ extension RecipientEmailsCellNode: ASCollectionDelegate, ASCollectionDataSource let width = collectionNode.style.preferredSize.width return { [weak self] in - guard let self = self else { + guard let self else { return ASCellNode() } if indexPath.row == self.recipients.count { @@ -181,7 +180,7 @@ extension RecipientEmailsCellNode { toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: angle) } - let angle = self.isToggleButtonRotated ? .pi : 0 + let angle = isToggleButtonRotated ? .pi : 0 if animated { UIView.animate(withDuration: 0.3) { rotateButton(angle: angle) diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index 1a594dfb9..b4f877aa2 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -9,8 +9,8 @@ import UIKit // MARK: Input -extension RecipientEmailsCellNode { - public struct Input { +public extension RecipientEmailsCellNode { + struct Input { public struct StateContext: Equatable { let backgroundColor, borderColor, textColor: UIColor let image: UIImage? @@ -55,11 +55,11 @@ extension RecipientEmailsCellNode { } } - public var backgroundColor: UIColor { + var backgroundColor: UIColor { stateContext.backgroundColor } - public var borderColor: UIColor { + var borderColor: UIColor { stateContext.borderColor } @@ -67,11 +67,11 @@ extension RecipientEmailsCellNode { stateContext.textColor } - public var stateImage: UIImage? { + var stateImage: UIImage? { stateContext.image } - public var accessibilityIdentifier: String? { + var accessibilityIdentifier: String? { stateContext.accessibilityIdentifier } @@ -96,9 +96,9 @@ extension RecipientEmailsCellNode { } } - public let email: NSAttributedString - public let type: String - public var state: State + let email: NSAttributedString + let type: String + var state: State public init( email: NSAttributedString, diff --git a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift index 42bcfccab..eab515499 100644 --- a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift @@ -7,15 +7,14 @@ // import AsyncDisplayKit -import FlowCryptCommon -final public class RecipientFromCellNode: CellNode { +public final class RecipientFromCellNode: CellNode { private enum Constants { static let sectionInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) static let minimumLineSpacing: CGFloat = 4 } - lazy var labelTextNode: ASTextNode2 = { + private lazy var labelTextNode: ASTextNode2 = { let textNode = ASTextNode2() let textTitle = "compose_recipient_from".localized textNode.attributedText = textTitle.attributed(.regular(17), color: .lightGray, alignment: .left) @@ -23,19 +22,15 @@ final public class RecipientFromCellNode: CellNode { return textNode }() - lazy var valueTextNode: ASTextNode2 = { + private lazy var valueTextNode: ASTextNode2 = { let textNode = ASTextNode2() textNode.accessibilityIdentifier = "aid-from-value-node" return textNode }() - public var fromEmail: String? { - didSet { - self.valueTextNode.attributedText = fromEmail?.attributed(.regular(17)) - } - } + private let fromEmail: String - lazy var toggleButtonNode: ASButtonNode = { + private lazy var toggleButtonNode: ASButtonNode = { let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) let button = ASButtonNode() @@ -43,18 +38,19 @@ final public class RecipientFromCellNode: CellNode { button.setImage(image, for: .normal) button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0) button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - button.addTarget(self, action: #selector(self.onToggleButtonTap), forControlEvents: .touchUpInside) + button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) return button }() - var toggleButtonAction: (() -> Void)? + private var toggleButtonAction: (() -> Void)? - public init(toggleButtonAction: (() -> Void)?) { - super.init() + public init(fromEmail: String, toggleButtonAction: (() -> Void)?) { + self.fromEmail = fromEmail self.toggleButtonAction = toggleButtonAction + super.init() } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let insets = UIEdgeInsets.deviceSpecificTextInsets(top: 0, bottom: 0) toggleButtonNode.style.preferredSize = CGSize(width: 40, height: 28) @@ -62,14 +58,15 @@ final public class RecipientFromCellNode: CellNode { let stack = ASStackLayoutSpec.horizontal() stack.verticalAlignment = .center valueTextNode.style.flexGrow = 1 + valueTextNode.attributedText = fromEmail.attributed(.regular(17)) - let textNodeStack = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0), child: labelTextNode) + let textNodeStack = ASInsetLayoutSpec(insets: .zero, child: labelTextNode) stack.children = [textNodeStack, valueTextNode, toggleButtonNode] return ASInsetLayoutSpec(insets: insets, child: stack) } - @objc func onToggleButtonTap() { + @objc private func onToggleButtonTap() { toggleButtonAction?() } } diff --git a/FlowCryptUI/Cell Nodes/SetupTitleNode.swift b/FlowCryptUI/Cell Nodes/SetupTitleNode.swift index 3c45c14ed..575bf83e2 100644 --- a/FlowCryptUI/Cell Nodes/SetupTitleNode.swift +++ b/FlowCryptUI/Cell Nodes/SetupTitleNode.swift @@ -43,7 +43,7 @@ public final class SetupTitleNode: CellNode { backgroundColor = input.backgroundColor } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let layout = ASInsetLayoutSpec( insets: input.insets, child: ASCenterLayoutSpec( @@ -67,7 +67,7 @@ public final class SetupTitleNode: CellNode { } } - public override var isSelected: Bool { + override public var isSelected: Bool { didSet { selectedNode.backgroundColor = isSelected ? input.selectedLineColor : diff --git a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift index 5476636cf..91ae04191 100644 --- a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift +++ b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift @@ -60,7 +60,7 @@ public final class SwitchCellNode: CellNode { onAction(sender.isOn) } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { switchNode.style.preferredSize = CGSize(width: 100, height: 30) return ASStackLayoutSpec( direction: .horizontal, diff --git a/FlowCryptUI/Cell Nodes/TextCellNode.swift b/FlowCryptUI/Cell Nodes/TextCellNode.swift index 4d6d1338f..3fde502b8 100644 --- a/FlowCryptUI/Cell Nodes/TextCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon import UIKit public final class TextCellNode: CellNode { @@ -65,7 +64,7 @@ public final class TextCellNode: CellNode { backgroundColor = input.backgroundColor } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let spec = ASStackLayoutSpec( direction: .vertical, spacing: 16, diff --git a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift index d6c6d1e72..6f782157d 100644 --- a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift @@ -88,7 +88,7 @@ public class TextFieldCellNode: CellNode { } } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let preferredWidth = input.width ?? (constrainedSize.max.width - input.insets.width) textField.style.preferredSize = CGSize( width: max(0, preferredWidth), @@ -98,7 +98,7 @@ public class TextFieldCellNode: CellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { textField.becomeFirstResponder() return true } diff --git a/FlowCryptUI/Cell Nodes/TextViewCellNode.swift b/FlowCryptUI/Cell Nodes/TextViewCellNode.swift index 7579e4c77..a5b82222a 100644 --- a/FlowCryptUI/Cell Nodes/TextViewCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextViewCellNode.swift @@ -71,7 +71,7 @@ public final class TextViewCellNode: CellNode { if shouldAnimate { action?(.heightChanged(textView.textView)) } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { textView.style.preferredSize.height = height return ASInsetLayoutSpec( @@ -81,7 +81,7 @@ public final class TextViewCellNode: CellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { DispatchQueue.main.async { _ = self.textView.textView.becomeFirstResponder() } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift index 44df1d049..ca537b0e1 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift @@ -12,7 +12,7 @@ import UIKit public final class ThreadMessageInfoCellNode: CellNode { // MARK: - Input public struct Input { - public let encryptionBadge: BadgeNode.Input + public let encryptionBadge: BadgeNode.Input? public let signatureBadge: BadgeNode.Input? public let sender: NSAttributedString public let recipientLabel: NSAttributedString @@ -26,7 +26,7 @@ public final class ThreadMessageInfoCellNode: CellNode { public let index: Int public init( - encryptionBadge: BadgeNode.Input, + encryptionBadge: BadgeNode.Input?, signatureBadge: BadgeNode.Input?, sender: NSAttributedString, recipientLabel: NSAttributedString, @@ -148,7 +148,7 @@ public final class ThreadMessageInfoCellNode: CellNode { bccRecipients: input.bccRecipients ) ) - private lazy var encryptionNode = BadgeNode(input: input.encryptionBadge) + private lazy var encryptionNode: BadgeNode? = input.encryptionBadge.map(BadgeNode.init) private lazy var signatureNode: BadgeNode? = input.signatureBadge.map(BadgeNode.init) // MARK: - Properties @@ -289,7 +289,7 @@ public final class ThreadMessageInfoCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { replyNode.style.preferredSize = CGSize(width: 44, height: 44) menuNode.style.preferredSize = CGSize(width: 36, height: 44) expandNode.style.preferredSize = CGSize(width: 36, height: 44) diff --git a/FlowCryptUI/Cell Nodes/TitleCellNode.swift b/FlowCryptUI/Cell Nodes/TitleCellNode.swift index 953439ddf..4bdd89591 100644 --- a/FlowCryptUI/Cell Nodes/TitleCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TitleCellNode.swift @@ -18,7 +18,7 @@ public final class TitleCellNode: CellNode { textNode.attributedText = title } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: textNode diff --git a/FlowCryptUI/Nodes/AttachmentNode.swift b/FlowCryptUI/Nodes/AttachmentNode.swift index 1509964d5..4f83a5211 100644 --- a/FlowCryptUI/Nodes/AttachmentNode.swift +++ b/FlowCryptUI/Nodes/AttachmentNode.swift @@ -68,7 +68,7 @@ public final class AttachmentNode: CellNode { onDeleteTap?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let verticalStack = ASStackLayoutSpec.vertical() verticalStack.spacing = 3 verticalStack.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Nodes/BadgeNode.swift b/FlowCryptUI/Nodes/BadgeNode.swift index 70996428b..be0b06b6e 100644 --- a/FlowCryptUI/Nodes/BadgeNode.swift +++ b/FlowCryptUI/Nodes/BadgeNode.swift @@ -50,7 +50,7 @@ public final class BadgeNode: ASDisplayNode { cornerRadius = 4 } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let contentSpec = ASStackLayoutSpec( direction: .horizontal, spacing: 2, diff --git a/FlowCryptUI/Nodes/ButtonNode.swift b/FlowCryptUI/Nodes/ButtonNode.swift index baeec6255..3ffbcab89 100644 --- a/FlowCryptUI/Nodes/ButtonNode.swift +++ b/FlowCryptUI/Nodes/ButtonNode.swift @@ -2,7 +2,7 @@ // ButtonNode.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 29.10.2019. +// Created by Anton Kharchevskyi on 29.10.2019 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift index 43e90966a..3c53048ef 100644 --- a/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift +++ b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift @@ -30,7 +30,7 @@ public final class ButtonWithPaddingNode: ASDisplayNode { self.cornerRadius = cornerRadius } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: buttonNode diff --git a/FlowCryptUI/Nodes/CheckBoxNode.swift b/FlowCryptUI/Nodes/CheckBoxNode.swift index 47822ecb9..920114001 100644 --- a/FlowCryptUI/Nodes/CheckBoxNode.swift +++ b/FlowCryptUI/Nodes/CheckBoxNode.swift @@ -8,7 +8,7 @@ import AsyncDisplayKit -final public class CheckBoxNode: ASDisplayNode { +public final class CheckBoxNode: ASDisplayNode { public struct Input { let color: UIColor let strokeWidth: CGFloat diff --git a/FlowCryptUI/Nodes/KeySettingCellNode.swift b/FlowCryptUI/Nodes/KeySettingCellNode.swift index 6d6bc0ece..68480b7a8 100644 --- a/FlowCryptUI/Nodes/KeySettingCellNode.swift +++ b/FlowCryptUI/Nodes/KeySettingCellNode.swift @@ -45,7 +45,7 @@ public final class KeySettingCellNode: CellNode { separatorNode.backgroundColor = .lightGray } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let nameLocationStack = ASStackLayoutSpec.vertical() nameLocationStack.spacing = 6 nameLocationStack.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Nodes/KeyTextCellNode.swift b/FlowCryptUI/Nodes/KeyTextCellNode.swift index 92ca1e76a..a91bc8547 100644 --- a/FlowCryptUI/Nodes/KeyTextCellNode.swift +++ b/FlowCryptUI/Nodes/KeyTextCellNode.swift @@ -22,7 +22,7 @@ public final class KeyTextCellNode: CellNode { textNode.attributedText = title } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: textNode diff --git a/FlowCryptUI/Nodes/LinkButtonNode.swift b/FlowCryptUI/Nodes/LinkButtonNode.swift index 49d976e02..f4073b299 100644 --- a/FlowCryptUI/Nodes/LinkButtonNode.swift +++ b/FlowCryptUI/Nodes/LinkButtonNode.swift @@ -43,7 +43,7 @@ public final class LinkButtonNode: CellNode { tapAction?(identifier) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 30, left: 16, bottom: 8, right: 18), child: ASCenterLayoutSpec( diff --git a/FlowCryptUI/Nodes/MessageRecipientsNode.swift b/FlowCryptUI/Nodes/MessageRecipientsNode.swift index c16431621..f59d19bd9 100644 --- a/FlowCryptUI/Nodes/MessageRecipientsNode.swift +++ b/FlowCryptUI/Nodes/MessageRecipientsNode.swift @@ -95,7 +95,7 @@ public final class MessageRecipientsNode: ASDisplayNode { return node } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let recipientsNodes: [ASStackLayoutSpec] = RecipientType.allCases.compactMap { type in let recipients: [MessageRecipient] switch type { diff --git a/FlowCryptUI/Nodes/SignInDescriptionNode.swift b/FlowCryptUI/Nodes/SignInDescriptionNode.swift index 69491d641..1d2b71d9d 100644 --- a/FlowCryptUI/Nodes/SignInDescriptionNode.swift +++ b/FlowCryptUI/Nodes/SignInDescriptionNode.swift @@ -19,7 +19,7 @@ public final class SignInDescriptionNode: CellNode { selectionStyle = .none } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 30, left: 16, bottom: 55, right: 16), child: ASCenterLayoutSpec( diff --git a/FlowCryptUI/Nodes/SignInImageNode.swift b/FlowCryptUI/Nodes/SignInImageNode.swift index 33d613691..1bfe9e9e3 100644 --- a/FlowCryptUI/Nodes/SignInImageNode.swift +++ b/FlowCryptUI/Nodes/SignInImageNode.swift @@ -23,7 +23,7 @@ public final class SignInImageNode: CellNode { setNeedsLayout() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize.height = imageHeight return ASInsetLayoutSpec( diff --git a/FlowCryptUI/Nodes/SigninButtonNode.swift b/FlowCryptUI/Nodes/SigninButtonNode.swift index 04e56fe83..93a52c69c 100644 --- a/FlowCryptUI/Nodes/SigninButtonNode.swift +++ b/FlowCryptUI/Nodes/SigninButtonNode.swift @@ -45,7 +45,7 @@ public final class SigninButtonNode: CellNode { onTap?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: .deviceSpecificInsets( top: 16, diff --git a/FlowCryptUI/Nodes/TableNode.swift b/FlowCryptUI/Nodes/TableNode.swift index dd837ec38..7e373e59a 100644 --- a/FlowCryptUI/Nodes/TableNode.swift +++ b/FlowCryptUI/Nodes/TableNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit public final class TableNode: ASTableNode { - public override init(style: UITableView.Style) { + override public init(style: UITableView.Style) { super.init(style: style) view.showsVerticalScrollIndicator = false view.separatorStyle = .none @@ -17,7 +17,7 @@ public final class TableNode: ASTableNode { backgroundColor = .backgroundColor } - public var bounces: Bool = true { + public var bounces = true { didSet { DispatchQueue.main.async { self.view.bounces = self.bounces @@ -25,14 +25,14 @@ public final class TableNode: ASTableNode { } } - public override func asyncTraitCollectionDidChange( + override public func asyncTraitCollectionDidChange( withPreviousTraitCollection previousTraitCollection: ASPrimitiveTraitCollection ) { super.asyncTraitCollectionDidChange(withPreviousTraitCollection: previousTraitCollection) backgroundColor = .backgroundColor } - public override func reloadData() { + override public func reloadData() { DispatchQueue.main.async { super.reloadData() } diff --git a/FlowCryptUI/Nodes/TableViewController.swift b/FlowCryptUI/Nodes/TableViewController.swift index d4a20ccb6..6378a5375 100644 --- a/FlowCryptUI/Nodes/TableViewController.swift +++ b/FlowCryptUI/Nodes/TableViewController.swift @@ -11,18 +11,18 @@ import FlowCryptCommon @MainActor open class TableNodeViewController: ASDKViewController { - public override var title: String? { + override public var title: String? { didSet { navigationItem.setAccessibility(id: title) } } - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) node.reloadData() } - open override func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() Logger.nested(Self.self).logDebug("View did load") } diff --git a/FlowCryptUI/Nodes/TextFieldNode.swift b/FlowCryptUI/Nodes/TextFieldNode.swift index 4641aa3bb..52c1259c9 100644 --- a/FlowCryptUI/Nodes/TextFieldNode.swift +++ b/FlowCryptUI/Nodes/TextFieldNode.swift @@ -57,7 +57,7 @@ public final class TextFieldNode: ASDisplayNode { } } - public var isSecureTextEntry: Bool = false { + public var isSecureTextEntry = false { didSet { DispatchQueue.main.async { self.textField.isSecureTextEntry = self.isSecureTextEntry @@ -159,12 +159,12 @@ public final class TextFieldNode: ASDisplayNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec(insets: .zero, child: node) } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { DispatchQueue.main.async { super.becomeFirstResponder() _ = self.textField.becomeFirstResponder() @@ -173,8 +173,8 @@ public final class TextFieldNode: ASDisplayNode { } } -extension TextFieldNode { - public func reset() { +public extension TextFieldNode { + func reset() { (node.view as? TextField)?.text = nil } } diff --git a/FlowCryptUI/Nodes/TextImageNode.swift b/FlowCryptUI/Nodes/TextImageNode.swift index b013d3e9d..ddf51e403 100644 --- a/FlowCryptUI/Nodes/TextImageNode.swift +++ b/FlowCryptUI/Nodes/TextImageNode.swift @@ -63,7 +63,7 @@ public final class TextImageNode: CellNode { onTap?(self) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize = input.imageSize subTitleNode.style.flexGrow = 1 subTitleNode.style.flexShrink = 1 diff --git a/FlowCryptUI/Nodes/TextWithIconNode.swift b/FlowCryptUI/Nodes/TextWithIconNode.swift index d023a5395..a71b4814d 100644 --- a/FlowCryptUI/Nodes/TextWithIconNode.swift +++ b/FlowCryptUI/Nodes/TextWithIconNode.swift @@ -42,7 +42,7 @@ public final class TextWithIconNode: CellNode { imageNode.image = input.image } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize = input.imageSize return ASInsetLayoutSpec( diff --git a/FlowCryptUI/Nodes/ViewController.swift b/FlowCryptUI/Nodes/ViewController.swift index f71c2bd9a..794b372c9 100644 --- a/FlowCryptUI/Nodes/ViewController.swift +++ b/FlowCryptUI/Nodes/ViewController.swift @@ -11,7 +11,7 @@ import FlowCryptCommon @MainActor open class ViewController: ASDKViewController { - open override func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() Logger.nested(Self.self).logDebug("View did load") } diff --git a/FlowCryptUI/Views/NavigationBarActionButton.swift b/FlowCryptUI/Views/NavigationBarActionButton.swift index d923efe6c..4ad781e96 100644 --- a/FlowCryptUI/Views/NavigationBarActionButton.swift +++ b/FlowCryptUI/Views/NavigationBarActionButton.swift @@ -18,7 +18,8 @@ public final class NavigationBarActionButton: UIBarButtonItem { public convenience init(imageSystemName: String, action: (() -> Void)?, accessibilityIdentifier: String? = nil) { self.init() onAction = action - customView = LeftAlignedIconButton(type: .system).with { + customView = UIButton(type: .system).with { + $0.contentHorizontalAlignment = .left $0.setImage(UIImage(systemName: imageSystemName), for: .normal) $0.frame.size = Constants.buttonSize $0.addTarget(self, action: #selector(tap), for: .touchUpInside) @@ -31,13 +32,3 @@ public final class NavigationBarActionButton: UIBarButtonItem { onAction?() } } - -private final class LeftAlignedIconButton: UIButton { - override func layoutSubviews() { - super.layoutSubviews() - contentHorizontalAlignment = .left - let availableSpace = bounds.inset(by: contentEdgeInsets) - let availableWidth = availableSpace.width - imageEdgeInsets.right - (imageView?.frame.width ?? 0) - (titleLabel?.frame.width ?? 0) - titleEdgeInsets = UIEdgeInsets(top: 0, left: availableWidth / 2, bottom: 0, right: 0) - } -} diff --git a/FlowCryptUI/Views/NavigationBarItemsView.swift b/FlowCryptUI/Views/NavigationBarItemsView.swift index e981ba3aa..3f3e29d21 100644 --- a/FlowCryptUI/Views/NavigationBarItemsView.swift +++ b/FlowCryptUI/Views/NavigationBarItemsView.swift @@ -69,7 +69,7 @@ public final class NavigationBarItemsView: UIBarButtonItem { fatalError("init(coder:) has not been implemented") } - public override var isEnabled: Bool { + override public var isEnabled: Bool { didSet { customView?.alpha = isEnabled ? 1 : 0.5 } diff --git a/Gemfile b/Gemfile index ab5236013..1e9a2fd1c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,7 @@ source 'https://rubygems.org' -gem "fastlane", ">= 2.134.0" -gem "xcode-install", ">= 2.6.1" -gem "cocoapods", ">= 1.10.2" -gem "rest-client" +gem "fastlane", ">= 2.210.0" +gem "cocoapods", ">= 1.11.3" # Fastlane plugins from fastlane/Pluginfile gem 'fastlane-plugin-semaphore' diff --git a/Gemfile.lock b/Gemfile.lock index 20ad5a487..101cccae4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -199,7 +199,6 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) @@ -209,9 +208,6 @@ GEM json (2.6.2) jwt (2.5.0) memoist (0.16.2) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) minitest (5.16.3) @@ -231,11 +227,6 @@ GEM declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - rest-client (2.1.0) - http-accept (>= 1.7.0, < 2.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) retriable (3.1.2) rexml (3.2.5) rouge (2.0.7) @@ -270,9 +261,6 @@ GEM unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcode-install (2.8.1) - claide (>= 0.9.1) - fastlane (>= 2.1.0, < 3.0.0) xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) @@ -292,11 +280,9 @@ PLATFORMS x86_64-darwin-20 DEPENDENCIES - cocoapods (>= 1.10.2) - fastlane (>= 2.134.0) + cocoapods (>= 1.11.3) + fastlane (>= 2.210.0) fastlane-plugin-semaphore - rest-client - xcode-install (>= 2.6.1) BUNDLED WITH 2.3.6 diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json new file mode 100644 index 000000000..2e683cc75 --- /dev/null +++ b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json @@ -0,0 +1,76 @@ +{ + "acctEmail": "e2e.enterprise.test@flowcrypt.com", + "full": { + "id": "180b3b5efbdc46da", + "threadId": "180b3b5efbdc46da", + "labelIds": [ + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.2.7 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAz8it/geQ2cIvZHYP5qii9N7LZUUTGAn/vYfPhDC4", + "payload": { + "partId": "", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AOAM531gNh/ZbIBtNaSbpmsTIrCxGynC5ug0qmpykuhO2lGmSZekWkmB Oiylv/cDcGIk6xRrvb74+Azez4oOVVSJvsIVl9mnfLMJ9hf1iQ==" + }, + { + "name": "Openpgp", + "value": "id=B98A4D267F824CCC" + }, + { + "name": "From", + "value": "Dmitry at FlowCrypt " + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Date", + "value": "Wed, 11 May 2022 08:21:25 -0700" + }, + { + "name": "Subject", + "value": "new message for reply" + }, + { + "name": "To", + "value": "e2e enterprise test at FlowCrypt , demo@flowcrypt.com" + }, + { + "name": "Cc", + "value": "robot@flowcrypt.com" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + } + ], + "body": { + "size": 1750, + "data": "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjIuNw0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQXo4aXQvZ2VRMmNJdlpIWVA1cWlpOU43TFpVVVRHQW4vdllmUGhEQzQNCm1VZ3docXZXcUZHbEwzZWluVmdscWdhVjFBL3RDWFFvbFh3UEgrT0JpYjVEdmdURXJkanorK2N6QWZoTg0KaW4wenVOWFh3VjREWmo0K3BvRzdLWjRTQVFkQVFsWVNVSnQwbTFUcjd3WWVJU0RBWTlneThCQndONTR5DQorTm53TzJRcHpIVXdBNXg2Z05vZmMrNUZKS3NUZ1hGS2FnZjdCSXVobnJKdEJOcnloM1NDYmdQa0oxRVENCkRtSWxCRXRNUHAzMG1PQ0h3Y0ZNQTNpRU8vdWJteHRaQVEvL1lvV0NLSmdEZzU4REc0ZUh2Y3NrVkJ3Lw0KQVVGd29ybUMxUHhhWHZqUDZCYzlVN3R4TU5pNWpTZ3dtOEpZQ3MzL0JFQ3FDRkFEditOb2ZsTGJoMEZODQo2eXhTTUxzampvc1cwcEN3N2xuYlFvb0U5dHB5NnRSanFDa25EZDJ5dmRwTUhnUE5TM2pacGZ6dTROd2UNCnZtUWdKbWVKcCtUWmlOZmM3d25aLzR3c0t3UmE5b3d3QmI2Zm12SUR3YmpVNUZrT29sNHU5d2NxUndHNg0KdngvSVZSZ0x5ZHVkNkFtM05adlBiSy8zUFg4REx5TkQyTnFkQjl6K2luM0NrQ2NUUFlNdWxUMThtRlBDDQpPWkNZWjdNZExDd3V3U0pBdUlhZC9YTkdvc09mWGtEc1dCOVhyNjFVdGhQNmpnbzFORzlaTHgydXVzQzYNClluMFpHajhXeFZVbkgzWnJBaVhVTlJqemxFUTAzbFlWcklWUmVQbG9XSm9mY1ErL3llNjhqN0srOEZRcA0KYWdoTEFtdWt5d0o5bVRMeU1PVjMrU3pjVDdoazd3Ym1KTXprVzZtUDV5ekRLbE5XbUVwMEJCWjBodzREDQptTXRhKzlzbEFTbjY0WTY3ejNaRFFwUktDOWh5dnQ0Znh4VFh0V0JXWXBiRFBJTlVrdHV4ZndqSG0vZzINCjA4RFpCMEdEeDgwUk1nenhUQlhuOWlTSndEa29WWGxoQWtCbC9aZFg2dytaOGlSYW56SktoN2s1aUF2Rw0KRzk1UXBDWkFnYXlOcVBHWjlFbDVtU2pqUnpSZGlxenhZbStkNjI3QmI4RVdFZVN6TE82VXBqSEwvMzhJDQpXbnl4aGIwb1ZSUVh2bFZEcWZOWDlyRlF5UGNHWVdkenVINDlFSVBTOXdMNFE0RklaWHZBYkhTckJ3VEINClhnTlZEcnpCUXVKckR4SUJCMEQrOU9PTjRQdXdDWWY5YkNtZ29xeEVHTXpFQzM2L1k0V1gvNTFlamNkcg0KUURCTGtkQVlpajdGZGtTNjVwYzVxQUcrK3FyaHNSRnJxTWxWclRnS1RVVEd2YTQzZjMwRlNXaGVZQVhEDQo1NDRpU0VuQlhnUDMzWXd2dnFtTmRSSUJCMEFtL3AzM0YxeTI1MEZ0S0NSR2VWd0xFQVRwdWdnSEFQZWwNCmFZWEk2VEJvQlRDOWNWY2dYZXFuU25KL3pJVnhKQTNwZmJQME9qeVZGL2FRNUhGN3RGeFBacGY3c0hpLw0KMUNCaFNVNDVYUnJqSzVMU3dDd0JvUkwvLzQrcEpYZzJpS24rUTkwN2RwMjNJU3lDUkpiVzVYZ3VQZFlJDQprbFQ0ME5zK281SkkrU1I5Y3FPa3hmNlFMK3pBTWVOSW5IWXNzdHlYOEdVb21EaVdTYUlYamVSWGF1OUwNCnpNdGxncnpOcFhyUWJsRWcvVW9TbkUxY3hBNTZuNGRLMmM2bGVjVmhDVFFtS0RVcDBMODZzMW1UblRIMQ0KYmV4N1dsYmRkNEwvZm5xREsweGZqOWw5U05MSFppUGplK1piUktlMEN4VXBnM0trTGxET1U1bXFTbFliDQpQRS9kdXZhMUF3SU9QUFBzZzBLdjFRalR4SFFlSDhVUG9Ec2MybVNzdVZkYUFObFN2NUxvR1IvSWZOZnENCnBvU2NuTjdxMkVSOXhwZ0lpUWdBQS9RckNOSVNYcWZyMFE9PQ0KPUE1RXoNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg==" + } + }, + "sizeEstimate": 6475, + "historyId": "190359", + "internalDate": "1652282485000" + }, + "attachments": {}, + "raw": { + "id": "180b3b5efbdc46da", + "threadId": "180b3b5efbdc46da", + "labelIds": [ + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.2.7 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAz8it/geQ2cIvZHYP5qii9N7LZUUTGAn/vYfPhDC4", + "sizeEstimate": 6475, + "raw": "RGVsaXZlcmVkLVRvOiBlMmUuZW50ZXJwcmlzZS50ZXN0QGZsb3djcnlwdC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmEwNTo2NjM4OmFjNjowOjA6MDowIHdpdGggU01UUCBpZCBtNmNzcDcwODg1NDdqYWI7DQogICAgICAgIFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjYgLTA3MDAgKFBEVCkNClgtUmVjZWl2ZWQ6IGJ5IDIwMDI6YTA1OjYwMDA6MWE4ZTpiMDoyMGM6YmU4YzoxMGE0IHdpdGggU01UUCBpZCBmMTQtMjAwMjBhMDU2MDAwMWE4ZTAwYjAwMjBjYmU4YzEwYTRtcjE2MTMxNzQwd3J5LjQzNy4xNjUyMjgyNDg2Njc1Ow0KICAgICAgICBXZWQsIDExIE1heSAyMDIyIDA4OjIxOjI2IC0wNzAwIChQRFQpDQpBUkMtU2VhbDogaT0xOyBhPXJzYS1zaGEyNTY7IHQ9MTY1MjI4MjQ4NjsgY3Y9bm9uZTsNCiAgICAgICAgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgYj1vMTljMjZsMjd6UWZ6cE1UMjR6RG50WXRtTll4cTRXcTdFN2pSZUJMWEJWMDBkRDBiVDhnUml2MFJ5MWZBclhuOWgNCiAgICAgICAgIDN6bXBFeHdLbXZhRnk5OWRsZDVZRHJMcCtSM2F5dzNKREdIc2dMRDRsK1c1bm5OcUhxTXNoeVpYeHJwaTBneEppZ0lSDQogICAgICAgICBTUzZ4eHNTYVdHckIvU3JSTHlXTlQ5ME9YenV5bkN3T2h2d05sQUpQeVV0SUY0dTdoMXRudnJYMldPdVFqRy80c3VLRg0KICAgICAgICAgdDR5QWFZWHVIYkNPTzU5cWdVRzV0VDFtdTBydGQ3OGw2aGxlWGJtaEsvbXFNVW9ETlJYQmZJRTJKb1F5QTY0YkF4djQNCiAgICAgICAgIDE0NFR1cjNVTFEwRlFYNHNUemNDOGpJQ1RpbG9FUHM5MGJSd3kyNU5UMXhqZVhiczhWMmZ2Wkxuazd1eVo0QWhxUjRuDQogICAgICAgICBQQjNRPT0NCkFSQy1NZXNzYWdlLVNpZ25hdHVyZTogaT0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOyBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBoPWNjOnRvOnN1YmplY3Q6bWVzc2FnZS1pZDpkYXRlOm1pbWUtdmVyc2lvbjpmcm9tOm9wZW5wZ3ANCiAgICAgICAgIDpka2ltLXNpZ25hdHVyZTsNCiAgICAgICAgYmg9ZjMxVzd3SS8xdWk2M2c4Tkgxb25HSU9rQUZ2Mm11ek5HbllVenA5VVh2Yz07DQogICAgICAgIGI9VEdVTEhDdjJMMkFyS0FJLzI4Y1JmYVg1TTllSE5GUGFXRDNwMytjVjNpdXhQenhhZzIzR2FtVTkvRFY3bHdGOEdQDQogICAgICAgICB0cGoyUlZLdld5d240MjZOV0FhR2gxWHdHeGhXWlBVTHZhaWNJWlp5cGdTbjllTjdxODdXNk9VM0tqa1V6cTVYQXFwTQ0KICAgICAgICAgcUlkS3J0ZFMrWXk1VUxrL0FJa1kxc0wyNHhiZ0dHb2hFbzljellPOGo0TGdaNDNBSm05WWRjd1YrdGh1WW94SGV5QmsNCiAgICAgICAgIFdZNHMvajVyY3Nid2pKZ0tBYnBFZlJtZ211NGorNzg0bmZlckZNcHB1d1BBSU9FdythRlp4a2FlUHRUQVBnbEVyZ1VxDQogICAgICAgICBsR0p4OUttVHA0L29LV2dlZjJJdTRudGMrTzRIVlZXSzFoRzArUzQwaGg0VUhDa2xobUNvbHNKZUNNZTBLS0hQejhEcg0KICAgICAgICAgQ3Urdz09DQpBUkMtQXV0aGVudGljYXRpb24tUmVzdWx0czogaT0xOyBteC5nb29nbGUuY29tOw0KICAgICAgIGRraW09cGFzcyBoZWFkZXIuaT1AZmxvd2NyeXB0LmNvbSBoZWFkZXIucz1nb29nbGUgaGVhZGVyLmI9TVk3WnJrdHo7DQogICAgICAgc3BmPXBhc3MgKGdvb2dsZS5jb206IGRvbWFpbiBvZiBkbWl0cnlAZmxvd2NyeXB0LmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgc210cC5tYWlsZnJvbT1kbWl0cnlAZmxvd2NyeXB0LmNvbTsNCiAgICAgICBkbWFyYz1wYXNzIChwPVJFSkVDVCBzcD1SRUpFQ1QgZGlzPU5PTkUpIGhlYWRlci5mcm9tPWZsb3djcnlwdC5jb20NClJldHVybi1QYXRoOiA8ZG1pdHJ5QGZsb3djcnlwdC5jb20-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIGMxMS0yMDAyMGFkZmMwNGIwMDAwMDBiMDAyMGM3ZGZiNjY5MXNvcjEzMTM5MTl3cmYuOS4yMDIyLjA1LjExLjA4LjIxLjI2DQogICAgICAgIGZvciA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjYgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRtaXRyeUBmbG93Y3J5cHQuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUBmbG93Y3J5cHQuY29tIGhlYWRlci5zPWdvb2dsZSBoZWFkZXIuYj1NWTdacmt0ejsNCiAgICAgICBzcGY9cGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRtaXRyeUBmbG93Y3J5cHQuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBzbXRwLm1haWxmcm9tPWRtaXRyeUBmbG93Y3J5cHQuY29tOw0KICAgICAgIGRtYXJjPXBhc3MgKHA9UkVKRUNUIHNwPVJFSkVDVCBkaXM9Tk9ORSkgaGVhZGVyLmZyb209Zmxvd2NyeXB0LmNvbQ0KREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD1mbG93Y3J5cHQuY29tOyBzPWdvb2dsZTsNCiAgICAgICAgaD1vcGVucGdwOmZyb206bWltZS12ZXJzaW9uOmRhdGU6bWVzc2FnZS1pZDpzdWJqZWN0OnRvOmNjOw0KICAgICAgICBiaD1mMzFXN3dJLzF1aTYzZzhOSDFvbkdJT2tBRnYybXV6TkduWVV6cDlVWHZjPTsNCiAgICAgICAgYj1NWTdacmt0enZaeEdxb2JwNlhIRVBWUjA5VE9mamFod2NNS2lZcWxjRXRYVWZVMDZNODd2ZnpyZmpndDdEWkppWlQNCiAgICAgICAgIDZ3bytyQndvSWxuK3ZENWJKRnA3K2JqYVBZdkJ6U25PZTFzRnExRmpkWUxqQXBzUlpTTkw1RkFlOHRFNTRZWDBJNmZsDQogICAgICAgICB3TFlzRUFOWmxpQmE0VlBNMzV3VTF4V1BtSnpXSTJ2cHFWN1JnPQ0KWC1Hb29nbGUtREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD0xZTEwMC5uZXQ7IHM9MjAyMTAxMTI7DQogICAgICAgIGg9eC1nbS1tZXNzYWdlLXN0YXRlOm9wZW5wZ3A6ZnJvbTptaW1lLXZlcnNpb246ZGF0ZTptZXNzYWdlLWlkDQogICAgICAgICA6c3ViamVjdDp0bzpjYzsNCiAgICAgICAgYmg9ZjMxVzd3SS8xdWk2M2c4Tkgxb25HSU9rQUZ2Mm11ek5HbllVenA5VVh2Yz07DQogICAgICAgIGI9TkNyWEFBMUU0d2pWd1RVMzFScUNIZm84dUFWbHNlTFcxcy92bUhDcTBqSE9aRzhnN0RJOG5YRS8zK2MwZVV3UW5QDQogICAgICAgICA5QjNIT3ZCQS9IN3NwVzlyZi9VOUZxUndwcHd5QXE5bXQ1NWI1blpzdk5pNVRTR3hiZ0dEbUczbUxvcUNZR3lRZmZ2UA0KICAgICAgICAgNFQ3TnlSS2htbXNUYXVKZmdKRmVIUGg2VXBMeW9icWFpcnFUZWlKdDZJVUh0dXpzMFVwTzQvLy9LdXJQMUpoZUtQc0MNCiAgICAgICAgIE0yVWVrSlZXRmUrS25UaTNLckt1TnQzYURnN3BDYkZnUjV6bzZjcWU2SnlpM3RPZEdpbUNHbGtwanFheHl6Zm1OZC9RDQogICAgICAgICBhQ2dIQ0NLUmZhWkdTRkNOU2pNd0Z2TEQ0TCtFeWw1L1hEd3lyTGtidXpWQ0lzM1kwVlhBMzhwaEVTK29UREhBUEFEMQ0KICAgICAgICAgSmpQQT09DQpYLUdtLU1lc3NhZ2UtU3RhdGU6IEFPQU01MzFnTmgvWmJJQnROYVNicG1zVElyQ3hHeW5DNXVnMHFtcHlrdWhPMmxHbVNaZWtXa21CDQoJT2l5bHYvY0RjR0lrNnhScnZiNzQrQXplejRvT1ZWU0p2c0lWbDltbmZMTUo5aGYxaVE9PQ0KWC1Hb29nbGUtU210cC1Tb3VyY2U6IEFCZGhQSnlMcSsyRkNmRUtJM3U4N0Z3OG5vbTV3SjdMbWhGQUNBdlFRQ3VrVFBFSmJaWFF1MmZCTkI3Zks3SkFUME9RNDJIYllNN3p0engrN3FXZkV0aHQydzg9DQpYLVJlY2VpdmVkOiBieSAyMDAyOmE1ZDo1MjI4OjA6YjA6MjBhOmQ3ZTk6N2VkOCB3aXRoIFNNVFAgaWQNCiBpOC0yMDAyMGE1ZDUyMjgwMDAwMDBiMDAyMGFkN2U5N2VkOG1yMjI4MTY0Nzl3cmEuNjg3LjE2NTIyODI0ODYxMTY7IFdlZCwgMTENCiBNYXkgMjAyMiAwODoyMToyNiAtMDcwMCAoUERUKQ0KUmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0IG5hbWVkIHVua25vd24gYnkgZ21haWxhcGkuZ29vZ2xlLmNvbSB3aXRoDQogSFRUUFJFU1Q7IFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjUgLTA3MDANCk9wZW5wZ3A6IGlkPUI5OEE0RDI2N0Y4MjRDQ0MNCkZyb206IERtaXRyeSBhdCBGbG93Q3J5cHQgPGRtaXRyeUBmbG93Y3J5cHQuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjUgLTA3MDANCk1lc3NhZ2UtSUQ6IDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBuZXcgbWVzc2FnZSBmb3IgcmVwbHkNClRvOiBlMmUgZW50ZXJwcmlzZSB0ZXN0IGF0IEZsb3dDcnlwdCA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPiwgZGVtb0BmbG93Y3J5cHQuY29tDQpDYzogcm9ib3RAZmxvd2NyeXB0LmNvbQ0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJVVEYtOCINCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjIuNw0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQXo4aXQvZ2VRMmNJdlpIWVA1cWlpOU43TFpVVVRHQW4vdllmUGhEQzQNCm1VZ3docXZXcUZHbEwzZWluVmdscWdhVjFBL3RDWFFvbFh3UEgrT0JpYjVEdmdURXJkanorK2N6QWZoTg0KaW4wenVOWFh3VjREWmo0K3BvRzdLWjRTQVFkQVFsWVNVSnQwbTFUcjd3WWVJU0RBWTlneThCQndONTR5DQorTm53TzJRcHpIVXdBNXg2Z05vZmMrNUZKS3NUZ1hGS2FnZjdCSXVobnJKdEJOcnloM1NDYmdQa0oxRVENCkRtSWxCRXRNUHAzMG1PQ0h3Y0ZNQTNpRU8vdWJteHRaQVEvL1lvV0NLSmdEZzU4REc0ZUh2Y3NrVkJ3Lw0KQVVGd29ybUMxUHhhWHZqUDZCYzlVN3R4TU5pNWpTZ3dtOEpZQ3MzL0JFQ3FDRkFEditOb2ZsTGJoMEZODQo2eXhTTUxzampvc1cwcEN3N2xuYlFvb0U5dHB5NnRSanFDa25EZDJ5dmRwTUhnUE5TM2pacGZ6dTROd2UNCnZtUWdKbWVKcCtUWmlOZmM3d25aLzR3c0t3UmE5b3d3QmI2Zm12SUR3YmpVNUZrT29sNHU5d2NxUndHNg0KdngvSVZSZ0x5ZHVkNkFtM05adlBiSy8zUFg4REx5TkQyTnFkQjl6K2luM0NrQ2NUUFlNdWxUMThtRlBDDQpPWkNZWjdNZExDd3V3U0pBdUlhZC9YTkdvc09mWGtEc1dCOVhyNjFVdGhQNmpnbzFORzlaTHgydXVzQzYNClluMFpHajhXeFZVbkgzWnJBaVhVTlJqemxFUTAzbFlWcklWUmVQbG9XSm9mY1ErL3llNjhqN0srOEZRcA0KYWdoTEFtdWt5d0o5bVRMeU1PVjMrU3pjVDdoazd3Ym1KTXprVzZtUDV5ekRLbE5XbUVwMEJCWjBodzREDQptTXRhKzlzbEFTbjY0WTY3ejNaRFFwUktDOWh5dnQ0Znh4VFh0V0JXWXBiRFBJTlVrdHV4ZndqSG0vZzINCjA4RFpCMEdEeDgwUk1nenhUQlhuOWlTSndEa29WWGxoQWtCbC9aZFg2dytaOGlSYW56SktoN2s1aUF2Rw0KRzk1UXBDWkFnYXlOcVBHWjlFbDVtU2pqUnpSZGlxenhZbStkNjI3QmI4RVdFZVN6TE82VXBqSEwvMzhJDQpXbnl4aGIwb1ZSUVh2bFZEcWZOWDlyRlF5UGNHWVdkenVINDlFSVBTOXdMNFE0RklaWHZBYkhTckJ3VEINClhnTlZEcnpCUXVKckR4SUJCMEQrOU9PTjRQdXdDWWY5YkNtZ29xeEVHTXpFQzM2L1k0V1gvNTFlamNkcg0KUURCTGtkQVlpajdGZGtTNjVwYzVxQUcrK3FyaHNSRnJxTWxWclRnS1RVVEd2YTQzZjMwRlNXaGVZQVhEDQo1NDRpU0VuQlhnUDMzWXd2dnFtTmRSSUJCMEFtL3AzM0YxeTI1MEZ0S0NSR2VWd0xFQVRwdWdnSEFQZWwNCmFZWEk2VEJvQlRDOWNWY2dYZXFuU25KL3pJVnhKQTNwZmJQME9qeVZGL2FRNUhGN3RGeFBacGY3c0hpLw0KMUNCaFNVNDVYUnJqSzVMU3dDd0JvUkwvLzQrcEpYZzJpS24rUTkwN2RwMjNJU3lDUkpiVzVYZ3VQZFlJDQprbFQ0ME5zK281SkkrU1I5Y3FPa3hmNlFMK3pBTWVOSW5IWXNzdHlYOEdVb21EaVdTYUlYamVSWGF1OUwNCnpNdGxncnpOcFhyUWJsRWcvVW9TbkUxY3hBNTZuNGRLMmM2bGVjVmhDVFFtS0RVcDBMODZzMW1UblRIMQ0KYmV4N1dsYmRkNEwvZm5xREsweGZqOWw5U05MSFppUGplK1piUktlMEN4VXBnM0trTGxET1U1bXFTbFliDQpQRS9kdXZhMUF3SU9QUFBzZzBLdjFRalR4SFFlSDhVUG9Ec2MybVNzdVZkYUFObFN2NUxvR1IvSWZOZnENCnBvU2NuTjdxMkVSOXhwZ0lpUWdBQS9RckNOSVNYcWZyMFE9PQ0KPUE1RXoNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg==", + "historyId": "190359", + "internalDate": "1652282485000" + } +} \ No newline at end of file diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json index ab23db653..22bf3af27 100644 --- a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json +++ b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json @@ -4,7 +4,7 @@ "id": "180b3b8f20d6cbd5", "threadId": "180b3b5efbdc46da", "labelIds": [ - "INBOX" + "SENT" ], "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", "payload": { @@ -18,11 +18,11 @@ }, { "name": "To", - "value": "e2e.enterprise.test@flowcrypt.com" + "value": "Demo key 2 , Dmitry at FlowCrypt " }, { "name": "From", - "value": "dmitry@flowcrypt.com" + "value": "e2e.enterprise.test@flowcrypt.com" }, { "name": "Subject", diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json deleted file mode 100644 index 259b0d994..000000000 --- a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "acctEmail": "e2e.enterprise.test@flowcrypt.com", - "full": { - "id": "180b3b8f20d6cbd5", - "threadId": "180b3b5efbdc46da", - "labelIds": [ - "SENT" - ], - "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", - "payload": { - "partId": "", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"----sinikael-?=_1-16522826838370.6208943736908692\"" - }, - { - "name": "To", - "value": "Demo key 2 , Dmitry at FlowCrypt " - }, - { - "name": "From", - "value": "e2e.enterprise.test@flowcrypt.com" - }, - { - "name": "Subject", - "value": "new message for reply" - }, - { - "name": "Cc", - "value": "FlowCrypt Robot " - }, - { - "name": "In-Reply-To", - "value": "" - }, - { - "name": "References", - "value": "" - }, - { - "name": "Date", - "value": "Wed, 11 May 2022 08:24:44 -0700" - }, - { - "name": "MIME-Version", - "value": "1.0" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0", - "mimeType": "text/plain", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "text/plain" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - } - ], - "body": { - "size": 1698, - "data": "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgaU9TIDAuMiBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndWNERUMlpsU21oWjFHb1NBUWRBSytacE52T3dSSERYaGcvM0JSeDFiVWpicTNIQURaK2x6ZGxSZVZEQg0KSzE0d2FDUlNSZHkzS1ZYMW9CaEIvNHgrbEo4U1dOQzE1Vy9VTUFiUW5BcFNSQ1lGbWttamVXekNseG4yDQovK0x6dHhuYXdWNERaajQrcG9HN0taNFNBUWRBNTZjMEtmK2o3WGJXaXJaV1FzRUczaXlWMnU5djl4QUoNClhjRmROamZGSW1Nd2pNcWt0RkdNc3F3WmhKS0FueStIUitDTzF5SzJQcTJUZnBmWkJnOW9GQXdSLzd4eA0KdC93S280SzBCK3owZHdOdndjRk1BM2lFTy91Ym14dFpBUS8vWkZ4bTBPRG9qRTJrZG1JNjhTQVJydHAwDQpwblA0UUxKV2tOZDZQaDE5OEMwUEFESk9nb2NFQmVMWEdFeDJXUWlQMG5UcEZ0TnRaNDhzQ0NVNUw1T2QNCmhzSzBLdGxTTFBVRmdLSjBWbEl1K1FLOURxcU8yTU1lb2h2dGFxMGxkS2pETXh4bFVBS1krTnQxN2dwNw0KVmRvYmZ5UDRKZ2tEMnEzdGdpaGNLTDR6cWNMclQraExZay9lcEZlSWR3b2RDWUhtWDVhOVZHdDhobm1XDQpLa21PZ3Jra29ieEtWRVFPZVBEckF2am42RUN0eHZlK0lta2RkMEsvSXRnemQ5aG9XMkFhakl1MUVxdUMNCmdHblJHdGppbEFwNUZzcEpMbDRneHFvZHgrQ2lPeE1jdWhHS0EzZ3NlMFoxVjFYU2RDazFqUitkNUlhTA0KVmF1OHAvK2g2MGVkVmdtakRRYndEUEdBb1RoR0dWRmMvYlFiSVovMTNRZHF0aGFmN1RVeVJaWFUxUFZoDQpxVExxWDYxdXF6QktsZ3RFOXVaK1daWmdmSVdsRzdwanFQWjJ4c01sekVqTzRaT3hNNTZTYTlpWHA1UlINCkpHZ3l3Z002OU9HRkxPMzlMNlVRWXZzNjRmZnBlY1NnT0tmQlRRN3NUZ1pFWWErLzA2aGluWXBrT1RjeA0KTmpmZzVOcUttU1lwUzZ3YUhMVU14MmU3MDNxRHd6WDU4Qmh5SklyRi9rSVRwSzdjNVVPNXMrTk9ycjVYDQo4aHgzeTdIWDBrWkRvT1pwTFNjLzhHaTZLSCtDUFlkc2N4MFJZNEE5Y1g4MUNnd2lYQU90NFdrOFRXS00NCm4yZUpsdGxsNjZ5N0ZxZGhLZ0xDMVdqK21rWk0zVXY1eElDeW9FMmxRTVZNTk5zTmRiMHJjN1hOYmEvQg0KWGdQMzNZd3Z2cW1OZFJJQkIwRCtyRnZLTWc1ZEhiMW5IQWFjV0NhZ2VGekFERUM4a1NjVW5wa0hNQjVMDQpXREEwWlBEdzB0SFlDTk5JeWh0YWZrTFdjT2ZMNCtyNzhkRWhPemRXRm83eXJVQytUUmRxenNaNjkzeUsNCnpNMjhIc3JCWGdOVkRyekJRdUpyRHhJQkIwQU1zbkc2eERqYmIyNENwTzhuQ2tITGZXSTF0RUFGVTRydQ0KdWdkUWR3TWRDREJLUnJEalF3Y0twd3FIUVJac0ZMRzg5VGNWOVZvVEp2dUlscEFmQWxCQTRPMXI3cUxODQpZYXVrU28zU05hVmJDVUxTd0FVQlBuSkhEQlBHUSt6NHdsdTlLa1FzNjVtRmhMVjhxUGo2NnE2ViszOGcNCjFSMHNPeFVjRUd4QXBYanBQamRqSVhsWmd5RTlhSDYyaFk3SXpweXNMZUxLSHFlOFZNeVFUNEp1UElPNA0KNXBPdUJJV1I5K0hEbVVVbGJ5dHkvcUtrWUV3dVVuUXZUb1RaSUZLQXRheU51VEhxN1FlMXA3ZURhR2FvDQpLdFFHWUNSUkdyRlJZamsyb3BDMUJMWUVBS08yVFdKdUk4anFzdlhLVWdmRFNUdXZXdnlvS0RvbTQxTjQNCnYxZVFuN1JyejM1ejV5OG1yZ0MwMlhaclpSTHg2cWhtaFgyNGhPKzhUdz09DQo9UE8yTw0KLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQ0K" - } - } - ] - }, - "sizeEstimate": 2613, - "historyId": "175344", - "internalDate": "1652282684000" - }, - "attachments": {}, - "raw": { - "id": "180b3b8f20d6cbd5", - "threadId": "180b3b5efbdc46da", - "labelIds": [ - "SENT" - ], - "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", - "sizeEstimate": 2613, - "raw": "UmVjZWl2ZWQ6IGZyb20gNjc5MzI2NzEzNDg3DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVdlZCwgMTEgTWF5IDIwMjIgMDg6MjQ6NDQgLTA3MDANCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tc2luaWthZWwtPz1fMS0xNjUyMjgyNjgzODM3MC42MjA4OTQzNzM2OTA4NjkyIg0KVG86IERlbW8ga2V5IDIgPGRlbW9AZmxvd2NyeXB0LmNvbT4sIERtaXRyeSBhdCBGbG93Q3J5cHQNCiA8ZG1pdHJ5QGZsb3djcnlwdC5jb20-DQpGcm9tOiBlMmUuZW50ZXJwcmlzZS50ZXN0QGZsb3djcnlwdC5jb20NClN1YmplY3Q6IG5ldyBtZXNzYWdlIGZvciByZXBseQ0KQ2M6IEZsb3dDcnlwdCBSb2JvdCA8cm9ib3RAZmxvd2NyeXB0LmNvbT4NCkluLVJlcGx5LVRvOg0KIDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpSZWZlcmVuY2VzOg0KIDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpEYXRlOiBXZWQsIDExIE1heSAyMDIyIDA4OjI0OjQ0IC0wNzAwDQpNZXNzYWdlLUlkOiA8Q0FHS2tKVVlYS3VvOGVxRXRwNFViQXZibkV3T3V0RTV1VStvc2phWktOdENDUVhLdEFRQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNjUyMjgyNjgzODM3MC42MjA4OTQzNzM2OTA4NjkyDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgaU9TIDAuMiBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndWNERUMlpsU21oWjFHb1NBUWRBSytacE52T3dSSERYaGcvM0JSeDFiVWpicTNIQURaK2x6ZGxSZVZEQg0KSzE0d2FDUlNSZHkzS1ZYMW9CaEIvNHgrbEo4U1dOQzE1Vy9VTUFiUW5BcFNSQ1lGbWttamVXekNseG4yDQovK0x6dHhuYXdWNERaajQrcG9HN0taNFNBUWRBNTZjMEtmK2o3WGJXaXJaV1FzRUczaXlWMnU5djl4QUoNClhjRmROamZGSW1Nd2pNcWt0RkdNc3F3WmhKS0FueStIUitDTzF5SzJQcTJUZnBmWkJnOW9GQXdSLzd4eA0KdC93S280SzBCK3owZHdOdndjRk1BM2lFTy91Ym14dFpBUS8vWkZ4bTBPRG9qRTJrZG1JNjhTQVJydHAwDQpwblA0UUxKV2tOZDZQaDE5OEMwUEFESk9nb2NFQmVMWEdFeDJXUWlQMG5UcEZ0TnRaNDhzQ0NVNUw1T2QNCmhzSzBLdGxTTFBVRmdLSjBWbEl1K1FLOURxcU8yTU1lb2h2dGFxMGxkS2pETXh4bFVBS1krTnQxN2dwNw0KVmRvYmZ5UDRKZ2tEMnEzdGdpaGNLTDR6cWNMclQraExZay9lcEZlSWR3b2RDWUhtWDVhOVZHdDhobm1XDQpLa21PZ3Jra29ieEtWRVFPZVBEckF2am42RUN0eHZlK0lta2RkMEsvSXRnemQ5aG9XMkFhakl1MUVxdUMNCmdHblJHdGppbEFwNUZzcEpMbDRneHFvZHgrQ2lPeE1jdWhHS0EzZ3NlMFoxVjFYU2RDazFqUitkNUlhTA0KVmF1OHAvK2g2MGVkVmdtakRRYndEUEdBb1RoR0dWRmMvYlFiSVovMTNRZHF0aGFmN1RVeVJaWFUxUFZoDQpxVExxWDYxdXF6QktsZ3RFOXVaK1daWmdmSVdsRzdwanFQWjJ4c01sekVqTzRaT3hNNTZTYTlpWHA1UlINCkpHZ3l3Z002OU9HRkxPMzlMNlVRWXZzNjRmZnBlY1NnT0tmQlRRN3NUZ1pFWWErLzA2aGluWXBrT1RjeA0KTmpmZzVOcUttU1lwUzZ3YUhMVU14MmU3MDNxRHd6WDU4Qmh5SklyRi9rSVRwSzdjNVVPNXMrTk9ycjVYDQo4aHgzeTdIWDBrWkRvT1pwTFNjLzhHaTZLSCtDUFlkc2N4MFJZNEE5Y1g4MUNnd2lYQU90NFdrOFRXS00NCm4yZUpsdGxsNjZ5N0ZxZGhLZ0xDMVdqK21rWk0zVXY1eElDeW9FMmxRTVZNTk5zTmRiMHJjN1hOYmEvQg0KWGdQMzNZd3Z2cW1OZFJJQkIwRCtyRnZLTWc1ZEhiMW5IQWFjV0NhZ2VGekFERUM4a1NjVW5wa0hNQjVMDQpXREEwWlBEdzB0SFlDTk5JeWh0YWZrTFdjT2ZMNCtyNzhkRWhPemRXRm83eXJVQytUUmRxenNaNjkzeUsNCnpNMjhIc3JCWGdOVkRyekJRdUpyRHhJQkIwQU1zbkc2eERqYmIyNENwTzhuQ2tITGZXSTF0RUFGVTRydQ0KdWdkUWR3TWRDREJLUnJEalF3Y0twd3FIUVJac0ZMRzg5VGNWOVZvVEp2dUlscEFmQWxCQTRPMXI3cUxODQpZYXVrU28zU05hVmJDVUxTd0FVQlBuSkhEQlBHUSt6NHdsdTlLa1FzNjVtRmhMVjhxUGo2NnE2ViszOGcNCjFSMHNPeFVjRUd4QXBYanBQamRqSVhsWmd5RTlhSDYyaFk3SXpweXNMZUxLSHFlOFZNeVFUNEp1UElPNA0KNXBPdUJJV1I5K0hEbVVVbGJ5dHkvcUtrWUV3dVVuUXZUb1RaSUZLQXRheU51VEhxN1FlMXA3ZURhR2FvDQpLdFFHWUNSUkdyRlJZamsyb3BDMUJMWUVBS08yVFdKdUk4anFzdlhLVWdmRFNUdXZXdnlvS0RvbTQxTjQNCnYxZVFuN1JyejM1ejV5OG1yZ0MwMlhaclpSTHg2cWhtaFgyNGhPKzhUdz0zRD0zRA0KPTNEUE8yTw0KLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQ0KDQotLS0tLS1zaW5pa2FlbC0_PV8xLTE2NTIyODI2ODM4MzcwLjYyMDg5NDM3MzY5MDg2OTItLQ0K", - "historyId": "175344", - "internalDate": "1652282684000" - } -} \ No newline at end of file diff --git a/appium/api-mocks/apis/google/google-data.ts b/appium/api-mocks/apis/google/google-data.ts index 000f49f18..8f0a35a46 100644 --- a/appium/api-mocks/apis/google/google-data.ts +++ b/appium/api-mocks/apis/google/google-data.ts @@ -25,16 +25,18 @@ export class GmailMsg { public historyId: string; public sizeEstimate?: number; public threadId: string | null; + public draftId?: string | null; public payload?: GmailMsg$payload; public internalDate?: number | string; public labelIds?: GmailMsg$labelId[]; public snippet?: string; public raw?: string; - constructor(msg: { id: string, labelIds?: GmailMsg$labelId[], raw: string, payload?: GmailMsg$payload, mimeMsg: ParsedMail, threadId?: string | null }) { + constructor(msg: { id: string, labelIds?: GmailMsg$labelId[], raw: string, payload?: GmailMsg$payload, mimeMsg: ParsedMail, threadId?: string | null, draftId?: string | null }) { this.id = msg.id; this.historyId = msg.id; this.threadId = msg.threadId ?? msg.id; + this.draftId = msg.draftId; this.labelIds = msg.labelIds; this.raw = msg.raw; this.sizeEstimate = Buffer.byteLength(msg.raw, "utf-8"); @@ -210,6 +212,11 @@ export class GoogleData { return (subjectHeader && subjectHeader.value) || ''; }; + private static msgId = (m: GmailMsg): string => { + const msgIdHeader = m.payload && m.payload.headers && m.payload.headers.find(h => h.name.toLowerCase() === 'message-id'); + return (msgIdHeader && msgIdHeader.value) || ''; + }; + private static parseAcctMessages = async (acct: GoogleMockAccountEmail, config?: GoogleConfig) => { if (config?.accounts[acct]?.messages) { const dir = GoogleData.exportedMsgsPath; @@ -269,7 +276,7 @@ export class GoogleData { } public getMessage = (id: string): GmailMsg | undefined => { - return DATA[this.acct].messages.find(m => m.id === id); + return this.getMessagesAndDrafts().find(m => m.id === id); }; public getMessageBySubject = (subject: string): GmailMsg | undefined => { @@ -311,10 +318,14 @@ export class GoogleData { public getMessages = (labelIds: string[] = [], query?: string) => { const subject = (query?.match(/subject: '([^"]+)'/) || [])[1]?.trim().toLowerCase(); + const rfc822Msgid = (query?.match(/rfc822msgid:([^"]+)/) || [])[1]?.trim(); return DATA[this.acct].messages.filter(m => { if (subject && !GoogleData.msgSubject(m).toLowerCase().includes(subject)) { return false; } + if (rfc822Msgid && GoogleData.msgId(m) !== rfc822Msgid) { + return false; + } if (labelIds && !m.labelIds?.some(l => labelIds.includes(l))) { return false; } @@ -331,6 +342,7 @@ export class GoogleData { const rawBase64 = Buffer.from(decodedRaw).toString('base64'); const msg = new GmailMsg({ labelIds: ['SENT'], id, raw: rawBase64, mimeMsg }); + DATA[this.acct].messages = DATA[this.acct].messages.filter(m => GoogleData.msgId(m) === mimeMsg.messageId); DATA[this.acct].messages.unshift(msg); }; @@ -342,18 +354,27 @@ export class GoogleData { DATA[this.acct].messages = DATA[this.acct].messages.filter(m => !ids.includes(m.id)); } - public addDraft = (id: string, raw: string, mimeMsg: ParsedMail) => { - const draft = new GmailMsg({ labelIds: ['DRAFT'], id, raw, mimeMsg }); - const index = DATA[this.acct].drafts.findIndex(d => d.id === draft.id); + public deleteDraft = (id: string) => { + DATA[this.acct].messages = DATA[this.acct].messages.filter(m => id !== m.draftId); + } + + public addDraft = (raw: string, mimeMsg: ParsedMail, id?: string, threadId?: string) => { + const draftId = id ?? `draft_id_${lousyRandom()}`; + const msgId = `msg_id_${lousyRandom()}`; + const draft = new GmailMsg({ labelIds: ['DRAFT'], id: msgId, raw, mimeMsg, threadId: threadId, draftId: draftId }); + const index = DATA[this.acct].messages.findIndex(d => d.draftId === draftId); + if (index === -1) { - DATA[this.acct].drafts.push(draft); + DATA[this.acct].messages.push(draft); } else { - DATA[this.acct].drafts[index] = draft; + DATA[this.acct].messages[index] = draft; } + + return draft; }; public getDraft = (id: string): GmailMsg | undefined => { - return DATA[this.acct].drafts.find(d => d.id === id); + return DATA[this.acct].messages.find(d => d.draftId === id); }; public getAttachment = (attachmentId: string) => { diff --git a/appium/api-mocks/apis/google/google-endpoints.ts b/appium/api-mocks/apis/google/google-endpoints.ts index 535c64b11..441208757 100644 --- a/appium/api-mocks/apis/google/google-endpoints.ts +++ b/appium/api-mocks/apis/google/google-endpoints.ts @@ -8,7 +8,7 @@ import { isDelete, isGet, isPost, isPut, parseResourceId } from '../../lib/mock- import { oauth } from '../../lib/oauth'; import { GoogleMockAccountEmail } from './google-messages'; -type DraftSaveModel = { message: { raw: string, threadId: string } }; +type DraftSaveModel = { message: { raw: string, threadId: string }, id?: string }; type LabelsModifyModel = { addLabelIds: string[], removeLabelIds: string[] } interface BatchModifyInterface { ids: string[]; @@ -121,6 +121,11 @@ export const getMockGoogleEndpoints = ( return GoogleData.fmtMsg(msg, parsedReq.query.format); } throw new HttpErr(`MOCK Message not found for ${acct}: ${id}`, Status.NOT_FOUND); + } else if (isDelete(req)) { + const id = parseResourceId(req.url!); + const data = await GoogleData.withInitializedData(acct, googleConfig); + data.deleteMessages([id]); + return {} } throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); }, @@ -148,8 +153,7 @@ export const getMockGoogleEndpoints = ( const id = parseResourceId(req.url!); const msgs = (await GoogleData.withInitializedData(acct, googleConfig)).getMessagesAndDraftsByThread(id); if (!msgs.length) { - const statusCode = id === '16841ce0ce5cb74d' ? 404 : 400; // intentionally testing missing thread - throw new HttpErr(`MOCK thread not found for ${acct}: ${id}`, statusCode); + throw new HttpErr(`MOCK thread not found for ${acct}: ${id}`, 404); } return { id, historyId: msgs[0].historyId, messages: msgs.map(m => GoogleData.fmtMsg(m, parsedReq.query.format)) }; } else if (isPost(req)) { @@ -174,22 +178,34 @@ export const getMockGoogleEndpoints = ( if (isPost(req)) { const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); const body = parsedReq.body as DraftSaveModel; + const data = await GoogleData.withInitializedData(acct, googleConfig); if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { - if (body.message.threadId && !(await GoogleData.withInitializedData(acct, googleConfig)).getThreads().find(t => t.id === body.message.threadId)) { + if (body.message.threadId && !(data.getThreads().find(t => t.id === body.message.threadId))) { throw new HttpErr('The thread you are replying to not found', 404); } const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); - if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { - throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); - } + const draft = data.addDraft(body.message.raw, decoded, undefined, body.message.threadId); + return { - id: 'mockfakedraftsave', message: { - id: 'mockfakedmessageraftsave', + id: draft.draftId, message: { + id: draft.id, labelIds: ['DRAFT'], - threadId: body.message.threadId + threadId: draft.threadId } - }; + } } + } else if (isGet(req)) { + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); + const data = await GoogleData.withInitializedData(acct, googleConfig); + const message = data.getMessages(['DRAFT'], parsedReq.query.q)[0]; + return { + drafts: [ + { + id: message.draftId, + message: message + } + ] + }; } throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); }, @@ -200,25 +216,31 @@ export const getMockGoogleEndpoints = ( const data = (await GoogleData.withInitializedData(acct, googleConfig)); const draft = data.getDraft(id); if (draft) { - return { id: draft.id, message: draft }; + return { id: draft.draftId, message: draft }; } throw new HttpErr(`MOCK draft not found for ${acct} (draftId: ${id})`, Status.NOT_FOUND); } else if (isPut(req)) { - const raw = (parsedReq.body as { message?: { raw?: string } })?.message?.raw; + const body = parsedReq.body as DraftSaveModel; + const raw = body.message?.raw; if (!raw) { throw new Error('mock Draft PUT without raw data'); } - const mimeMsg = await Parse.convertBase64ToMimeMsg(raw); - if ((mimeMsg.subject || '').includes('saving and rendering a draft with image')) { - const data = (await GoogleData.withInitializedData(acct, googleConfig)); - data.addDraft('draft_with_image', raw, mimeMsg); - } - if ((mimeMsg.subject || '').includes('RTL')) { - const data = await GoogleData.withInitializedData(acct, googleConfig); - data.addDraft(`draft_with_rtl_text_${mimeMsg.subject?.includes('rich text') ? 'rich' : 'plain'}`, raw, mimeMsg); + const decoded = await Parse.convertBase64ToMimeMsg(raw); + + const data = (await GoogleData.withInitializedData(acct, googleConfig)); + const draft = data.addDraft(raw, decoded, body.id, body.message?.threadId); + + return { + id: draft.draftId, message: { + id: draft.id, + labelIds: ['DRAFT'], + threadId: draft.threadId + } } - return {}; } else if (isDelete(req)) { + const id = parseResourceId(req.url!); + const data = (await GoogleData.withInitializedData(acct, googleConfig)); + data.deleteDraft(id); return {}; } throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); diff --git a/appium/package.json b/appium/package.json index 362c478fe..1339b4c67 100644 --- a/appium/package.json +++ b/appium/package.json @@ -30,7 +30,7 @@ "@wdio/junit-reporter": "^7.16.15", "@wdio/local-runner": "^7.16.15", "@wdio/spec-reporter": "^7.16.14", - "appium": "^2.0.0-beta.44", + "appium": "^2.0.0-beta.46", "appium-xcuitest-driver": "^4.12.1", "dotenv": "^16.0.0", "eslint-plugin-node": "^11.1.0", diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 9140882a6..a177e01bb 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -46,6 +46,13 @@ export const CommonData = { secondDate: 'Feb 07', thirdDate: 'Feb 08', }, + draft: { + subject1: 'Draft subject', + subject2: 'Subject for another draft', + text1: 'Draft text', + updatedText1: 'Some new text', + text2: 'Another draft' + }, archivedThread: { subject: 'Archived thread' }, diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index 2cc9d2530..dc9f613d8 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -292,6 +292,26 @@ class EmailScreen extends BaseScreen { const text = await el.getText(); expect(text.includes(value)).toBeTrue(); } + + draftBody = async (index: number) => { + return $(`~aid-draft-body-${index}`); + } + + checkDraft = async (text: string, index: number) => { + await browser.pause(500); + const draftBodyEl = await this.draftBody(index); + expect(await draftBodyEl.getValue()).toEqual(text); + } + + openDraft = async (index: number) => { + await ElementHelper.waitAndClick(await this.draftBody(index)); + } + + deleteDraft = async (index: number) => { + await ElementHelper.waitAndClick(await $(`~aid-draft-delete-button-${index}`)); + await this.confirmDelete(); + await ElementHelper.waitElementInvisible(await this.draftBody(index)); + } } export default new EmailScreen(); diff --git a/appium/tests/screenobjects/mail-folder.screen.ts b/appium/tests/screenobjects/mail-folder.screen.ts index fa4597090..6b39302a5 100644 --- a/appium/tests/screenobjects/mail-folder.screen.ts +++ b/appium/tests/screenobjects/mail-folder.screen.ts @@ -7,6 +7,7 @@ const SELECTORS = { SENT_HEADER: '~aid-navigation-item-sent', CREATE_EMAIL_BUTTON: '~aid-compose-message-button', INBOX_HEADER: '~aid-navigation-item-inbox', + DRAFTS_HEADER: '~aid-navigation-item-drafts', SEARCH_BTN: '~aid-search-btn', HELP_BTN: '~aid-help-btn', SEARCH_FIELD: '~aid-search-all-emails', @@ -14,7 +15,8 @@ const SELECTORS = { // INBOX_ITEM: '~aid-inbox-item', // TODO: Couldn't use accessibility identifier because $$ selector returns only visible cells INBOX_ITEM: '-ios class chain:**/XCUIElementTypeOther/XCUIElementTypeTable[2]/XCUIElementTypeCell', - IDLE_NODE: '~aid-inbox-idle-node' + IDLE_NODE: '~aid-inbox-idle-node', + EMPTY_CELL_NODE: '~aid-empty-cell-node' }; class MailFolderScreen extends BaseScreen { @@ -42,6 +44,10 @@ class MailFolderScreen extends BaseScreen { return $(SELECTORS.INBOX_HEADER) } + get draftsHeader() { + return $(SELECTORS.DRAFTS_HEADER) + } + get createEmailButton() { return $(SELECTORS.CREATE_EMAIL_BUTTON); } @@ -62,6 +68,10 @@ class MailFolderScreen extends BaseScreen { return $(SELECTORS.IDLE_NODE); } + get emptyCellNode() { + return $(SELECTORS.EMPTY_CELL_NODE); + } + checkTrashScreen = async () => { await ElementHelper.waitElementVisible(await this.trashHeader); await ElementHelper.waitElementVisible(await this.searchBtn); @@ -112,10 +122,10 @@ class MailFolderScreen extends BaseScreen { await TouchHelper.scrollDownToElement(await elem); }; - getEmailCount = async () => { + checkEmailCount = async (expectedCount: number) => { await ElementHelper.waitElementInvisible(await this.idleNode); await browser.pause(1000); - return await this.inboxList.length; + expect(await this.inboxList.length).toEqual(expectedCount); }; scrollUpToFirstEmail = async () => { @@ -131,11 +141,21 @@ class MailFolderScreen extends BaseScreen { await ElementHelper.waitElementVisible(await this.helpBtn); } + checkDraftsScreen = async () => { + await ElementHelper.waitElementVisible(await this.draftsHeader); + await ElementHelper.waitElementVisible(await this.searchBtn); + await ElementHelper.waitElementVisible(await this.helpBtn); + } + + checkIfFolderIsEmpty = async () => { + await ElementHelper.waitElementVisible(await this.emptyCellNode); + } + emptyFolder = async () => { await ElementHelper.waitAndClick(await this.emptyFolderBtn); await BaseScreen.clickConfirmButton(); // Give some time to delete messages - await browser.pause(3000); + await browser.pause(500); } clickSearchButton = async () => { diff --git a/appium/tests/screenobjects/menu-bar.screen.ts b/appium/tests/screenobjects/menu-bar.screen.ts index 095739e63..78312aae1 100644 --- a/appium/tests/screenobjects/menu-bar.screen.ts +++ b/appium/tests/screenobjects/menu-bar.screen.ts @@ -9,6 +9,7 @@ const SELECTORS = { INBOX_BTN: '~aid-menu-bar-item-inbox', SENT_BTN: '~aid-menu-bar-item-sent', TRASH_BTN: '~aid-menu-bar-item-trash', + DRAFTS_BTN: '~aid-menu-bar-item-drafts', ALL_MAIL_BTN: '~aid-menu-bar-item-all-mail', ADD_ACCOUNT_BUTTON: '~aid-add-account-btn' }; @@ -42,6 +43,10 @@ class MenuBarScreen extends BaseScreen { return $(SELECTORS.TRASH_BTN) } + get draftsButton() { + return $(SELECTORS.DRAFTS_BTN) + } + get allMailButton() { return $(SELECTORS.ALL_MAIL_BTN) } @@ -97,6 +102,10 @@ class MenuBarScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.trashButton); } + clickDraftsButton = async () => { + await ElementHelper.waitAndClick(await this.draftsButton); + } + clickAllMailButton = async () => { await ElementHelper.waitAndClick(await this.allMailButton); } diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index fafefae7e..a4d4b994e 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -18,8 +18,10 @@ const SELECTORS = { SET_PASSWORD_BUTTON: '~Set', CANCEL_BUTTON: '~Cancel', BACK_BUTTON: '~aid-back-button', + DELETE_BUTTON: '~aid-compose-delete', SEND_BUTTON: '~aid-compose-send', - MESSAGE_PASSWORD_MODAL: '~aid-message-password-modal', + CONFIRM_DELETING: '~Delete', + MESSAGE_PASSPHRASE_TEXTFIELD: '~aid-message-passphrase-textfield', MESSAGE_PASSWORD_TEXTFIELD: '~aid-message-password-textfield', ALERT: "-ios predicate string:type == 'XCUIElementTypeAlert'", RECIPIENT_POPUP_EMAIL_NODE: '~aid-recipient-popup-email-node', @@ -84,22 +86,30 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.BACK_BUTTON); } + get deleteButton() { + return $(SELECTORS.DELETE_BUTTON); + } + get sendButton() { return $(SELECTORS.SEND_BUTTON); } - get passwordCell() { - return $(SELECTORS.PASSWORD_CELL); + get confirmDeletingButton() { + return $(SELECTORS.CONFIRM_DELETING) } - get passwordModal() { - return $(SELECTORS.MESSAGE_PASSWORD_MODAL); + get passwordCell() { + return $(SELECTORS.PASSWORD_CELL); } get currentModal() { return $(SELECTORS.ALERT); } + get passphraseTextField() { + return $(SELECTORS.MESSAGE_PASSPHRASE_TEXTFIELD); + } + get passwordTextField() { return $(SELECTORS.MESSAGE_PASSWORD_TEXTFIELD); } @@ -364,10 +374,18 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.backButton); } + clickDeleteButton = async () => { + await ElementHelper.waitAndClick(await this.deleteButton); + } + clickSendButton = async () => { await ElementHelper.waitAndClick(await this.sendButton); } + confirmDelete = async () => { + await ElementHelper.waitAndClick(await this.confirmDeletingButton); + } + clickToggleRecipientsButton = async () => { await browser.pause(500); await ElementHelper.waitAndClick(await this.toggleRecipientsButton); @@ -400,6 +418,15 @@ class NewMessageScreen extends BaseScreen { await (await this.passwordTextField).setValue(password); await this.clickSetPasswordButton(); } + + clickComposeMessage = async () => { + await ElementHelper.waitAndClick(await this.composeSecurityMessage); + } + + addMessageText = async (text: string) => { + const messageEl = await this.composeSecurityMessage; + await messageEl.sendKeys([text]); + } } export default new NewMessageScreen(); diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts new file mode 100644 index 000000000..1ec8366dd --- /dev/null +++ b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts @@ -0,0 +1,128 @@ +import { MockApi } from 'api-mocks/mock'; +import { MockApiConfig } from 'api-mocks/mock-config'; +import { MockUserList } from 'api-mocks/mock-data'; +import { CommonData } from 'tests/data'; +import { + EmailScreen, + MailFolderScreen, MenuBarScreen, NewMessageScreen, + SetupKeyScreen, + SplashScreen +} from '../../../screenobjects/all-screens'; + +describe('COMPOSE EMAIL: ', () => { + + it('check drafts functionality', async () => { + const mockApi = new MockApi(); + + const recipient = MockUserList.dmitry; + const recipientWithoutPubKeys = MockUserList.demo; + const subject = CommonData.simpleEmail.subject; + const draftSubject1 = CommonData.draft.subject1; + const draftSubject2 = CommonData.draft.subject2; + const draftText1 = CommonData.draft.text1; + const updatedDraftText = CommonData.draft.updatedText1; + const draftText2 = CommonData.draft.text2; + + mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; + mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { + messages: ['Test 1'], + }); + mockApi.attesterConfig = { + servedPubkeys: { + [MockUserList.dmitry.email]: MockUserList.dmitry.pub!, + } + }; + + await mockApi.withMockedApis(async () => { + await SplashScreen.mockLogin(); + await SetupKeyScreen.setPassPhrase(); + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickOnEmailBySubject(subject); + + // compose draft as reply to existing thread + await EmailScreen.clickReplyButton(); + await NewMessageScreen.checkMessageFieldFocus(); + await NewMessageScreen.addMessageText(draftText1); + await NewMessageScreen.clickBackButton(); + + // compose another draft + await EmailScreen.clickReplyButton(); + await NewMessageScreen.checkMessageFieldFocus(); + await NewMessageScreen.addMessageText(draftText2); + await NewMessageScreen.clickBackButton(); + + // check if drafts are added to thread messages + await EmailScreen.checkDraft(draftText1, 1); + await EmailScreen.checkDraft(draftText2, 2); + + // update draft and check if changes are applied + await EmailScreen.openDraft(1); + + await NewMessageScreen.setComposeSecurityMessage(updatedDraftText); + await NewMessageScreen.clickBackButton(); + + await EmailScreen.checkDraft(updatedDraftText, 1); + await EmailScreen.checkDraft(draftText2, 2); + + // delete draft from thread screen + await EmailScreen.deleteDraft(1); + await EmailScreen.clickBackButton(); + + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickDraftsButton(); + await MailFolderScreen.checkDraftsScreen(); + + // delete draft from compose screen + await MailFolderScreen.clickOnEmailBySubject(subject); + await EmailScreen.checkDraft(draftText2, 1); + await EmailScreen.openDraft(1); + + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + + await EmailScreen.clickBackButton(); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkIfFolderIsEmpty(); + + // compose 2 new drafts and then delete them both + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipientWithoutPubKeys.email, draftSubject1, draftText1); + await NewMessageScreen.clickBackButton(); + + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipient.email, draftSubject2, draftText2); + await NewMessageScreen.clickBackButton(); + + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkEmailCount(2); + + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject2); + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkIfFolderIsEmpty(); + + // compose draft, send it and check if sent message added to 'sent' folder + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipient.email, draftSubject1, draftText1); + await NewMessageScreen.clickBackButton(); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); + await NewMessageScreen.clickSendButton(); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkIfFolderIsEmpty(); + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickSentButton(); + await MailFolderScreen.checkSentScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); + }); + }); +}); \ No newline at end of file diff --git a/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts b/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts index 652f1234e..2f0f8c220 100644 --- a/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts +++ b/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts @@ -25,7 +25,7 @@ describe('INBOX: ', () => { const replySubject = `Re: ${emailSubject}`; const forwardSubject = `Fwd: ${emailSubject}`; - const quoteText = `${senderEmail} wrote:\n > ${emailText}`; + const quoteText = `${senderName} <${senderEmail}> wrote:\n > ${emailText}`; const mockApi = new MockApi(); diff --git a/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts b/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts index 1d2c44505..d75cbb2d9 100644 --- a/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts +++ b/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts @@ -22,6 +22,7 @@ describe('SETUP: ', () => { } }; mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com') await mockApi.withMockedApis(async () => { await SplashScreen.mockLogin(); diff --git a/package-lock.json b/package-lock.json index 65d68f0dc..6ee04d921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "eslint-plugin-jsdoc": "^39.3.6", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", + "git-format-staged": "^3.0.0", "husky": "^8.0.1", "lint-staged": "^13.0.0" } @@ -1376,6 +1377,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/git-format-staged": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/git-format-staged/-/git-format-staged-3.0.0.tgz", + "integrity": "sha512-cdDJxV06qY8ucBsW/uIFR4PYN/kDHl43nG8yg+VPPaDeLAf8hEPhEIJTeJ+yRClxcDSpoTmDPiFZUoxFx1wPCg==", + "dev": true, + "bin": { + "git-format-staged": "git-format-staged" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4167,6 +4177,12 @@ "get-intrinsic": "^1.1.1" } }, + "git-format-staged": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/git-format-staged/-/git-format-staged-3.0.0.tgz", + "integrity": "sha512-cdDJxV06qY8ucBsW/uIFR4PYN/kDHl43nG8yg+VPPaDeLAf8hEPhEIJTeJ+yRClxcDSpoTmDPiFZUoxFx1wPCg==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", diff --git a/package.json b/package.json index d3b8c2165..a2a4ab44d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "eslint-plugin-jsdoc": "^39.3.6", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", + "git-format-staged": "^3.0.0", "husky": "^8.0.1", "lint-staged": "^13.0.0" },