chore(healthchat): resolve the rebase onto upstream 26.10.3

This commit is contained in:
Sedat Qaja
2026-10-19 09:18:06 +02:00
parent 323970aa93
commit 1b34a71857
19 changed files with 656 additions and 258 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
# than deleted so the file keeps rebasing cleanly. Run locally with # than deleted so the file keeps rebasing cleanly. Run locally with
# `swift run tools ci accessibility-tests`. # `swift run tools ci accessibility-tests`.
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 150 timeout-minutes: 150
concurrency: concurrency:
@@ -36,7 +36,7 @@ jobs:
run: swift run -q tools ci accessibility-tests run: swift run -q tools ci accessibility-tests
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure() if: failure()
with: with:
name: Results name: Results
+8 -8
View File
@@ -25,8 +25,8 @@ jobs:
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather # permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
# than deleted so the file keeps rebasing cleanly. # than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 15 timeout-minutes: 25
concurrency: concurrency:
# When running on develop, use the sha to allow all runs of this workflow to run concurrently. # When running on develop, use the sha to allow all runs of this workflow to run concurrently.
@@ -39,8 +39,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Configure Xcode 26 - name: Configure Xcode 27
run: sudo xcode-select -s /Applications/Xcode_26.5.app run: sudo xcode-select -s /Applications/Xcode_27.app
- name: Install sourcery - name: Install sourcery
run: brew install sourcery run: brew install sourcery
@@ -50,11 +50,11 @@ jobs:
run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml
- name: Create simulator - name: Create simulator
run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-26-5" run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-27-0"
- name: Run tests - name: Run tests
working-directory: compound-ios working-directory: compound-ios
run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=26.5' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=27.0' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions
- name: Zip artifacts - name: Zip artifacts
if: failure() # We only care about artefacts if the tests fail if: failure() # We only care about artefacts if the tests fail
@@ -62,7 +62,7 @@ jobs:
run: zip -r Logs/CompoundTests.xcresult.zip Logs/CompoundTests.xcresult/ run: zip -r Logs/CompoundTests.xcresult.zip Logs/CompoundTests.xcresult/
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure() # We only care about artefacts if the tests fail if: failure() # We only care about artefacts if the tests fail
with: with:
name: test-results name: test-results
@@ -71,7 +71,7 @@ jobs:
if-no-files-found: ignore if-no-files-found: ignore
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
fail_ci_if_error: false fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+4 -4
View File
@@ -18,7 +18,7 @@ jobs:
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI # HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly. # these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 45 timeout-minutes: 45
concurrency: concurrency:
@@ -42,7 +42,7 @@ jobs:
INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }} INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
@@ -56,7 +56,7 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/integration-cobertura.xml files: test_output/integration-cobertura.xml
@@ -68,7 +68,7 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
# UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file # UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file
# keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`. # keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`.
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 90 timeout-minutes: 90
strategy: strategy:
fail-fast: false fail-fast: false
@@ -52,7 +52,7 @@ jobs:
GITHUB_EVENT_INPUTS_TEST_NAME: ${{ github.event.inputs.test_name }} GITHUB_EVENT_INPUTS_TEST_NAME: ${{ github.event.inputs.test_name }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
@@ -64,7 +64,7 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/ui-cobertura.xml files: test_output/ui-cobertura.xml
@@ -76,7 +76,7 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+3 -3
View File
@@ -102,7 +102,7 @@ jobs:
key: compilation-cache-unit-tests-${{ github.sha }} key: compilation-cache-unit-tests-${{ github.sha }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
@@ -118,7 +118,7 @@ jobs:
# runs without a CODECOV_TOKEN — which fails the job via # runs without a CODECOV_TOKEN — which fails the job via
# fail_ci_if_error. Gate on the upstream repo as post-release.yml does. # fail_ci_if_error. Gate on the upstream repo as post-release.yml does.
if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/unit-cobertura.xml,test_output/preview-cobertura.xml files: test_output/unit-cobertura.xml,test_output/preview-cobertura.xml
@@ -132,7 +132,7 @@ jobs:
# HealthChat: gated on the upstream repo for the same reason as the # HealthChat: gated on the upstream repo for the same reason as the
# coverage step above — no CODECOV_TOKEN exists in this fork. # coverage step above — no CODECOV_TOKEN exists in this fork.
if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+4 -14
View File
@@ -18,22 +18,9 @@ DerivedData/
.swiftpm .swiftpm
.netrc .netrc
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
towncrier.toml.tmp
version_changes.md
build build
/vendor/ /vendor/
*/xcshareddata/xcodecloud
## macOS Files ## macOS Files
.DS_Store .DS_Store
@@ -60,3 +47,6 @@ fastlane/README.md
# Review working data (per-machine) # Review working data (per-machine)
.claude/review-context/ .claude/review-context/
.claude/review-log.jsonl .claude/review-log.jsonl
# Local SDK development override
matrix-rust-components-swift
+63
View File
@@ -1,3 +1,66 @@
## Changes in 26.09.2 (2026-09-23)
### What's Changed
🙌 Improvements
* Add an `AccountProvider` type and use it within the authentication flow. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6103
* Update the Start Chat button's icon. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6179
🐛 Bugfixes
* Update the read marker colour to match the Figma. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6146
* Fix some isolation errors when compiling with Swift 6.4. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6148
* Cancel message edit if content remains unchanged by @thepearl in https://github.com/element-hq/element-x-ios/pull/6153
* Fix HDR UI bugs on iOS 27.0. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6177
* Fix the split view layout on iOS 27. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6180
* Only make toolbar buttons prominent when tinted. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6188
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6151
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6164
🚧 In development 🚧
* Introduce a local mechanism for managing the app's badge count by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6135
* Various search tab tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6157
* Add experimental native calls behind a developer flag by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6159
* Bump element-call-ios to 0.1.0-rc.7 by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6168
* Add specific prefix for native call logs by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6169
* Bump element-call native to 0.1.0-rc.8 by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6181
Others
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6136
* Fix some SwiftLint warnings. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6149
* Update zizmorcore/zizmor-action action to v0.6.4 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6155
* Bump the RustSDK to v26.09.17 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6160
* Update codecov/codecov-action action to v7.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6176
* Update dependency siteline/SwiftUI-Introspect to v27 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6175
* Drop Intel device support from the build scripts and project by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6171
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6182
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.09.1...release/26.09.2
## Changes in 26.09.1 (2026-09-10)
### What's Changed
✨ Features
* Add a developer option and selection mode for timeline messages by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6102
🐛 Bugfixes
* Limit the custom status input to 30 characters. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6139
* Scope dark mode change only to the presenting sheet by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6144
🚧 In development 🚧
* Show message counts on room list notification badges by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6132
Others
* Fix start chat UI test snapshots following copy changes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6137
* Add a couple of points about code style/ordering to the AGENTS.md by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6133
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6147
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.09.0...release/26.09.1
## Changes in 26.09.0 (2026-09-08) ## Changes in 26.09.0 (2026-09-08)
### What's Changed ### What's Changed
@@ -1,13 +1,13 @@
{ {
"originHash" : "f1791683f9f09408a7746ce0e163941b3ce7e83e84f1a73b2fecc2b5d2f2f4b6", "originHash" : "fbdb76ae8c78e5a86db4812456fcb4da5795eed4bc8f6715726fec41e87a1e0f",
"pins" : [ "pins" : [
{ {
"identity" : "compound-design-tokens", "identity" : "compound-design-tokens",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens", "location" : "https://github.com/element-hq/compound-design-tokens",
"state" : { "state" : {
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419", "revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
"version" : "11.0.0" "version" : "11.3.1"
} }
}, },
{ {
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/devicekit/DeviceKit", "location" : "https://github.com/devicekit/DeviceKit",
"state" : { "state" : {
"revision" : "56b997e8a61707218f9af09f32b2a1d1806fd792", "revision" : "19528aa07e1426626ea9fb2c4a161edf43c4f9e8",
"version" : "5.8.0" "version" : "5.9.0"
} }
}, },
{ {
@@ -55,13 +55,22 @@
"version" : "1.2.0" "version" : "1.2.0"
} }
}, },
{
"identity" : "element-call-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/element-call-ios",
"state" : {
"revision" : "d77b4ee326582083cabe53ffbd872a751c3cb989",
"version" : "0.1.0-rc.11"
}
},
{ {
"identity" : "element-call-swift", "identity" : "element-call-swift",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/element-call-swift", "location" : "https://github.com/element-hq/element-call-swift",
"state" : { "state" : {
"revision" : "13eac3567461a9825018e10e9f4475f68215dae7", "revision" : "befb5d37a7859ad38cf83719cbfd4abaffa04d82",
"version" : "0.24.0" "version" : "0.26.1"
} }
}, },
{ {
@@ -95,8 +104,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher", "location" : "https://github.com/onevcat/Kingfisher",
"state" : { "state" : {
"revision" : "410984bf301f4fa224fe56277b3f8672cc465c79", "revision" : "8648fe649c5d62be38721110f835821906e9242a",
"version" : "8.11.0" "version" : "8.13.0"
} }
}, },
{ {
@@ -122,8 +131,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/LRUCache", "location" : "https://github.com/nicklockwood/LRUCache",
"state" : { "state" : {
"revision" : "cb5b2bd0da83ad29c0bec762d39f41c8ad0eaf3e", "revision" : "f0570e943ea4445d258113ba73f50cb2b7e564ac",
"version" : "1.2.1" "version" : "1.3.0"
} }
}, },
{ {
@@ -140,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
"state" : { "state" : {
"revision" : "5ee345ca5d65238a6fce29bba87816204be7df20", "revision" : "13e41ab3d77ff5113e7e5d4ee87803b1f81f5683",
"version" : "6.28.0" "version" : "6.31.0"
} }
}, },
{ {
@@ -149,8 +158,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-analytics-events", "location" : "https://github.com/matrix-org/matrix-analytics-events",
"state" : { "state" : {
"revision" : "4b143b14a16a231bd4fc3c6705845f852177abe3", "revision" : "7d668b3e4be493843805b8a1ff4e13b261a19965",
"version" : "0.37.0" "version" : "0.38.0"
} }
}, },
{ {
@@ -158,8 +167,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rich-text-editor-swift", "location" : "https://github.com/element-hq/matrix-rich-text-editor-swift",
"state" : { "state" : {
"revision" : "c0877fb9323d1ec2de56e4fe2da97f73cac36c38", "revision" : "56077711bf495db926ec3f3efd36ed127fb9ceb1",
"version" : "2.42.0" "version" : "2.42.1"
} }
}, },
{ {
@@ -167,8 +176,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "56ec8d0458a688018bbd9f1f2fd70fae7919f415", "revision" : "dbdf59e13d0eade7fd9cbd01cfd564693b85b761",
"version" : "26.9.9" "version" : "26.10.15"
}
},
{
"identity" : "matrix-rust-rtc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-rtc",
"state" : {
"revision" : "c7a81b6742dc5e571b90b05f5109677384efb933",
"version" : "0.5.0-rc.5"
} }
}, },
{ {
@@ -194,8 +212,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/PostHog/posthog-ios", "location" : "https://github.com/PostHog/posthog-ios",
"state" : { "state" : {
"revision" : "c0218386c49115cce6b05a50b41f4f60bc995acb", "revision" : "d97d9a0babda42ecee026bd0a5a9ea1756349dc1",
"version" : "3.69.6" "version" : "3.85.3"
} }
}, },
{ {
@@ -203,8 +221,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa", "location" : "https://github.com/getsentry/sentry-cocoa",
"state" : { "state" : {
"revision" : "cd213f98af160e6497eeff0707bc3f6a205b159c", "revision" : "8689e780a295dfdc6501ac0dbb502461e16d6551",
"version" : "9.26.0" "version" : "9.29.2"
} }
}, },
{ {
@@ -230,8 +248,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms", "location" : "https://github.com/apple/swift-async-algorithms",
"state" : { "state" : {
"revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", "revision" : "789dcf1f3d3f00251482f40a432ab7144c181987",
"version" : "1.1.5" "version" : "1.1.6"
} }
}, },
{ {
@@ -239,8 +257,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections", "location" : "https://github.com/apple/swift-collections",
"state" : { "state" : {
"revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", "revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6",
"version" : "1.6.0" "version" : "1.7.1"
} }
}, },
{ {
@@ -275,8 +293,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing", "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : { "state" : {
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6", "revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
"version" : "1.19.4" "version" : "1.19.6"
} }
}, },
{ {
@@ -284,8 +302,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax", "location" : "https://github.com/swiftlang/swift-syntax",
"state" : { "state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
"version" : "603.0.2" "version" : "604.0.0"
} }
}, },
{ {
@@ -293,8 +311,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git", "location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : { "state" : {
"revision" : "8d6ad267714cac3ae747cefdd21f7a6665006e1f", "revision" : "18b80329749eca5ea29fc50211dca5c7eff5bfec",
"version" : "2.13.7" "version" : "2.13.9"
} }
}, },
{ {
@@ -320,8 +338,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect", "location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : { "state" : {
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840", "revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
"version" : "26.0.2" "version" : "27.0.0"
} }
}, },
{ {
@@ -15,27 +15,6 @@ import Foundation
import Macros import Macros
import SwiftUI import SwiftUI
/// Common settings between app and NSE
nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable {
var lastNotificationBootTime: TimeInterval? { get set }
var selectedNotificationTone: NotificationTone? { get set }
var logLevel: LogLevel { get }
var traceLogPacks: Set<TraceLogPack> { get }
var bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> { get }
var contentScannerURL: RemotePreference<URL?> { get }
var forceDisableE2EE: RemotePreference<Bool> { get }
var mapTilerConfiguration: RemotePreference<MapTilerConfiguration> { get }
var enableOnlySignedDeviceIsolationMode: Bool { get }
var threadsEnabled: Bool { get }
var hideQuietNotificationAlerts: Bool { get }
var voipPusherAppID: String { get }
var pushKitDeviceToken: String? { get }
var voipPushGatewayNotifyURL: String? { get }
}
nonisolated enum AppBuildType { nonisolated enum AppBuildType {
case debug case debug
case nightly case nightly
@@ -72,15 +51,10 @@ final nonisolated class AppSettings: @unchecked Sendable {
store.reset() store.reset()
} }
func resetSessionSpecificSettings() {
MXLog.warning("Resetting the user session specific AppSettings.")
resetHasRunIdentityConfirmationOnboarding()
}
// MARK: - Hooks // MARK: - Hooks
// swiftlint:disable:next function_parameter_count // swiftlint:disable:next function_parameter_count
func override(accountProviders: [String], func override(accountProviders: [AccountProvider],
allowOtherAccountProviders: Bool, allowOtherAccountProviders: Bool,
hideBrandChrome: Bool, hideBrandChrome: Bool,
pushGatewayBaseURL: URL, pushGatewayBaseURL: URL,
@@ -141,11 +115,15 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(defaultValue: true) @UserPreference(defaultValue: true)
var hasSeenNewSoundBanner: Bool var hasSeenNewSoundBanner: Bool
/// Defaults to `true` so new users never see it. Developer options arms it for now, an upgrade migration will at launch.
@UserPreference(defaultValue: true)
var hasSeenMultiAccountAnnouncement: Bool
/// The initial set of account providers shown to the user in the authentication flow. /// The initial set of account providers shown to the user in the authentication flow.
/// ///
/// Account provider is the friendly term for the server name. It should not contain an `https` prefix and should /// Account provider is the friendly term for the server name. It should not contain an `https` prefix and should
/// match the last part of the user ID. For example `example.com` and not `https://matrix.example.com`. /// match the last part of the user ID. For example `example.com` and not `https://matrix.example.com`.
private(set) var accountProviders = ["matrix.org"] private(set) var accountProviders: [AccountProvider] = [.managed(serverName: "matrix.org", baseURL: "https://matrix-client.matrix.org")]
/// Whether or not the user is allowed to manually enter their own account provider or must select from one of `defaultAccountProviders`. /// Whether or not the user is allowed to manually enter their own account provider or must select from one of `defaultAccountProviders`.
private(set) var allowOtherAccountProviders = true private(set) var allowOtherAccountProviders = true
/// Whether the components surrounding the app brand/logo should be hidden or not /// Whether the components surrounding the app brand/logo should be hidden or not
@@ -153,6 +131,8 @@ final nonisolated class AppSettings: @unchecked Sendable {
/// The task identifier used for background app refresh. Also used in main target's the Info.plist /// The task identifier used for background app refresh. Also used in main target's the Info.plist
let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh" let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh"
/// The task identifier used for the search index backfill. Also used in main target's the Info.plist
let searchBackfillTaskIdentifier = "io.element.elementx.background.search-backfill"
/// A URL where users can go read more about the app. /// A URL where users can go read more about the app.
private(set) var websiteURL: URL = "https://element.io" private(set) var websiteURL: URL = "https://element.io"
@@ -190,8 +170,12 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(key: "previousServers", defaultValue: []) @UserPreference(key: "previousServers", defaultValue: [])
var previousServers: [String] var previousServers: [String]
var defaultServer: String { var defaultAccountProvider: AccountProvider {
previousServers.first ?? accountProviders[0] if allowOtherAccountProviders {
previousServers.first.map { .generic($0) } ?? accountProviders[0]
} else {
accountProviders[0]
}
} }
// MARK: - Security // MARK: - Security
@@ -270,12 +254,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference @UserPreference
var voipPushGatewayNotifyURL: String? var voipPushGatewayNotifyURL: String?
@UserPreference(defaultValue: true)
var enableNotifications: Bool
@UserPreference(defaultValue: true)
var enableInAppNotifications: Bool
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var hideQuietNotificationAlerts: Bool var hideQuietNotificationAlerts: Bool
@@ -287,9 +265,9 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference @UserPreference
var lastNotificationBootTime: TimeInterval? var lastNotificationBootTime: TimeInterval?
/// The sound played when delivering noisy notifications. If nil, use the ElementX default /// The app icon badge value the app last computed from the SDK's unread notification counts.
@UserPreference @UserPreference(defaultValue: 0)
var selectedNotificationTone: NotificationTone? var lastKnownBadgeCount: Int
// MARK: - Logging // MARK: - Logging
@@ -348,9 +326,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var hasRunNotificationPermissionsOnboarding: Bool var hasRunNotificationPermissionsOnboarding: Bool
@UserPreference(defaultValue: false)
var hasRunIdentityConfirmationOnboarding: Bool
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var hasRequestedLocationAlwaysLocationAuthorization: Bool var hasRequestedLocationAlwaysLocationAuthorization: Bool
@@ -368,14 +343,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var liveLocationDisclaimerDisplayed: Bool var liveLocationDisclaimerDisplayed: Bool
// MARK: - Home Screen
@UserPreference(defaultValue: RoomListActivityVisibility.current)
var roomListActivityVisibility: RoomListActivityVisibility
@UserPreference(defaultValue: false)
var roomListNotificationCountEnabled: Bool
// MARK: - Room Screen // MARK: - Room Screen
@UserPreference(defaultValue: AppBuildType.current == .debug) @UserPreference(defaultValue: AppBuildType.current == .debug)
@@ -422,11 +389,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
/// The MapTiler configuration used to build map URLs, which defaults to the bundled one. /// The MapTiler configuration used to build map URLs, which defaults to the bundled one.
private(set) var mapTilerConfiguration = RemotePreference(AppSettings.bundledMapTilerConfiguration) private(set) var mapTilerConfiguration = RemotePreference(AppSettings.bundledMapTilerConfiguration)
// MARK: - Presence
@UserPreference(defaultValue: true)
var sharePresence: Bool
// MARK: - Feature Flags // MARK: - Feature Flags
/// Others /// Others
@@ -452,7 +414,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var messageMultiSelectEnabled: Bool var messageMultiSelectEnabled: Bool
@UserPreference(defaultValue: ProcessInfo().isiOSAppOnMac) @UserPreference(key: "globalSearchEnabledV2", defaultValue: true)
var globalSearchEnabled: Bool var globalSearchEnabled: Bool
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
@@ -472,22 +434,32 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(defaultValue: false) @UserPreference(defaultValue: false)
var linkNewDeviceEnabled: Bool var linkNewDeviceEnabled: Bool
@UserPreference(defaultValue: false)
var automaticBackPaginationEnabled: Bool
@UserPreference(key: "clientPausingAndResumingEnabledV2", defaultValue: false, volatile: true) @UserPreference(key: "clientPausingAndResumingEnabledV2", defaultValue: false, volatile: true)
var clientPausingAndResumingEnabled: Bool var clientPausingAndResumingEnabled: Bool
@UserPreference(defaultValue: AppBuildType.current != .release) @UserPreference(defaultValue: AppBuildType.current != .release)
var developerOptionsEnabled: Bool var developerOptionsEnabled: Bool
/// Runs calls through the native matrix-rust-rtc stack instead of the Element Call web view.
@UserPreference(defaultValue: false)
var nativeCallEnabled: Bool
@UserPreference(defaultValue: false)
var multiAccountEnabled: Bool
/// Lets the add account flow start once `multiAccountEnabled` shows its entry points. Not a user preference, so testers
/// can't add a second account while signing out of one isn't safe yet. Only set it to `true` locally, for development.
let addAccountFlowEnabled = false
init(store: UserDefaultsProtocol) { init(store: UserDefaultsProtocol) {
self.store = store self.store = store
} }
func userSettings(for userID: String) -> UserSettings {
UserSettings(appSettings: self, accountSettings: .init(userID: userID, store: store))
}
static func volatile() -> AppSettings { static func volatile() -> AppSettings {
AppSettings(store: VolatileUserDefaults()) AppSettings(store: VolatileUserDefaults())
} }
} }
nonisolated extension AppSettings: CommonSettingsProtocol { }
@@ -23,6 +23,10 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
private let navigationStackCoordinator: NavigationStackCoordinator private let navigationStackCoordinator: NavigationStackCoordinator
private let flowParameters: CommonFlowParameters private let flowParameters: CommonFlowParameters
private var userSettings: UserSettings {
flowParameters.userSession.userSettings
}
// periphery:ignore - retaining purpose // periphery:ignore - retaining purpose
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator? private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
private var bugReportFlowCoordinator: BugReportFlowCoordinator? private var bugReportFlowCoordinator: BugReportFlowCoordinator?
@@ -73,8 +77,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
private func presentSettingsScreen(animated: Bool) { private func presentSettingsScreen(animated: Bool) {
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession, let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
appSettings: flowParameters.appSettings,
appHooks: flowParameters.appHooks,
isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled, isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled,
isInSecondaryWindow: isInSecondaryWindow, isInSecondaryWindow: isInSecondaryWindow,
userIndicatorController: flowParameters.userIndicatorController)) userIndicatorController: flowParameters.userIndicatorController))
@@ -86,42 +88,52 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .logout:
actionsSubject.send(.runLogoutFlow)
case .secureBackup:
startEncryptionSettingsFlow()
case .userDetails: case .userDetails:
presentUserDetailsEditScreen() presentUserDetailsEditScreen()
case let .userStatusEmojiPicker(continuation): case let .userStatusEmojiPicker(continuation):
presentEmojiPicker(emojiPickerContinuation: continuation) presentEmojiPicker(emojiPickerContinuation: continuation)
case .linkNewDevice:
startLinkNewDeviceFlow() case .addAccount:
break // Nothing to present until the add account flow exists.
case let .manageAccount(url): case let .manageAccount(url):
presentAccountManagementURL(url) presentAccountManagementURL(url)
case .analytics: case .linkNewDevice:
presentAnalyticsScreen() startLinkNewDeviceFlow()
case .appLock: case .notifications:
presentAppLockSetupFlow() presentNotificationSettings()
case .secureBackup:
startEncryptionSettingsFlow()
case .moderationAndSafety:
presentModerationAndSafetySettings()
case .bugReport: case .bugReport:
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .push(navigationStackCoordinator), bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .push(navigationStackCoordinator),
userIndicatorController: flowParameters.userIndicatorController, userIndicatorController: flowParameters.userIndicatorController,
bugReportService: flowParameters.bugReportService, bugReportService: flowParameters.bugReportService,
userSession: flowParameters.userSession)) userSession: flowParameters.userSession))
bugReportFlowCoordinator?.start() bugReportFlowCoordinator?.start()
case .about:
presentLegalInformationScreen() case .logout:
case .blockedUsers: actionsSubject.send(.runLogoutFlow)
presentBlockedUsersScreen()
case .notifications:
presentNotificationSettings()
case .advancedSettings:
presentAdvancedSettings()
case .labs:
presentLabs()
case .developerOptions:
presentDeveloperOptions()
case .deactivateAccount: case .deactivateAccount:
presentDeactivateAccount() presentDeactivateAccount()
case .mediaUploadQuality:
presentMediaUploadQualitySettings()
case .appLock:
presentAppLockSetupFlow()
case .locationSharing:
presentLocationSharingSettings()
case .analytics:
presentAnalyticsScreen()
case .labs:
presentLabs()
case .about:
presentLegalInformationScreen()
case .developerOptions:
presentDeveloperOptions()
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -130,7 +142,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
} }
private func presentLabs() { private func presentLabs() {
let coordinator = LabsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings)) let coordinator = LabsScreenCoordinator(parameters: .init(userSettings: userSettings))
coordinator.actions coordinator.actions
.sink { [weak self] action in .sink { [weak self] action in
switch action { switch action {
@@ -145,8 +157,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
private func startEncryptionSettingsFlow() { private func startEncryptionSettingsFlow() {
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: flowParameters.userSession, let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: flowParameters.userSession,
appSettings: flowParameters.appSettings,
appHooks: flowParameters.appHooks,
userIndicatorController: flowParameters.userIndicatorController, userIndicatorController: flowParameters.userIndicatorController,
navigationStackCoordinator: navigationStackCoordinator)) navigationStackCoordinator: navigationStackCoordinator))
coordinator.actionsPublisher.sink { [weak self] action in coordinator.actionsPublisher.sink { [weak self] action in
@@ -183,7 +193,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
let coordinator = flowParameters.appHooks.userDetailsEditScreenHook let coordinator = flowParameters.appHooks.userDetailsEditScreenHook
.makeCoordinator(parameters: .init(orientationManager: flowParameters.windowManager, .makeCoordinator(parameters: .init(orientationManager: flowParameters.windowManager,
userSession: flowParameters.userSession, userSession: flowParameters.userSession,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings), mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: userSettings),
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: flowParameters.userIndicatorController)) userIndicatorController: flowParameters.userIndicatorController))
coordinator.actions coordinator.actions
@@ -227,7 +237,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
} }
private func presentAnalyticsScreen() { private func presentAnalyticsScreen() {
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings, let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(userSettings: userSettings,
analytics: flowParameters.analytics)) analytics: flowParameters.analytics))
navigationStackCoordinator.push(coordinator) navigationStackCoordinator.push(coordinator)
} }
@@ -253,11 +263,11 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
} }
private func presentLegalInformationScreen() { private func presentLegalInformationScreen() {
navigationStackCoordinator.push(LegalInformationScreenCoordinator(appSettings: flowParameters.appSettings)) navigationStackCoordinator.push(LegalInformationScreenCoordinator(userSettings: userSettings))
} }
private func presentBlockedUsersScreen() { private func presentBlockedUsersScreen() {
let coordinator = BlockedUsersScreenCoordinator(parameters: .init(hideProfiles: flowParameters.appSettings.hideIgnoredUserProfiles, let coordinator = BlockedUsersScreenCoordinator(parameters: .init(hideProfiles: userSettings.app.hideIgnoredUserProfiles,
userSession: flowParameters.userSession, userSession: flowParameters.userSession,
userIndicatorController: flowParameters.userIndicatorController)) userIndicatorController: flowParameters.userIndicatorController))
navigationStackCoordinator.push(coordinator) navigationStackCoordinator.push(coordinator)
@@ -268,22 +278,44 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
userSession: flowParameters.userSession, userSession: flowParameters.userSession,
userNotificationCenter: UNUserNotificationCenter.current(), userNotificationCenter: UNUserNotificationCenter.current(),
userIndicatorController: flowParameters.userIndicatorController, userIndicatorController: flowParameters.userIndicatorController,
isModallyPresented: false, isModallyPresented: false)
appSettings: flowParameters.appSettings)
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters) let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
navigationStackCoordinator.push(coordinator) navigationStackCoordinator.push(coordinator)
} }
private func presentAdvancedSettings() { private func presentModerationAndSafetySettings() {
let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings, let parameters = ModerationAndSafetySettingsScreenCoordinatorParameters(userSession: flowParameters.userSession,
analytics: flowParameters.analytics, userIndicatorController: flowParameters.userIndicatorController)
clientProxy: flowParameters.userSession.clientProxy, let coordinator = ModerationAndSafetySettingsScreenCoordinator(parameters: parameters)
userIndicatorController: flowParameters.userIndicatorController)) coordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .blockedUsers:
presentBlockedUsersScreen()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func presentMediaUploadQualitySettings() {
let parameters = MediaUploadQualitySettingsScreenCoordinatorParameters(userSettings: userSettings,
analytics: flowParameters.analytics)
let coordinator = MediaUploadQualitySettingsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator)
}
private func presentLocationSharingSettings() {
let parameters = LocationSharingSettingsScreenCoordinatorParameters(userSettings: userSettings)
let coordinator = LocationSharingSettingsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator) navigationStackCoordinator.push(coordinator)
} }
private func presentDeveloperOptions() { private func presentDeveloperOptions() {
let coordinator = DeveloperOptionsScreenCoordinator(appSettings: flowParameters.appSettings, let coordinator = DeveloperOptionsScreenCoordinator(appSettings: userSettings.app,
appHooks: flowParameters.appHooks, appHooks: flowParameters.appHooks,
clientProxy: flowParameters.userSession.clientProxy) clientProxy: flowParameters.userSession.clientProxy)
@@ -329,7 +361,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
accountSettingsPresenter = OAuthAccountSettingsPresenter(accountURL: url, accountSettingsPresenter = OAuthAccountSettingsPresenter(accountURL: url,
presentationAnchor: flowParameters.windowManager.mainWindow, presentationAnchor: flowParameters.windowManager.mainWindow,
appMediator: flowParameters.appMediator, appMediator: flowParameters.appMediator,
appSettings: flowParameters.appSettings, userSettings: userSettings,
appHooks: flowParameters.appHooks, appHooks: flowParameters.appHooks,
continuation: continuation) continuation: continuation)
accountSettingsPresenter?.start() accountSettingsPresenter?.start()
@@ -40,27 +40,30 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
canReportProblem = isBugReportServiceEnabled canReportProblem = isBugReportServiceEnabled
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
let classicAppAccountProvider = authenticationService.classicAppAccount?.serverName let isClassicAppAccountAllowed = if let classicAppAccount = authenticationService.classicAppAccount {
let isClassicAppAccountAllowed = classicAppAccountProvider.map { appSettings.accountProviders.contains($0) } ?? false appSettings.accountProviders.contains(serverName: classicAppAccount.serverName, orBaseURL: classicAppAccount.homeserverURL)
} else {
false
}
let initialViewState = if !appSettings.allowOtherAccountProviders { let initialViewState = if !appSettings.allowOtherAccountProviders {
// We don't show the create account button when custom providers are disallowed. // We don't show the create account button when custom providers are disallowed.
// The assumption here being that if you're running a custom app, your users will already be created. // The assumption here being that if you're running a custom app, your users will already be created.
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil, AuthenticationStartScreenViewState(serverNameOrBaseURL: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0].serverNameOrBaseURL : nil,
showCreateAccountButton: false, showCreateAccountButton: false,
showQRCodeLoginButton: isQRCodeScanningSupported, showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: isClassicAppAccountAllowed ? authenticationService.classicAppAccount.map { .welcomeBack($0) } : nil, classicAppMode: isClassicAppAccountAllowed ? authenticationService.classicAppAccount.map { .welcomeBack($0) } : nil,
hideBrandChrome: appSettings.hideBrandChrome) hideBrandChrome: appSettings.hideBrandChrome)
} else if let provisioningParameters { } else if let provisioningParameters {
// We only show the "Sign in to …" button when using a provisioning link. // We only show the "Sign in to …" button when using a provisioning link.
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider, AuthenticationStartScreenViewState(serverNameOrBaseURL: provisioningParameters.accountProvider,
showCreateAccountButton: false, showCreateAccountButton: false,
showQRCodeLoginButton: false, showQRCodeLoginButton: false,
classicAppMode: nil, classicAppMode: nil,
hideBrandChrome: appSettings.hideBrandChrome) hideBrandChrome: appSettings.hideBrandChrome)
} else { } else {
// The default configuration. // The default configuration.
AuthenticationStartScreenViewState(serverName: nil, AuthenticationStartScreenViewState(serverNameOrBaseURL: nil,
showCreateAccountButton: appSettings.showCreateAccountButton, showCreateAccountButton: appSettings.showCreateAccountButton,
showQRCodeLoginButton: isQRCodeScanningSupported, showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: authenticationService.classicAppAccount.map { .welcomeBack($0) }, classicAppMode: authenticationService.classicAppAccount.map { .welcomeBack($0) },
@@ -116,21 +119,21 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
// bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to // bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to
// open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in // open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in
// instead: the user re-verifies and loses only history that by definition has no backup. // instead: the user re-verifies and loses only history that by definition has no backup.
await configureAccountProvider(classicAppAccount.serverName, await loginDirectly(using: classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)", loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL) fallbackHomeserverURL: classicAppAccount.homeserverURL)
} else if let serverName = state.serverName { } else if let serverNameOrBaseURL = state.serverNameOrBaseURL {
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint) await loginDirectly(using: serverNameOrBaseURL, loginHint: provisioningParameters?.loginHint)
} else { } else {
actionsSubject.send(.login) // No need to configure anything here, continue the flow. actionsSubject.send(.login) // No need to configure anything here, continue the flow.
} }
} }
private func configureAccountProvider(_ accountProvider: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async { private func loginDirectly(using serverNameOrBaseURL: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async {
startLoading() startLoading()
defer { stopLoading() } defer { stopLoading() }
if case .failure = await authenticationService.configure(for: accountProvider, flow: .login) { if case .failure = await authenticationService.configure(for: serverNameOrBaseURL, flow: .login) {
// Try the fallback URL before showing an error. // Try the fallback URL before showing an error.
if let fallbackHomeserverURL, if let fallbackHomeserverURL,
case .success = await authenticationService.configure(for: fallbackHomeserverURL.absoluteString, flow: .login) { case .success = await authenticationService.configure(for: fallbackHomeserverURL.absoluteString, flow: .login) {
@@ -11,8 +11,6 @@ import SwiftUI
struct SettingsScreenCoordinatorParameters { struct SettingsScreenCoordinatorParameters {
let userSession: UserSessionProtocol let userSession: UserSessionProtocol
let appSettings: AppSettings
let appHooks: AppHooks
let isBugReportServiceEnabled: Bool let isBugReportServiceEnabled: Bool
let isInSecondaryWindow: Bool let isInSecondaryWindow: Bool
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
@@ -20,22 +18,30 @@ struct SettingsScreenCoordinatorParameters {
enum SettingsScreenCoordinatorAction { enum SettingsScreenCoordinatorAction {
case dismiss case dismiss
case logout
case secureBackup
case userDetails case userDetails
case userStatusEmojiPicker(EmojiPickerScreenContinuation) case userStatusEmojiPicker(EmojiPickerScreenContinuation)
case analytics
case appLock case addAccount
case bugReport
case about
case blockedUsers
case linkNewDevice
case manageAccount(url: URL) case manageAccount(url: URL)
case linkNewDevice
case notifications case notifications
case advancedSettings case secureBackup
case labs case moderationAndSafety
case developerOptions case bugReport
case logout
case deactivateAccount case deactivateAccount
case mediaUploadQuality
case appLock
case locationSharing
case analytics
case labs
case about
case developerOptions
} }
final class SettingsScreenCoordinator: CoordinatorProtocol { final class SettingsScreenCoordinator: CoordinatorProtocol {
@@ -52,8 +58,6 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
init(parameters: SettingsScreenCoordinatorParameters) { init(parameters: SettingsScreenCoordinatorParameters) {
viewModel = SettingsScreenViewModel(userSession: parameters.userSession, viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
appSettings: parameters.appSettings,
appHooks: parameters.appHooks,
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled, isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
isInSecondaryWindow: parameters.isInSecondaryWindow, isInSecondaryWindow: parameters.isInSecondaryWindow,
userIndicatorController: parameters.userIndicatorController) userIndicatorController: parameters.userIndicatorController)
@@ -65,38 +69,48 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
switch action { switch action {
case .close: case .close:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .userDetails: case .userDetails:
actionsSubject.send(.userDetails) actionsSubject.send(.userDetails)
case let .userStatusEmojiPicker(continuation): case let .userStatusEmojiPicker(continuation):
actionsSubject.send(.userStatusEmojiPicker(continuation)) actionsSubject.send(.userStatusEmojiPicker(continuation))
case .linkNewDevice:
actionsSubject.send(.linkNewDevice) case .addAccount:
actionsSubject.send(.addAccount)
case let .manageAccount(url): case let .manageAccount(url):
actionsSubject.send(.manageAccount(url: url)) actionsSubject.send(.manageAccount(url: url))
case .analytics: case .linkNewDevice:
actionsSubject.send(.analytics) actionsSubject.send(.linkNewDevice)
case .appLock:
actionsSubject.send(.appLock)
case .reportBug:
actionsSubject.send(.bugReport)
case .about:
actionsSubject.send(.about)
case .blockedUsers:
actionsSubject.send(.blockedUsers)
case .secureBackup:
actionsSubject.send(.secureBackup)
case .notifications: case .notifications:
actionsSubject.send(.notifications) actionsSubject.send(.notifications)
case .advancedSettings: case .secureBackup:
actionsSubject.send(.advancedSettings) actionsSubject.send(.secureBackup)
case .labs: case .moderationAndSafety:
actionsSubject.send(.labs) actionsSubject.send(.moderationAndSafety)
case .developerOptions: case .reportBug:
actionsSubject.send(.developerOptions) actionsSubject.send(.bugReport)
case .logout: case .logout:
actionsSubject.send(.logout) actionsSubject.send(.logout)
case .deactivateAccount: case .deactivateAccount:
actionsSubject.send(.deactivateAccount) actionsSubject.send(.deactivateAccount)
case .mediaUploadQuality:
actionsSubject.send(.mediaUploadQuality)
case .appLock:
actionsSubject.send(.appLock)
case .locationSharing:
actionsSubject.send(.locationSharing)
case .analytics:
actionsSubject.send(.analytics)
case .labs:
actionsSubject.send(.labs)
case .about:
actionsSubject.send(.about)
case .developerOptions:
actionsSubject.send(.developerOptions)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
Submodule
+1
Submodule Enterprise added at 8b65ca7f13
+287 -14
View File
@@ -6,36 +6,46 @@
// //
import CallKit import CallKit
import ElementCall
@testable import ElementX @testable import ElementX
import PushKit import PushKit
import Testing import Testing
@MainActor @MainActor
final class ElementCallServiceTests { final class ElementCallServiceTests {
private var appSettings: AppSettings!
private var callProvider: CXProviderMock! private var callProvider: CXProviderMock!
private var callController: CXCallControllerMock!
private var currentDate: Date! private var currentDate: Date!
private var clock: ManualClock! private var clock: ManualClock!
private var pushRegistry: PKPushRegistry! private var pushRegistry: PKPushRegistry!
private var service: ElementCallService! private var service: ElementCallService!
private var userSession: UserSessionMock!
init() { init() {
appSettings = AppSettings.volatile()
pushRegistry = PKPushRegistry(queue: nil) pushRegistry = PKPushRegistry(queue: nil)
callProvider = CXProviderMock(.init()) callProvider = CXProviderMock(.init())
callController = CXCallControllerMock(.init())
currentDate = Date() currentDate = Date()
clock = ManualClock() clock = ManualClock()
let dateProvider: () -> Date = { let dateProvider: () -> Date = {
self.currentDate self.currentDate
} }
service = ElementCallService(callProvider: callProvider, service = ElementCallService(appSettings: appSettings,
timeProvider: TimeProvider(clock: clock, now: dateProvider), callProvider: callProvider,
appSettings: AppSettings.volatile()) callController: callController,
timeProvider: TimeProvider(clock: clock, now: dateProvider))
} }
isolated deinit { isolated deinit {
appSettings = nil
callProvider = nil callProvider = nil
callController = nil
currentDate = nil currentDate = nil
clock = nil clock = nil
pushRegistry = nil pushRegistry = nil
userSession = nil
} }
@Test @Test
@@ -199,7 +209,7 @@ final class ElementCallServiceTests {
} }
@Test @Test
func setupCallSessionCancelsPendingUnansweredTimeout() async { func setupCallSessionCancelsPendingUnansweredTimeout() async throws {
// Schedule the 60s unanswered timer via an incoming push // Schedule the 60s unanswered timer via an incoming push
await waitForConfirmation { confirmation in await waitForConfirmation { confirmation in
let payload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60) let payload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
@@ -213,22 +223,20 @@ final class ElementCallServiceTests {
// Simulate the answer flow handing off to setupCallSession, which must cancel // Simulate the answer flow handing off to setupCallSession, which must cancel
// the pending endUnansweredCallTask as part of clearing the incoming state. // the pending endUnansweredCallTask as part of clearing the incoming state.
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome") await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
var unansweredFired = false let (endedCalls, endedCallsContinuation) = AsyncStream<CXCallEndedReason>.makeStream()
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
if reason == .unanswered { endedCallsContinuation.yield(reason)
unansweredFired = true
}
} }
let deferredUnansweredCall = deferFailure(endedCalls,
timeout: .milliseconds(100),
message: "endUnansweredCallTask should have been cancelled by setupCallSession") { $0 == .unanswered }
// Advance past what would have been the 60s unanswered timeout // Advance past what would have been the 60s unanswered timeout
clock.advance(by: .seconds(120)) clock.advance(by: .seconds(120))
for _ in 0..<3 {
await Task.yield()
}
#expect(!unansweredFired, "endUnansweredCallTask should have been cancelled by setupCallSession") try await deferredUnansweredCall.fulfill()
} }
@Test @Test
@@ -246,7 +254,7 @@ final class ElementCallServiceTests {
@Test @Test
func duplicateRoomPushReportsCallAsHandled() async { func duplicateRoomPushReportsCallAsHandled() async {
// A duplicate push for an ongoing call is reported as handled, leaving the ongoing call alone. // A duplicate push for an ongoing call is reported as handled, leaving the ongoing call alone.
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome") await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30) let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
await expectImmediatelyEndedCallReported(forPayload: pushPayload, expectedReason: .answeredElsewhere) await expectImmediatelyEndedCallReported(forPayload: pushPayload, expectedReason: .answeredElsewhere)
@@ -257,6 +265,271 @@ final class ElementCallServiceTests {
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com") #expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
} }
@Test
func webViewCallSessionIsNotTrackedByCallKit() async {
// CallKit tracking a call gives this process exclusive media access, which starves the web
// view. So a web view call is only ever tracked on the app side.
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
service.reportCallSessionConnected(roomID: "!room:example.com")
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
#expect(!callProvider.reportCallWithUpdatedCalled)
#expect(!callProvider.reportOutgoingCallWithConnectedAtCalled)
}
@Test
func endingAReplacedCallLeavesTheCallThatReplacedItUp() async throws {
enableNativeCalls()
// Leaving a call to start one in another room requests an end call action for the first,
// then starts the second before the system performs it.
await service.setupCallSession(roomID: "!first:example.com", roomDisplayName: "first", isVideo: true)
let firstCallID = try #require(callProvider.reportCallWithUpdatedReceivedArguments?.uuid)
service.tearDownCallSession(roomID: "!first:example.com")
await service.setupCallSession(roomID: "!second:example.com", roomDisplayName: "second", isVideo: true)
var endedRooms: [String] = []
let cancellable = service.actions.sink { action in
if case .endCall(let roomID) = action {
endedRooms.append(roomID)
}
}
defer { cancellable.cancel() }
service.endCall(withCallKitID: firstCallID)
#expect(endedRooms.isEmpty, "The first call's end action must not hang up the second call")
#expect(service.ongoingCallRoomIDPublisher.value == "!second:example.com")
// And the second call still ends when it's the one the system means.
let secondCallID = try #require(callProvider.reportCallWithUpdatedReceivedArguments?.uuid)
service.endCall(withCallKitID: secondCallID)
#expect(endedRooms == ["!second:example.com"])
#expect(service.ongoingCallRoomIDPublisher.value == nil)
}
/// A declined or unanswered call is the system's to log as such: reported with its reason, and
/// never as an end call action, which would log it as one we hung up.
@Test
func aCallThatEndedWithoutUsIsReportedRatherThanEnded() async throws {
enableNativeCalls()
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
let callID = try #require(callProvider.reportCallWithUpdatedReceivedArguments?.uuid)
let requestsBeforeTheEnd = callController.requestCompletionCallsCount
service.reportCallSessionEnded(roomID: "!other:example.com", reason: .unanswered)
#expect(!callProvider.reportCallWithEndedAtReasonCalled, "Another room's end is not this call's")
service.reportCallSessionEnded(roomID: "!room:example.com", reason: .unanswered)
#expect(callProvider.reportCallWithEndedAtReasonReceivedArguments?.uuid == callID)
#expect(callProvider.reportCallWithEndedAtReasonReceivedArguments?.reason == .unanswered)
#expect(callController.requestCompletionCallsCount == requestsBeforeTheEnd)
#expect(service.ongoingCallRoomIDPublisher.value == nil)
}
@Test
func tearingDownAnotherRoomLeavesTheOngoingCallUp() async {
// A call screen that is dismissed after another room's call replaced it must not take the
// new call down with it.
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
service.tearDownCallSession(roomID: "!other:example.com")
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
service.tearDownCallSession(roomID: "!room:example.com")
#expect(service.ongoingCallRoomIDPublisher.value == nil)
}
// MARK: - Native calls
@Test
func nativeModeReportsVoiceCallsWithoutVideo() async {
appSettings.nativeCallEnabled = true
await reportIncomingPush(isVoiceCall: true)
#expect(callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.update.hasVideo == false)
}
@Test
func nativeModeStillReportsVideoCallsWithVideo() async {
appSettings.nativeCallEnabled = true
await reportIncomingPush(isVoiceCall: false)
#expect(callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.update.hasVideo == true)
}
@Test
func answeringInNativeModeKeepsTheCallUp() async throws {
appSettings.nativeCallEnabled = true
await reportIncomingPush(isVoiceCall: false)
await clock.waitForScheduledSleep()
try await answerReportedCall()
// The webview path ends the call a second after answering so that `setupCallSession` can
// start a new one. The native stack takes this one over instead, so it has to survive.
#expect(!callProvider.reportCallWithEndedAtReasonCalled)
}
@Test
func answeredNativeCallEndsWhenNothingTakesItOver() async throws {
appSettings.nativeCallEnabled = true
await reportIncomingPush(isVoiceCall: false)
await clock.waitForScheduledSleep()
let answeredUUID = try await answerReportedCall()
let (endedCalls, endedCallsContinuation) = AsyncStream<(UUID, CXCallEndedReason)>.makeStream()
callProvider.reportCallWithEndedAtReasonClosure = { uuid, _, reason in
endedCallsContinuation.yield((uuid, reason))
}
let deferredEndedCall = deferFulfillment(endedCalls) { _ in true }
// Answering swaps the unanswered timer for the watchdog that catches a hand-off that never
// happens — the room isn't joined, there's no transport, a call is already running.
#expect(await clock.waitForScheduledSleep() == .seconds(30))
clock.advance(by: .seconds(30))
let (uuid, reason) = try await deferredEndedCall.fulfill()
#expect(uuid == answeredUUID)
#expect(reason == .failed)
}
@Test
func nativeCallStackTakesOverTheAnsweredCall() async throws {
enableNativeCalls()
await reportIncomingPush(isVoiceCall: false)
await clock.waitForScheduledSleep()
try await answerReportedCall()
// Answering starts the watchdog, so wait for it before asserting that the hand-over stops it.
await clock.waitForScheduledSleep()
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome", isVideo: true)
// Taking the answered call over means keeping its identifier rather than reporting a new
// outgoing call, which is what the update below would be part of.
#expect(!callProvider.reportCallWithUpdatedCalled)
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
// And the watchdog must be gone, otherwise it ends the call that was just taken over.
#expect(!clock.hasPendingSleep, "The watchdog must not outlive the hand-over")
}
@Test
func nativeCallStackForAnotherRoomReportsANewCall() async throws {
enableNativeCalls()
await reportIncomingPush(isVoiceCall: false)
await clock.waitForScheduledSleep()
let answeredUUID = try await answerReportedCall()
await service.setupCallSession(roomID: "!other:example.com", roomDisplayName: "other", isVideo: true)
let update = try #require(callProvider.reportCallWithUpdatedReceivedArguments)
#expect(update.uuid != answeredUUID)
#expect(update.update.localizedCallerName == "other")
#expect(service.ongoingCallRoomIDPublisher.value == "!other:example.com")
// Unlike a web view call, a native one is tracked by CallKit, so it gets the connected report too.
service.reportCallSessionConnected(roomID: "!other:example.com")
#expect(callProvider.reportOutgoingCallWithConnectedAtReceivedArguments?.uuid == update.uuid)
}
@Test
func nativeCallStackNeedsBothTheSettingAndASession() {
#expect(service.nativeCallController == nil)
appSettings.nativeCallEnabled = true
#expect(service.nativeCallController == nil, "The setting alone has no transport to run a call over")
service.setUserSession(nativeUserSession())
#expect(service.nativeCallController != nil)
service.setUserSession(nil)
#expect(service.nativeCallController == nil, "Signing out has to release the stack's to-device subscription")
}
@Test
func turningTheSettingOnMidSessionBuildsTheStack() {
service.setUserSession(nativeUserSession())
#expect(service.nativeCallController == nil)
// The transport is built on demand, so the setting takes effect without an app restart.
appSettings.nativeCallEnabled = true
#expect(service.nativeCallController != nil)
}
@Test
func aNewSessionGetsANewStack() {
appSettings.nativeCallEnabled = true
service.setUserSession(nativeUserSession())
let firstController = service.nativeCallController
// A soft logout or a cache clear brings a new session without the sign out that drops this
// one, and the stack is built against the client proxy it was given.
service.setUserSession(nativeUserSession())
#expect(service.nativeCallController !== firstController)
}
@Test
func callsFallBackToTheWebViewWithoutAStack() {
let roomProxy = JoinedRoomProxyMock(.init(id: "!room:example.com"))
#expect(!service.handleNativeCallRequest(roomProxy: roomProxy, isVoiceCall: false))
appSettings.nativeCallEnabled = true
service.setUserSession(nativeUserSession())
#expect(service.handleNativeCallRequest(roomProxy: roomProxy, isVoiceCall: false))
}
/// Builds a session whose client proxy can serve the native call stack, and holds on to it: the
/// service only keeps a weak reference, as the app owns the session.
private func nativeUserSession() -> UserSessionMock {
let clientProxy = ClientProxyMock(.init())
clientProxy.makeNativeCallTransportReturnValue = ElementCallFakeTransport()
let session = UserSessionMock(.init(clientProxy: clientProxy))
userSession = session
return session
}
/// Puts the service in native mode, with the stack the CallKit paths check for.
private func enableNativeCalls() {
appSettings.nativeCallEnabled = true
service.setUserSession(nativeUserSession())
}
/// Delivers a VoIP push and waits for it to be reported, returning once the call is ringing.
private func reportIncomingPush(isVoiceCall: Bool) async {
await waitForConfirmation { confirmation in
let payload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
.updateIsVoice(isVoiceCall)
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: payload, for: .voIP) {
confirmation()
}
}
}
/// Answers the call the provider last reported, returning its identifier.
@discardableResult
private func answerReportedCall() async throws -> UUID {
let uuid = try #require(callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.uuid)
let provider = CXProvider(configuration: CXProviderConfiguration())
service.provider(provider, perform: CXAnswerCallAction(call: uuid))
return uuid
}
private func expectImmediatelyEndedCallReported(forPayload payload: PKPushPayloadMock, private func expectImmediatelyEndedCallReported(forPayload payload: PKPushPayloadMock,
expectedReason: CXCallEndedReason) async { expectedReason: CXCallEndedReason) async {
let baselineNewIncomingCount = callProvider.reportNewIncomingCallWithUpdateCompletionCallsCount let baselineNewIncomingCount = callProvider.reportNewIncomingCallWithUpdateCompletionCallsCount
@@ -23,7 +23,6 @@ nonisolated struct NotificationContentBuilderTests {
style: .plain) style: .plain)
mediaProvider = MediaProviderMock(.init()) mediaProvider = MediaProviderMock(.init())
notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder, notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder,
notificationSoundName: UNNotificationSoundName("message.caf"),
userSession: NSEUserSessionMock(.init())) userSession: NSEUserSessionMock(.init()))
} }
@@ -226,6 +225,7 @@ nonisolated struct NotificationContentBuilderTests {
mutating func liveLocationStartNotification() async { mutating func liveLocationStartNotification() async {
let event = TimelineEventSDKMock() let event = TimelineEventSDKMock()
event.eventIdReturnValue = UUID().uuidString event.eventIdReturnValue = UUID().uuidString
event.timestampReturnValue = 0
event.contentReturnValue = .state(content: .beaconInfo) event.contentReturnValue = .state(content: .beaconInfo)
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event), let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
@@ -253,6 +253,7 @@ nonisolated struct NotificationContentBuilderTests {
mutating func otherStateEventNotification() async { mutating func otherStateEventNotification() async {
let event = TimelineEventSDKMock() let event = TimelineEventSDKMock()
event.eventIdReturnValue = UUID().uuidString event.eventIdReturnValue = UUID().uuidString
event.timestampReturnValue = 0
event.contentReturnValue = .state(content: .roomName) event.contentReturnValue = .state(content: .roomName)
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event), let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
@@ -172,13 +172,37 @@ struct SettingsScreenViewModelTests {
try await deferred.fulfill() try await deferred.fulfill()
} }
// MARK: - Add account
@Test
mutating func addAccountButtonFollowsTheFeatureFlag() async throws {
let userSettings = UserSettings.mock()
setupViewModel(userSettings: userSettings)
#expect(!context.viewState.showAddAccountButton)
var deferred = deferFulfillment(context.observe(\.viewState.showAddAccountButton)) { $0 }
userSettings.app.multiAccountEnabled = true
try await deferred.fulfill()
deferred = deferFulfillment(context.observe(\.viewState.showAddAccountButton)) { !$0 }
userSettings.app.multiAccountEnabled = false
try await deferred.fulfill()
}
@Test
mutating func addAccount() async throws {
setupViewModel()
let deferred = deferFulfillment(viewModel.actions) { $0.isAddAccount }
context.send(viewAction: .addAccount)
try await deferred.fulfill()
}
// MARK: - Helpers // MARK: - Helpers
private mutating func setupViewModel(status: UserStatus = .init()) { private mutating func setupViewModel(status: UserStatus = .init(), userSettings: UserSettings = .mock()) {
clientProxy = ClientProxyMock(.init(userID: "", status: status)) clientProxy = ClientProxyMock(.init(userID: "", status: status))
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings)),
appSettings: AppSettings.volatile(),
appHooks: AppHooks(),
isBugReportServiceEnabled: true, isBugReportServiceEnabled: true,
isInSecondaryWindow: false, isInSecondaryWindow: false,
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
@@ -227,4 +251,11 @@ private extension SettingsScreenViewModelAction {
default: false default: false
} }
} }
var isAddAccount: Bool {
switch self {
case .addAccount: true
default: false
}
}
} }
@@ -1,13 +1,13 @@
{ {
"originHash" : "7a6117b3894e217e5bc0fe7497b0b868c49d51f41e0d8be554dc766cb9566ec9", "originHash" : "43fad9d39aa61aa7add5e5768908e9ed7948ca1b8e8b61cf9b7b585f4e8fd1a7",
"pins" : [ "pins" : [
{ {
"identity" : "compound-design-tokens", "identity" : "compound-design-tokens",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens", "location" : "https://github.com/element-hq/compound-design-tokens",
"state" : { "state" : {
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419", "revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
"version" : "11.0.0" "version" : "11.3.1"
} }
}, },
{ {
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing", "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : { "state" : {
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6", "revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
"version" : "1.19.4" "version" : "1.19.6"
} }
}, },
{ {
@@ -42,8 +42,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax", "location" : "https://github.com/swiftlang/swift-syntax",
"state" : { "state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
"version" : "603.0.2" "version" : "604.0.0"
} }
}, },
{ {
@@ -51,8 +51,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect", "location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : { "state" : {
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840", "revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
"version" : "26.0.2" "version" : "27.0.0"
} }
}, },
{ {
+9 -9
View File
@@ -1,13 +1,13 @@
{ {
"originHash" : "7a6117b3894e217e5bc0fe7497b0b868c49d51f41e0d8be554dc766cb9566ec9", "originHash" : "43fad9d39aa61aa7add5e5768908e9ed7948ca1b8e8b61cf9b7b585f4e8fd1a7",
"pins" : [ "pins" : [
{ {
"identity" : "compound-design-tokens", "identity" : "compound-design-tokens",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens", "location" : "https://github.com/element-hq/compound-design-tokens",
"state" : { "state" : {
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419", "revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
"version" : "11.0.0" "version" : "11.3.1"
} }
}, },
{ {
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing", "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : { "state" : {
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6", "revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
"version" : "1.19.4" "version" : "1.19.6"
} }
}, },
{ {
@@ -42,8 +42,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax", "location" : "https://github.com/swiftlang/swift-syntax",
"state" : { "state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
"version" : "603.0.2" "version" : "604.0.0"
} }
}, },
{ {
@@ -51,8 +51,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect", "location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : { "state" : {
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840", "revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
"version" : "26.0.2" "version" : "27.0.0"
} }
}, },
{ {
+3 -3
View File
@@ -9,11 +9,11 @@ let package = Package(
.library(name: "Compound", targets: ["Compound"]) .library(name: "Compound", targets: ["Compound"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/element-hq/compound-design-tokens", exact: "11.0.0"), .package(url: "https://github.com/element-hq/compound-design-tokens", exact: "11.3.1"),
// .package(path: "../../compound-design-tokens"), // .package(path: "../../compound-design-tokens"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect", exact: "26.0.2"), .package(url: "https://github.com/siteline/SwiftUI-Introspect", exact: "27.0.0"),
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", exact: "7.0.0"), .package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", exact: "7.0.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", exact: "1.19.4") .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", exact: "1.19.6")
], ],
targets: [ targets: [
.target( .target(