mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
chore(healthchat): resolve the rebase onto upstream 26.10.3
This commit is contained in:
@@ -16,7 +16,7 @@ jobs:
|
||||
# than deleted so the file keeps rebasing cleanly. Run locally with
|
||||
# `swift run tools ci accessibility-tests`.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
runs-on: xcode-27
|
||||
timeout-minutes: 150
|
||||
|
||||
concurrency:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
run: swift run -q tools ci accessibility-tests
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
if: failure()
|
||||
with:
|
||||
name: Results
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
|
||||
# than deleted so the file keeps rebasing cleanly.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 15
|
||||
runs-on: xcode-27
|
||||
timeout-minutes: 25
|
||||
|
||||
concurrency:
|
||||
# When running on develop, use the sha to allow all runs of this workflow to run concurrently.
|
||||
@@ -39,8 +39,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Xcode 26
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.5.app
|
||||
- name: Configure Xcode 27
|
||||
run: sudo xcode-select -s /Applications/Xcode_27.app
|
||||
|
||||
- name: Install sourcery
|
||||
run: brew install sourcery
|
||||
@@ -50,11 +50,11 @@ jobs:
|
||||
run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml
|
||||
|
||||
- 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
|
||||
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
|
||||
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/
|
||||
|
||||
- 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
|
||||
with:
|
||||
name: test-results
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
|
||||
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
runs-on: xcode-27
|
||||
timeout-minutes: 45
|
||||
|
||||
concurrency:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }}
|
||||
|
||||
- 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
|
||||
if: failure()
|
||||
with:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Upload coverage to Codecov
|
||||
# Skip if not successful and in forks
|
||||
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:
|
||||
report_type: coverage
|
||||
files: test_output/integration-cobertura.xml
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Upload test results to Codecov
|
||||
# Skip if cancelled and in forks
|
||||
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
|
||||
with:
|
||||
report_type: test_results
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# 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`.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
runs-on: xcode-27
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
GITHUB_EVENT_INPUTS_TEST_NAME: ${{ github.event.inputs.test_name }}
|
||||
|
||||
- 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
|
||||
if: failure()
|
||||
with:
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
- name: Upload coverage to Codecov
|
||||
# Skip if not successful and in forks
|
||||
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:
|
||||
report_type: coverage
|
||||
files: test_output/ui-cobertura.xml
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Upload test results to Codecov
|
||||
# Skip if cancelled and in forks
|
||||
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
|
||||
with:
|
||||
report_type: test_results
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
key: compilation-cache-unit-tests-${{ github.sha }}
|
||||
|
||||
- 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
|
||||
if: failure()
|
||||
with:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
# runs without a CODECOV_TOKEN — which fails the job via
|
||||
# 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) }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
report_type: coverage
|
||||
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
|
||||
# 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) }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
report_type: test_results
|
||||
|
||||
+4
-14
@@ -18,22 +18,9 @@ DerivedData/
|
||||
.swiftpm
|
||||
.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
|
||||
/vendor/
|
||||
*/xcshareddata/xcodecloud
|
||||
|
||||
## macOS Files
|
||||
.DS_Store
|
||||
@@ -60,3 +47,6 @@ fastlane/README.md
|
||||
# Review working data (per-machine)
|
||||
.claude/review-context/
|
||||
.claude/review-log.jsonl
|
||||
|
||||
# Local SDK development override
|
||||
matrix-rust-components-swift
|
||||
|
||||
+63
@@ -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)
|
||||
|
||||
### What's Changed
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "f1791683f9f09408a7746ce0e163941b3ce7e83e84f1a73b2fecc2b5d2f2f4b6",
|
||||
"originHash" : "fbdb76ae8c78e5a86db4812456fcb4da5795eed4bc8f6715726fec41e87a1e0f",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "compound-design-tokens",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-design-tokens",
|
||||
"state" : {
|
||||
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419",
|
||||
"version" : "11.0.0"
|
||||
"revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -15,8 +15,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/devicekit/DeviceKit",
|
||||
"state" : {
|
||||
"revision" : "56b997e8a61707218f9af09f32b2a1d1806fd792",
|
||||
"version" : "5.8.0"
|
||||
"revision" : "19528aa07e1426626ea9fb2c4a161edf43c4f9e8",
|
||||
"version" : "5.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -55,13 +55,22 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/element-call-swift",
|
||||
"state" : {
|
||||
"revision" : "13eac3567461a9825018e10e9f4475f68215dae7",
|
||||
"version" : "0.24.0"
|
||||
"revision" : "befb5d37a7859ad38cf83719cbfd4abaffa04d82",
|
||||
"version" : "0.26.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -95,8 +104,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "410984bf301f4fa224fe56277b3f8672cc465c79",
|
||||
"version" : "8.11.0"
|
||||
"revision" : "8648fe649c5d62be38721110f835821906e9242a",
|
||||
"version" : "8.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -122,8 +131,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nicklockwood/LRUCache",
|
||||
"state" : {
|
||||
"revision" : "cb5b2bd0da83ad29c0bec762d39f41c8ad0eaf3e",
|
||||
"version" : "1.2.1"
|
||||
"revision" : "f0570e943ea4445d258113ba73f50cb2b7e564ac",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,8 +149,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
|
||||
"state" : {
|
||||
"revision" : "5ee345ca5d65238a6fce29bba87816204be7df20",
|
||||
"version" : "6.28.0"
|
||||
"revision" : "13e41ab3d77ff5113e7e5d4ee87803b1f81f5683",
|
||||
"version" : "6.31.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -149,8 +158,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-analytics-events",
|
||||
"state" : {
|
||||
"revision" : "4b143b14a16a231bd4fc3c6705845f852177abe3",
|
||||
"version" : "0.37.0"
|
||||
"revision" : "7d668b3e4be493843805b8a1ff4e13b261a19965",
|
||||
"version" : "0.38.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -158,8 +167,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rich-text-editor-swift",
|
||||
"state" : {
|
||||
"revision" : "c0877fb9323d1ec2de56e4fe2da97f73cac36c38",
|
||||
"version" : "2.42.0"
|
||||
"revision" : "56077711bf495db926ec3f3efd36ed127fb9ceb1",
|
||||
"version" : "2.42.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -167,8 +176,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "56ec8d0458a688018bbd9f1f2fd70fae7919f415",
|
||||
"version" : "26.9.9"
|
||||
"revision" : "dbdf59e13d0eade7fd9cbd01cfd564693b85b761",
|
||||
"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",
|
||||
"location" : "https://github.com/PostHog/posthog-ios",
|
||||
"state" : {
|
||||
"revision" : "c0218386c49115cce6b05a50b41f4f60bc995acb",
|
||||
"version" : "3.69.6"
|
||||
"revision" : "d97d9a0babda42ecee026bd0a5a9ea1756349dc1",
|
||||
"version" : "3.85.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -203,8 +221,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||
"state" : {
|
||||
"revision" : "cd213f98af160e6497eeff0707bc3f6a205b159c",
|
||||
"version" : "9.26.0"
|
||||
"revision" : "8689e780a295dfdc6501ac0dbb502461e16d6551",
|
||||
"version" : "9.29.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -230,8 +248,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-async-algorithms",
|
||||
"state" : {
|
||||
"revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c",
|
||||
"version" : "1.1.5"
|
||||
"revision" : "789dcf1f3d3f00251482f40a432ab7144c181987",
|
||||
"version" : "1.1.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -239,8 +257,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections",
|
||||
"state" : {
|
||||
"revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a",
|
||||
"version" : "1.6.0"
|
||||
"revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6",
|
||||
"version" : "1.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -275,8 +293,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6",
|
||||
"version" : "1.19.4"
|
||||
"revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
|
||||
"version" : "1.19.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -284,8 +302,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
|
||||
"version" : "603.0.2"
|
||||
"revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
|
||||
"version" : "604.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -293,8 +311,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "8d6ad267714cac3ae747cefdd21f7a6665006e1f",
|
||||
"version" : "2.13.7"
|
||||
"revision" : "18b80329749eca5ea29fc50211dca5c7eff5bfec",
|
||||
"version" : "2.13.9"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -320,8 +338,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||
"state" : {
|
||||
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840",
|
||||
"version" : "26.0.2"
|
||||
"revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
|
||||
"version" : "27.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,27 +15,6 @@ import Foundation
|
||||
import Macros
|
||||
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 {
|
||||
case debug
|
||||
case nightly
|
||||
@@ -72,15 +51,10 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
store.reset()
|
||||
}
|
||||
|
||||
func resetSessionSpecificSettings() {
|
||||
MXLog.warning("Resetting the user session specific AppSettings.")
|
||||
resetHasRunIdentityConfirmationOnboarding()
|
||||
}
|
||||
|
||||
// MARK: - Hooks
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func override(accountProviders: [String],
|
||||
func override(accountProviders: [AccountProvider],
|
||||
allowOtherAccountProviders: Bool,
|
||||
hideBrandChrome: Bool,
|
||||
pushGatewayBaseURL: URL,
|
||||
@@ -141,11 +115,15 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(defaultValue: true)
|
||||
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.
|
||||
///
|
||||
/// 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`.
|
||||
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`.
|
||||
private(set) var allowOtherAccountProviders = true
|
||||
/// 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
|
||||
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.
|
||||
private(set) var websiteURL: URL = "https://element.io"
|
||||
@@ -190,8 +170,12 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(key: "previousServers", defaultValue: [])
|
||||
var previousServers: [String]
|
||||
|
||||
var defaultServer: String {
|
||||
previousServers.first ?? accountProviders[0]
|
||||
var defaultAccountProvider: AccountProvider {
|
||||
if allowOtherAccountProviders {
|
||||
previousServers.first.map { .generic($0) } ?? accountProviders[0]
|
||||
} else {
|
||||
accountProviders[0]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Security
|
||||
@@ -270,12 +254,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference
|
||||
var voipPushGatewayNotifyURL: String?
|
||||
|
||||
@UserPreference(defaultValue: true)
|
||||
var enableNotifications: Bool
|
||||
|
||||
@UserPreference(defaultValue: true)
|
||||
var enableInAppNotifications: Bool
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
var hideQuietNotificationAlerts: Bool
|
||||
|
||||
@@ -287,9 +265,9 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference
|
||||
var lastNotificationBootTime: TimeInterval?
|
||||
|
||||
/// The sound played when delivering noisy notifications. If nil, use the ElementX default
|
||||
@UserPreference
|
||||
var selectedNotificationTone: NotificationTone?
|
||||
/// The app icon badge value the app last computed from the SDK's unread notification counts.
|
||||
@UserPreference(defaultValue: 0)
|
||||
var lastKnownBadgeCount: Int
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
@@ -348,9 +326,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(defaultValue: false)
|
||||
var hasRunNotificationPermissionsOnboarding: Bool
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
var hasRunIdentityConfirmationOnboarding: Bool
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
var hasRequestedLocationAlwaysLocationAuthorization: Bool
|
||||
|
||||
@@ -368,14 +343,6 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(defaultValue: false)
|
||||
var liveLocationDisclaimerDisplayed: Bool
|
||||
|
||||
// MARK: - Home Screen
|
||||
|
||||
@UserPreference(defaultValue: RoomListActivityVisibility.current)
|
||||
var roomListActivityVisibility: RoomListActivityVisibility
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
var roomListNotificationCountEnabled: Bool
|
||||
|
||||
// MARK: - Room Screen
|
||||
|
||||
@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.
|
||||
private(set) var mapTilerConfiguration = RemotePreference(AppSettings.bundledMapTilerConfiguration)
|
||||
|
||||
// MARK: - Presence
|
||||
|
||||
@UserPreference(defaultValue: true)
|
||||
var sharePresence: Bool
|
||||
|
||||
// MARK: - Feature Flags
|
||||
|
||||
/// Others
|
||||
@@ -452,7 +414,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(defaultValue: false)
|
||||
var messageMultiSelectEnabled: Bool
|
||||
|
||||
@UserPreference(defaultValue: ProcessInfo().isiOSAppOnMac)
|
||||
@UserPreference(key: "globalSearchEnabledV2", defaultValue: true)
|
||||
var globalSearchEnabled: Bool
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
@@ -472,22 +434,32 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
@UserPreference(defaultValue: false)
|
||||
var linkNewDeviceEnabled: Bool
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
var automaticBackPaginationEnabled: Bool
|
||||
|
||||
@UserPreference(key: "clientPausingAndResumingEnabledV2", defaultValue: false, volatile: true)
|
||||
var clientPausingAndResumingEnabled: Bool
|
||||
|
||||
@UserPreference(defaultValue: AppBuildType.current != .release)
|
||||
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) {
|
||||
self.store = store
|
||||
}
|
||||
|
||||
func userSettings(for userID: String) -> UserSettings {
|
||||
UserSettings(appSettings: self, accountSettings: .init(userID: userID, store: store))
|
||||
}
|
||||
|
||||
static func volatile() -> AppSettings {
|
||||
AppSettings(store: VolatileUserDefaults())
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension AppSettings: CommonSettingsProtocol { }
|
||||
|
||||
@@ -23,6 +23,10 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let flowParameters: CommonFlowParameters
|
||||
|
||||
private var userSettings: UserSettings {
|
||||
flowParameters.userSession.userSettings
|
||||
}
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
@@ -73,8 +77,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentSettingsScreen(animated: Bool) {
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||
appSettings: flowParameters.appSettings,
|
||||
appHooks: flowParameters.appHooks,
|
||||
isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled,
|
||||
isInSecondaryWindow: isInSecondaryWindow,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
@@ -86,42 +88,52 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .logout:
|
||||
actionsSubject.send(.runLogoutFlow)
|
||||
case .secureBackup:
|
||||
startEncryptionSettingsFlow()
|
||||
|
||||
case .userDetails:
|
||||
presentUserDetailsEditScreen()
|
||||
case let .userStatusEmojiPicker(continuation):
|
||||
presentEmojiPicker(emojiPickerContinuation: continuation)
|
||||
case .linkNewDevice:
|
||||
startLinkNewDeviceFlow()
|
||||
|
||||
case .addAccount:
|
||||
break // Nothing to present until the add account flow exists.
|
||||
|
||||
case let .manageAccount(url):
|
||||
presentAccountManagementURL(url)
|
||||
case .analytics:
|
||||
presentAnalyticsScreen()
|
||||
case .appLock:
|
||||
presentAppLockSetupFlow()
|
||||
case .linkNewDevice:
|
||||
startLinkNewDeviceFlow()
|
||||
case .notifications:
|
||||
presentNotificationSettings()
|
||||
case .secureBackup:
|
||||
startEncryptionSettingsFlow()
|
||||
case .moderationAndSafety:
|
||||
presentModerationAndSafetySettings()
|
||||
case .bugReport:
|
||||
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .push(navigationStackCoordinator),
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
bugReportService: flowParameters.bugReportService,
|
||||
userSession: flowParameters.userSession))
|
||||
bugReportFlowCoordinator?.start()
|
||||
case .about:
|
||||
presentLegalInformationScreen()
|
||||
case .blockedUsers:
|
||||
presentBlockedUsersScreen()
|
||||
case .notifications:
|
||||
presentNotificationSettings()
|
||||
case .advancedSettings:
|
||||
presentAdvancedSettings()
|
||||
case .labs:
|
||||
presentLabs()
|
||||
case .developerOptions:
|
||||
presentDeveloperOptions()
|
||||
|
||||
case .logout:
|
||||
actionsSubject.send(.runLogoutFlow)
|
||||
case .deactivateAccount:
|
||||
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)
|
||||
@@ -130,7 +142,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentLabs() {
|
||||
let coordinator = LabsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings))
|
||||
let coordinator = LabsScreenCoordinator(parameters: .init(userSettings: userSettings))
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
switch action {
|
||||
@@ -145,8 +157,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func startEncryptionSettingsFlow() {
|
||||
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||
appSettings: flowParameters.appSettings,
|
||||
appHooks: flowParameters.appHooks,
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
@@ -183,7 +193,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
let coordinator = flowParameters.appHooks.userDetailsEditScreenHook
|
||||
.makeCoordinator(parameters: .init(orientationManager: flowParameters.windowManager,
|
||||
userSession: flowParameters.userSession,
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings),
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: userSettings),
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
coordinator.actions
|
||||
@@ -227,7 +237,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentAnalyticsScreen() {
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings,
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(userSettings: userSettings,
|
||||
analytics: flowParameters.analytics))
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
@@ -253,11 +263,11 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentLegalInformationScreen() {
|
||||
navigationStackCoordinator.push(LegalInformationScreenCoordinator(appSettings: flowParameters.appSettings))
|
||||
navigationStackCoordinator.push(LegalInformationScreenCoordinator(userSettings: userSettings))
|
||||
}
|
||||
|
||||
private func presentBlockedUsersScreen() {
|
||||
let coordinator = BlockedUsersScreenCoordinator(parameters: .init(hideProfiles: flowParameters.appSettings.hideIgnoredUserProfiles,
|
||||
let coordinator = BlockedUsersScreenCoordinator(parameters: .init(hideProfiles: userSettings.app.hideIgnoredUserProfiles,
|
||||
userSession: flowParameters.userSession,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
@@ -268,22 +278,44 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userSession: flowParameters.userSession,
|
||||
userNotificationCenter: UNUserNotificationCenter.current(),
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
isModallyPresented: false,
|
||||
appSettings: flowParameters.appSettings)
|
||||
isModallyPresented: false)
|
||||
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentAdvancedSettings() {
|
||||
let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings,
|
||||
analytics: flowParameters.analytics,
|
||||
clientProxy: flowParameters.userSession.clientProxy,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
private func presentModerationAndSafetySettings() {
|
||||
let parameters = ModerationAndSafetySettingsScreenCoordinatorParameters(userSession: flowParameters.userSession,
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = ModerationAndSafetySettingsScreenCoordinator(parameters: parameters)
|
||||
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)
|
||||
}
|
||||
|
||||
private func presentDeveloperOptions() {
|
||||
let coordinator = DeveloperOptionsScreenCoordinator(appSettings: flowParameters.appSettings,
|
||||
let coordinator = DeveloperOptionsScreenCoordinator(appSettings: userSettings.app,
|
||||
appHooks: flowParameters.appHooks,
|
||||
clientProxy: flowParameters.userSession.clientProxy)
|
||||
|
||||
@@ -329,7 +361,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
accountSettingsPresenter = OAuthAccountSettingsPresenter(accountURL: url,
|
||||
presentationAnchor: flowParameters.windowManager.mainWindow,
|
||||
appMediator: flowParameters.appMediator,
|
||||
appSettings: flowParameters.appSettings,
|
||||
userSettings: userSettings,
|
||||
appHooks: flowParameters.appHooks,
|
||||
continuation: continuation)
|
||||
accountSettingsPresenter?.start()
|
||||
|
||||
+13
-10
@@ -40,27 +40,30 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
canReportProblem = isBugReportServiceEnabled
|
||||
|
||||
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
|
||||
let classicAppAccountProvider = authenticationService.classicAppAccount?.serverName
|
||||
let isClassicAppAccountAllowed = classicAppAccountProvider.map { appSettings.accountProviders.contains($0) } ?? false
|
||||
let isClassicAppAccountAllowed = if let classicAppAccount = authenticationService.classicAppAccount {
|
||||
appSettings.accountProviders.contains(serverName: classicAppAccount.serverName, orBaseURL: classicAppAccount.homeserverURL)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
let initialViewState = if !appSettings.allowOtherAccountProviders {
|
||||
// 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.
|
||||
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil,
|
||||
AuthenticationStartScreenViewState(serverNameOrBaseURL: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0].serverNameOrBaseURL : nil,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
classicAppMode: isClassicAppAccountAllowed ? authenticationService.classicAppAccount.map { .welcomeBack($0) } : nil,
|
||||
hideBrandChrome: appSettings.hideBrandChrome)
|
||||
} else if let provisioningParameters {
|
||||
// We only show the "Sign in to …" button when using a provisioning link.
|
||||
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider,
|
||||
AuthenticationStartScreenViewState(serverNameOrBaseURL: provisioningParameters.accountProvider,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: false,
|
||||
classicAppMode: nil,
|
||||
hideBrandChrome: appSettings.hideBrandChrome)
|
||||
} else {
|
||||
// The default configuration.
|
||||
AuthenticationStartScreenViewState(serverName: nil,
|
||||
AuthenticationStartScreenViewState(serverNameOrBaseURL: nil,
|
||||
showCreateAccountButton: appSettings.showCreateAccountButton,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
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
|
||||
// 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.
|
||||
await configureAccountProvider(classicAppAccount.serverName,
|
||||
await loginDirectly(using: classicAppAccount.serverName,
|
||||
loginHint: "mxid:\(classicAppAccount.userID)",
|
||||
fallbackHomeserverURL: classicAppAccount.homeserverURL)
|
||||
} else if let serverName = state.serverName {
|
||||
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
|
||||
} else if let serverNameOrBaseURL = state.serverNameOrBaseURL {
|
||||
await loginDirectly(using: serverNameOrBaseURL, loginHint: provisioningParameters?.loginHint)
|
||||
} else {
|
||||
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()
|
||||
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.
|
||||
if let fallbackHomeserverURL,
|
||||
case .success = await authenticationService.configure(for: fallbackHomeserverURL.absoluteString, flow: .login) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import SwiftUI
|
||||
|
||||
struct SettingsScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let appSettings: AppSettings
|
||||
let appHooks: AppHooks
|
||||
let isBugReportServiceEnabled: Bool
|
||||
let isInSecondaryWindow: Bool
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
@@ -20,22 +18,30 @@ struct SettingsScreenCoordinatorParameters {
|
||||
|
||||
enum SettingsScreenCoordinatorAction {
|
||||
case dismiss
|
||||
case logout
|
||||
case secureBackup
|
||||
|
||||
case userDetails
|
||||
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||
case analytics
|
||||
case appLock
|
||||
case bugReport
|
||||
case about
|
||||
case blockedUsers
|
||||
case linkNewDevice
|
||||
|
||||
case addAccount
|
||||
|
||||
case manageAccount(url: URL)
|
||||
case linkNewDevice
|
||||
case notifications
|
||||
case advancedSettings
|
||||
case labs
|
||||
case developerOptions
|
||||
case secureBackup
|
||||
case moderationAndSafety
|
||||
case bugReport
|
||||
|
||||
case logout
|
||||
case deactivateAccount
|
||||
|
||||
case mediaUploadQuality
|
||||
case appLock
|
||||
case locationSharing
|
||||
case analytics
|
||||
case labs
|
||||
case about
|
||||
|
||||
case developerOptions
|
||||
}
|
||||
|
||||
final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -52,8 +58,6 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||
viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
|
||||
appSettings: parameters.appSettings,
|
||||
appHooks: parameters.appHooks,
|
||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||
isInSecondaryWindow: parameters.isInSecondaryWindow,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
@@ -65,38 +69,48 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .close:
|
||||
actionsSubject.send(.dismiss)
|
||||
|
||||
case .userDetails:
|
||||
actionsSubject.send(.userDetails)
|
||||
case let .userStatusEmojiPicker(continuation):
|
||||
actionsSubject.send(.userStatusEmojiPicker(continuation))
|
||||
case .linkNewDevice:
|
||||
actionsSubject.send(.linkNewDevice)
|
||||
|
||||
case .addAccount:
|
||||
actionsSubject.send(.addAccount)
|
||||
|
||||
case let .manageAccount(url):
|
||||
actionsSubject.send(.manageAccount(url: url))
|
||||
case .analytics:
|
||||
actionsSubject.send(.analytics)
|
||||
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 .linkNewDevice:
|
||||
actionsSubject.send(.linkNewDevice)
|
||||
case .notifications:
|
||||
actionsSubject.send(.notifications)
|
||||
case .advancedSettings:
|
||||
actionsSubject.send(.advancedSettings)
|
||||
case .labs:
|
||||
actionsSubject.send(.labs)
|
||||
case .developerOptions:
|
||||
actionsSubject.send(.developerOptions)
|
||||
case .secureBackup:
|
||||
actionsSubject.send(.secureBackup)
|
||||
case .moderationAndSafety:
|
||||
actionsSubject.send(.moderationAndSafety)
|
||||
case .reportBug:
|
||||
actionsSubject.send(.bugReport)
|
||||
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
case .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)
|
||||
|
||||
Submodule
+1
Submodule Enterprise added at 8b65ca7f13
@@ -6,36 +6,46 @@
|
||||
//
|
||||
|
||||
import CallKit
|
||||
import ElementCall
|
||||
@testable import ElementX
|
||||
import PushKit
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
final class ElementCallServiceTests {
|
||||
private var appSettings: AppSettings!
|
||||
private var callProvider: CXProviderMock!
|
||||
private var callController: CXCallControllerMock!
|
||||
private var currentDate: Date!
|
||||
private var clock: ManualClock!
|
||||
private var pushRegistry: PKPushRegistry!
|
||||
private var service: ElementCallService!
|
||||
private var userSession: UserSessionMock!
|
||||
|
||||
init() {
|
||||
appSettings = AppSettings.volatile()
|
||||
pushRegistry = PKPushRegistry(queue: nil)
|
||||
callProvider = CXProviderMock(.init())
|
||||
callController = CXCallControllerMock(.init())
|
||||
currentDate = Date()
|
||||
clock = ManualClock()
|
||||
let dateProvider: () -> Date = {
|
||||
self.currentDate
|
||||
}
|
||||
service = ElementCallService(callProvider: callProvider,
|
||||
timeProvider: TimeProvider(clock: clock, now: dateProvider),
|
||||
appSettings: AppSettings.volatile())
|
||||
service = ElementCallService(appSettings: appSettings,
|
||||
callProvider: callProvider,
|
||||
callController: callController,
|
||||
timeProvider: TimeProvider(clock: clock, now: dateProvider))
|
||||
}
|
||||
|
||||
isolated deinit {
|
||||
appSettings = nil
|
||||
callProvider = nil
|
||||
callController = nil
|
||||
currentDate = nil
|
||||
clock = nil
|
||||
pushRegistry = nil
|
||||
userSession = nil
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,7 +209,7 @@ final class ElementCallServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func setupCallSessionCancelsPendingUnansweredTimeout() async {
|
||||
func setupCallSessionCancelsPendingUnansweredTimeout() async throws {
|
||||
// Schedule the 60s unanswered timer via an incoming push
|
||||
await waitForConfirmation { confirmation in
|
||||
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
|
||||
// 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
|
||||
if reason == .unanswered {
|
||||
unansweredFired = true
|
||||
}
|
||||
endedCallsContinuation.yield(reason)
|
||||
}
|
||||
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
|
||||
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
|
||||
@@ -246,7 +254,7 @@ final class ElementCallServiceTests {
|
||||
@Test
|
||||
func duplicateRoomPushReportsCallAsHandled() async {
|
||||
// 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)
|
||||
await expectImmediatelyEndedCallReported(forPayload: pushPayload, expectedReason: .answeredElsewhere)
|
||||
|
||||
@@ -257,6 +265,271 @@ final class ElementCallServiceTests {
|
||||
#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,
|
||||
expectedReason: CXCallEndedReason) async {
|
||||
let baselineNewIncomingCount = callProvider.reportNewIncomingCallWithUpdateCompletionCallsCount
|
||||
|
||||
@@ -23,7 +23,6 @@ nonisolated struct NotificationContentBuilderTests {
|
||||
style: .plain)
|
||||
mediaProvider = MediaProviderMock(.init())
|
||||
notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder,
|
||||
notificationSoundName: UNNotificationSoundName("message.caf"),
|
||||
userSession: NSEUserSessionMock(.init()))
|
||||
}
|
||||
|
||||
@@ -226,6 +225,7 @@ nonisolated struct NotificationContentBuilderTests {
|
||||
mutating func liveLocationStartNotification() async {
|
||||
let event = TimelineEventSDKMock()
|
||||
event.eventIdReturnValue = UUID().uuidString
|
||||
event.timestampReturnValue = 0
|
||||
event.contentReturnValue = .state(content: .beaconInfo)
|
||||
|
||||
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
|
||||
@@ -253,6 +253,7 @@ nonisolated struct NotificationContentBuilderTests {
|
||||
mutating func otherStateEventNotification() async {
|
||||
let event = TimelineEventSDKMock()
|
||||
event.eventIdReturnValue = UUID().uuidString
|
||||
event.timestampReturnValue = 0
|
||||
event.contentReturnValue = .state(content: .roomName)
|
||||
|
||||
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
|
||||
|
||||
@@ -172,13 +172,37 @@ struct SettingsScreenViewModelTests {
|
||||
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
|
||||
|
||||
private mutating func setupViewModel(status: UserStatus = .init()) {
|
||||
private mutating func setupViewModel(status: UserStatus = .init(), userSettings: UserSettings = .mock()) {
|
||||
clientProxy = ClientProxyMock(.init(userID: "", status: status))
|
||||
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
appSettings: AppSettings.volatile(),
|
||||
appHooks: AppHooks(),
|
||||
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings)),
|
||||
isBugReportServiceEnabled: true,
|
||||
isInSecondaryWindow: false,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
@@ -227,4 +251,11 @@ private extension SettingsScreenViewModelAction {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isAddAccount: Bool {
|
||||
switch self {
|
||||
case .addAccount: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "7a6117b3894e217e5bc0fe7497b0b868c49d51f41e0d8be554dc766cb9566ec9",
|
||||
"originHash" : "43fad9d39aa61aa7add5e5768908e9ed7948ca1b8e8b61cf9b7b585f4e8fd1a7",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "compound-design-tokens",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-design-tokens",
|
||||
"state" : {
|
||||
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419",
|
||||
"version" : "11.0.0"
|
||||
"revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,8 +33,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6",
|
||||
"version" : "1.19.4"
|
||||
"revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
|
||||
"version" : "1.19.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
|
||||
"version" : "603.0.2"
|
||||
"revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
|
||||
"version" : "604.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||
"state" : {
|
||||
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840",
|
||||
"version" : "26.0.2"
|
||||
"revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
|
||||
"version" : "27.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "7a6117b3894e217e5bc0fe7497b0b868c49d51f41e0d8be554dc766cb9566ec9",
|
||||
"originHash" : "43fad9d39aa61aa7add5e5768908e9ed7948ca1b8e8b61cf9b7b585f4e8fd1a7",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "compound-design-tokens",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-design-tokens",
|
||||
"state" : {
|
||||
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419",
|
||||
"version" : "11.0.0"
|
||||
"revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,8 +33,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6",
|
||||
"version" : "1.19.4"
|
||||
"revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
|
||||
"version" : "1.19.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
|
||||
"version" : "603.0.2"
|
||||
"revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
|
||||
"version" : "604.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||
"state" : {
|
||||
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840",
|
||||
"version" : "26.0.2"
|
||||
"revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
|
||||
"version" : "27.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,11 +9,11 @@ let package = Package(
|
||||
.library(name: "Compound", targets: ["Compound"])
|
||||
],
|
||||
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(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/pointfreeco/swift-snapshot-testing", exact: "1.19.4")
|
||||
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", exact: "1.19.6")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
Reference in New Issue
Block a user