Merge branch 'develop' into feature/bma/notificationCustomSound
This commit is contained in:
commit
c1f3233dce
66 changed files with 894 additions and 590 deletions
24
.github/workflows/maestro-local.yml
vendored
24
.github/workflows/maestro-local.yml
vendored
|
|
@ -18,9 +18,8 @@ jobs:
|
||||||
build-apk:
|
build-apk:
|
||||||
name: Build APK
|
name: Build APK
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Allow one per PR.
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('maestro-{0}', github.ref) }}
|
group: ${{ format('maestro-build-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
@ -57,10 +56,10 @@ jobs:
|
||||||
name: Maestro test suite
|
name: Maestro test suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ build-apk ]
|
needs: [ build-apk ]
|
||||||
# Allow one per PR.
|
# Allow only one to run at a time, since they use the same environment.
|
||||||
|
# Otherwise, tests running in parallel can break each other.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('maestro-{0}', github.ref) }}
|
group: maestro-test
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
|
|
@ -110,6 +109,21 @@ jobs:
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
overwrite: true
|
overwrite: true
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
- name: Update summary (success)
|
||||||
|
if: steps.maestro_test.outcome == 'success'
|
||||||
|
run: |
|
||||||
|
echo "### Maestro tests worked :rocket:!"
|
||||||
|
- name: Update summary (failure)
|
||||||
|
if: steps.maestro_test.outcome != 'success'
|
||||||
|
run: |
|
||||||
|
LOG_FILE=$(find ~/.maestro/tests/ -name maestro.log)
|
||||||
|
echo "Log file: $LOG_FILE"
|
||||||
|
LOG_LINES="$(tail -n 30 $LOG_FILE)"
|
||||||
|
echo "### :x: Maestro tests failed...
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
$LOG_LINES
|
||||||
|
\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Fail the workflow in case of error in test
|
- name: Fail the workflow in case of error in test
|
||||||
if: steps.maestro_test.outcome != 'success'
|
if: steps.maestro_test.outcome != 'success'
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
# Please see LICENSE in the repository root for full details.
|
# Please see LICENSE in the repository root for full details.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# First we disable the onboarding flow on Chrome, which is a source of issues
|
||||||
|
# (see https://stackoverflow.com/a/64629745)
|
||||||
|
echo "Disabling Chrome onboarding flow"
|
||||||
|
adb shell am set-debug-app --persistent com.android.chrome
|
||||||
|
adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line'
|
||||||
|
adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main
|
||||||
|
|
||||||
adb install -r $1
|
adb install -r $1
|
||||||
echo "Starting the screen recording..."
|
echo "Starting the screen recording..."
|
||||||
adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/
|
adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,6 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "login-continue"
|
id: "login-continue"
|
||||||
## MAS page
|
## MAS page
|
||||||
## Conditional workflow to pass the Chrome first launch welcome page.
|
|
||||||
- retry:
|
|
||||||
maxRetries: 3
|
|
||||||
commands:
|
|
||||||
- runFlow:
|
|
||||||
when:
|
|
||||||
visible: 'Use without an account'
|
|
||||||
commands:
|
|
||||||
- tapOn: "Use without an account"
|
|
||||||
## For older chrome versions
|
|
||||||
- runFlow:
|
|
||||||
when:
|
|
||||||
visible: 'Accept & continue'
|
|
||||||
commands:
|
|
||||||
- tapOn: "Accept & continue"
|
|
||||||
- runFlow:
|
|
||||||
when:
|
|
||||||
visible: 'No thanks'
|
|
||||||
commands:
|
|
||||||
- tapOn: "No thanks"
|
|
||||||
## Working when running Maestro locally, but not on the CI yet.
|
|
||||||
- retry:
|
- retry:
|
||||||
maxRetries: 3
|
maxRetries: 3
|
||||||
commands:
|
commands:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "Enter recovery key"
|
||||||
|
timeout: 30000
|
||||||
- takeScreenshot: build/maestro/150-Verify
|
- takeScreenshot: build/maestro/150-Verify
|
||||||
- tapOn: "Enter recovery key"
|
- tapOn: "Enter recovery key"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
@ -7,7 +10,10 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- inputText: ${MAESTRO_RECOVERY_KEY}
|
- inputText: ${MAESTRO_RECOVERY_KEY}
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
- extendedWaitUntil:
|
- retry:
|
||||||
visible: "Device verified"
|
maxRetries: 3
|
||||||
timeout: 30000
|
commands:
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "Device verified"
|
||||||
|
timeout: 30000
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Be in your element"
|
visible: "Be in your element"
|
||||||
timeout: 10000
|
timeout: 30000
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Confirm your identity"
|
visible: "Confirm your identity"
|
||||||
timeout: 20000
|
timeout: 60000
|
||||||
|
|
|
||||||
74
CHANGES.md
74
CHANGES.md
|
|
@ -1,3 +1,77 @@
|
||||||
|
Changes in Element X v26.02.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.02.0 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* When a background SDK task fails, react in the client by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6166
|
||||||
|
* Enable space feature flags by default by @ganfra in https://github.com/element-hq/element-x-android/pull/6171
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Improve space management with pagination and partial failure handling by @ganfra in https://github.com/element-hq/element-x-android/pull/6099
|
||||||
|
* Iterate on QrCode login error buttons by @bmarty in https://github.com/element-hq/element-x-android/pull/6101
|
||||||
|
* Update icon shown for world_readable rooms by @richvdh in https://github.com/element-hq/element-x-android/pull/6111
|
||||||
|
* QRCode login: treat not found error as expired error. by @bmarty in https://github.com/element-hq/element-x-android/pull/6161
|
||||||
|
* Iterate on Space related UI by @ganfra in https://github.com/element-hq/element-x-android/pull/6150
|
||||||
|
### 🔒 Security
|
||||||
|
* Ensure aspect ratio of images in the timeline is restricted by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6168
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Ensure that Element Call activity is not closed when using an external link by @bmarty in https://github.com/element-hq/element-x-android/pull/6114
|
||||||
|
* Refresh a Space's room list after creating a room in it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6135
|
||||||
|
* When creating a DM, set room history visibility to `invited` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6138
|
||||||
|
* Fix back navigation after creating a room in a space by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6134
|
||||||
|
* Fix `LinkifyHelper` index out of bounds with parenthesis by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6140
|
||||||
|
* Change role screen won't be dismissed until changes take effect by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6141
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6122
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6155
|
||||||
|
### 🧱 Build
|
||||||
|
* Try fixing Maestro tests (again) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6149
|
||||||
|
* Add a stale bot for X-Needs-Info issues. by @bmarty in https://github.com/element-hq/element-x-android/pull/6153
|
||||||
|
* [Release script] Ensure that the release version will match the next Monday date by @bmarty in https://github.com/element-hq/element-x-android/pull/6152
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Add Space Filters feature for Room List by @ganfra in https://github.com/element-hq/element-x-android/pull/6136
|
||||||
|
* Add history sharing badges to room details by @kaylendog in https://github.com/element-hq/element-x-android/pull/6132
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency androidx.work:work-runtime-ktx to v2.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6105
|
||||||
|
* Update metro to v0.10.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6106
|
||||||
|
* Update camera to v1.5.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6103
|
||||||
|
* Update activity to v1.12.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6104
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.30.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6120
|
||||||
|
* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6102
|
||||||
|
* Update roborazzi to v1.58.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6124
|
||||||
|
* Update kover by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6139
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.31.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6145
|
||||||
|
* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6142
|
||||||
|
* Update media3 to v1.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6151
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.02.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6144
|
||||||
|
* Update firebaseAppDistribution to v5.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6146
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v34.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6148
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.32.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6157
|
||||||
|
* Update metro to v0.10.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6164
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.2.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6169
|
||||||
|
* chore(deps): update plugin paparazzi to v2.0.0-alpha04 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6048
|
||||||
|
* fix(deps): update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6173
|
||||||
|
* fix(deps): update haze to v1.7.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6175
|
||||||
|
### Others
|
||||||
|
* Improve favorite wording and icon of room by @bmarty in https://github.com/element-hq/element-x-android/pull/6097
|
||||||
|
* Add special flow for leaving a space as the last owner by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6112
|
||||||
|
* Remove `runBlocking` in `ThreadedMessagesNode` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6108
|
||||||
|
* Revert "Add "call.pro.element.io" in the list of known hosts for Element Call." by @bmarty in https://github.com/element-hq/element-x-android/pull/6118
|
||||||
|
* Refactor room list filtering to use Rust SDK by @ganfra in https://github.com/element-hq/element-x-android/pull/6117
|
||||||
|
* Ensure http 429 are retried 3 times before failing. by @bmarty in https://github.com/element-hq/element-x-android/pull/6119
|
||||||
|
* Remove `JoinRule.Private` from the codebase by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6129
|
||||||
|
* Fix voice message recording not starting after permission is granted by @kknappe in https://github.com/element-hq/element-x-android/pull/6109
|
||||||
|
* Use correct bg color. by @bmarty in https://github.com/element-hq/element-x-android/pull/6165
|
||||||
|
* Document "Developer options" and remove outdated instructions by @MadLittleMods in https://github.com/element-hq/element-x-android/pull/6162
|
||||||
|
* Update SpaceFilterButton selected state color by @ganfra in https://github.com/element-hq/element-x-android/pull/6178
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @kknappe made their first contribution in https://github.com/element-hq/element-x-android/pull/6109
|
||||||
|
* @MadLittleMods made their first contribution in https://github.com/element-hq/element-x-android/pull/6162
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.01.2...v26.02.0
|
||||||
|
|
||||||
Changes in Element X v26.01.2
|
Changes in Element X v26.01.2
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -211,8 +210,6 @@ class LoggedInFlowNode(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
analyticsRoomListStateWatcher.start()
|
analyticsRoomListStateWatcher.start()
|
||||||
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||||
// TODO We do not support Space yet, so directly navigate to main space
|
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
|
||||||
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
|
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
|
||||||
matrixClient.sessionVerificationService.setListener(verificationListener)
|
matrixClient.sessionVerificationService.setListener(verificationListener)
|
||||||
mediaPreviewConfigMigration()
|
mediaPreviewConfigMigration()
|
||||||
|
|
@ -242,7 +239,6 @@ class LoggedInFlowNode(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
appNavigationStateService.onLeavingSpace(id)
|
|
||||||
appNavigationStateService.onLeavingSession(id)
|
appNavigationStateService.onLeavingSession(id)
|
||||||
loggedInFlowProcessor.stopObserving()
|
loggedInFlowProcessor.stopObserving()
|
||||||
matrixClient.sessionVerificationService.setListener(null)
|
matrixClient.sessionVerificationService.setListener(null)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
* [Logging](#logging)
|
* [Logging](#logging)
|
||||||
* [Translations](#translations)
|
* [Translations](#translations)
|
||||||
* [Rageshake](#rageshake)
|
* [Rageshake](#rageshake)
|
||||||
|
* [Developer options](#developer-options)
|
||||||
* [Tips](#tips)
|
* [Tips](#tips)
|
||||||
* [Happy coding!](#happy-coding)
|
* [Happy coding!](#happy-coding)
|
||||||
|
|
||||||
|
|
@ -409,14 +410,31 @@ The data will be sent to an internal server, which is not publicly accessible. A
|
||||||
|
|
||||||
Rageshake can be very useful to get logs from a release version of the application.
|
Rageshake can be very useful to get logs from a release version of the application.
|
||||||
|
|
||||||
|
|
||||||
|
#### Developer options
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Developer options can result in unexpected application behavior or destructive
|
||||||
|
> actions. Use with caution and only if you are instructed by someone at Element or are
|
||||||
|
> already familiar.
|
||||||
|
|
||||||
|
These options provide advanced controls for testing and debugging. They are visible by
|
||||||
|
default in debug and nightly builds but are hidden in release versions.
|
||||||
|
|
||||||
|
**Enabling in release builds:** Navigate to application settings and tap the version
|
||||||
|
number at the bottom 7 times. After tapping, a new "Developer options" entry will appear
|
||||||
|
at the bottom of the list.
|
||||||
|
|
||||||
|
The developer options include feature flags, notification/push history, Element call
|
||||||
|
customization, Rust SDK log levels, per-feature tracing toggles, Showkase to debug UI
|
||||||
|
components, rageshake controls, app crash controls, cache details/controls, persistent
|
||||||
|
storage maintenance tasks.
|
||||||
|
|
||||||
|
Keywords: Developer settings, developer mode
|
||||||
|
|
||||||
|
|
||||||
### Tips
|
### Tips
|
||||||
|
|
||||||
- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here; (TODO Not supported yet!)
|
|
||||||
- Show hidden Events can also help to debug feature. When developer mode is enabled, it is possible to view the source (= the Json content) of any Events; (TODO
|
|
||||||
Not supported yet!)
|
|
||||||
- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; (TODO Not supported yet!)
|
|
||||||
- Hidden debug menu: when developer mode is enabled and on debug build, there are some extra screens that can be accessible using the green wheel. In those
|
|
||||||
screens, it will be possible to toggle some feature flags; (TODO Not supported yet!)
|
|
||||||
- Using logcat, filtering with `Compositions` can help you to understand what screen are currently displayed on your device. Searching for string displayed on
|
- Using logcat, filtering with `Compositions` can help you to understand what screen are currently displayed on your device. Searching for string displayed on
|
||||||
the screen can also help to find the running code in the codebase.
|
the screen can also help to find the running code in the codebase.
|
||||||
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202602000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202602000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -29,6 +29,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA"
|
const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var activeCallManager: ActiveCallManager
|
lateinit var activeCallManager: ActiveCallManager
|
||||||
|
|
||||||
|
|
@ -40,7 +41,13 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
|
||||||
?: return
|
?: return
|
||||||
context.bindings<CallBindings>().inject(this)
|
context.bindings<CallBindings>().inject(this)
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
activeCallManager.hangUpCall(
|
||||||
|
callType = CallType.RoomCall(
|
||||||
|
sessionId = notificationData.sessionId,
|
||||||
|
roomId = notificationData.roomId,
|
||||||
|
),
|
||||||
|
notificationData = notificationData,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class CallScreenPresenter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
appCoroutineScope.launch { activeCallManager.hungUpCall(callType) }
|
appCoroutineScope.launch { activeCallManager.hangUpCall(callType) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ class IncomingCallActivity : AppCompatActivity() {
|
||||||
private fun onCancel() {
|
private fun onCancel() {
|
||||||
val activeCall = activeCallManager.activeCall.value ?: return
|
val activeCall = activeCallManager.activeCall.value ?: return
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
activeCallManager.hungUpCall(callType = activeCall.callType)
|
activeCallManager.hangUpCall(callType = activeCall.callType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=16501-5740
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IncomingCallScreen(
|
internal fun IncomingCallScreen(
|
||||||
notificationData: CallNotificationData,
|
notificationData: CallNotificationData,
|
||||||
|
|
@ -94,11 +97,8 @@ internal fun IncomingCallScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(bottom = 64.dp),
|
||||||
.fillMaxWidth()
|
horizontalArrangement = Arrangement.spacedBy(48.dp),
|
||||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
size = 64.dp,
|
size = 64.dp,
|
||||||
|
|
@ -108,7 +108,6 @@ internal fun IncomingCallScreen(
|
||||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
||||||
borderColor = ElementTheme.colors.borderSuccessSubtle
|
borderColor = ElementTheme.colors.borderSuccessSubtle
|
||||||
)
|
)
|
||||||
|
|
||||||
ActionButton(
|
ActionButton(
|
||||||
size = 64.dp,
|
size = 64.dp,
|
||||||
onClick = onCancel,
|
onClick = onCancel,
|
||||||
|
|
@ -143,7 +142,7 @@ private fun ActionButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
colors = IconButtonDefaults.filledIconButtonColors(
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
containerColor = backgroundColor,
|
containerColor = backgroundColor,
|
||||||
contentColor = Color.White,
|
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,14 @@ interface ActiveCallManager {
|
||||||
suspend fun registerIncomingCall(notificationData: CallNotificationData)
|
suspend fun registerIncomingCall(notificationData: CallNotificationData)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the active call has been hung up. It will remove any existing UI and the active call.
|
* Called to hang up the active call. It will hang up the call and remove any existing UI and the active call.
|
||||||
* @param callType The type of call that the user hung up, either an external url one or a room one.
|
* @param callType The type of call that the user hangs up, either an external url one or a room one.
|
||||||
|
* @param notificationData The data for the incoming call notification.
|
||||||
*/
|
*/
|
||||||
suspend fun hungUpCall(callType: CallType)
|
suspend fun hangUpCall(
|
||||||
|
callType: CallType,
|
||||||
|
notificationData: CallNotificationData? = null,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall].
|
* Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall].
|
||||||
|
|
@ -192,12 +196,28 @@ class DefaultActiveCallManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun hungUpCall(callType: CallType) = mutex.withLock {
|
override suspend fun hangUpCall(
|
||||||
Timber.tag(tag).d("Hung up call: $callType")
|
callType: CallType,
|
||||||
|
notificationData: CallNotificationData?,
|
||||||
|
) = mutex.withLock {
|
||||||
|
Timber.tag(tag).d("Hang up call: $callType")
|
||||||
|
cancelIncomingCallNotification()
|
||||||
val currentActiveCall = activeCall.value ?: run {
|
val currentActiveCall = activeCall.value ?: run {
|
||||||
|
// activeCall.value can be null if the application has been killed while the call was ringing
|
||||||
|
// Build a currentActiveCall with the provided parameters.
|
||||||
|
notificationData?.let {
|
||||||
|
ActiveCall(
|
||||||
|
callType = callType,
|
||||||
|
callState = CallState.Ringing(
|
||||||
|
notificationData = notificationData,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
Timber.tag(tag).w("No active call, ignoring hang up")
|
Timber.tag(tag).w("No active call, ignoring hang up")
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentActiveCall.callType != callType) {
|
if (currentActiveCall.callType != callType) {
|
||||||
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
|
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
|
||||||
return@withLock
|
return@withLock
|
||||||
|
|
@ -208,9 +228,13 @@ class DefaultActiveCallManager(
|
||||||
matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
|
matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
|
||||||
?.getRoom(notificationData.roomId)
|
?.getRoom(notificationData.roomId)
|
||||||
?.declineCall(notificationData.eventId)
|
?.declineCall(notificationData.eventId)
|
||||||
|
?.onFailure {
|
||||||
|
Timber.e(it, "Failed to decline incoming call")
|
||||||
|
}
|
||||||
|
?: run {
|
||||||
|
Timber.tag(tag).d("Couldn't find session or room to decline call for incoming call")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelIncomingCallNotification()
|
|
||||||
if (activeWakeLock?.isHeld == true) {
|
if (activeWakeLock?.isHeld == true) {
|
||||||
Timber.tag(tag).d("Releasing partial wakelock after hang up")
|
Timber.tag(tag).d("Releasing partial wakelock after hang up")
|
||||||
activeWakeLock.release()
|
activeWakeLock.release()
|
||||||
|
|
@ -221,7 +245,6 @@ class DefaultActiveCallManager(
|
||||||
|
|
||||||
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
|
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
|
||||||
Timber.tag(tag).d("Joined call: $callType")
|
Timber.tag(tag).d("Joined call: $callType")
|
||||||
|
|
||||||
cancelIncomingCallNotification()
|
cancelIncomingCallNotification()
|
||||||
if (activeWakeLock?.isHeld == true) {
|
if (activeWakeLock?.isHeld == true) {
|
||||||
Timber.tag(tag).d("Releasing partial wakelock after joining call")
|
Timber.tag(tag).d("Releasing partial wakelock after joining call")
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ class WebViewAudioManager(
|
||||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
|
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val audioDeviceComparator = Comparator<AudioDeviceInfo> { a, b ->
|
||||||
|
// If the device type is not in the wantedDeviceTypes list, we give it a high index, (i.e. low priority)
|
||||||
|
val indexOfA = wantedDeviceTypes.indexOf(a.type).let { if (it == -1) Int.MAX_VALUE else it }
|
||||||
|
val indexOfB = wantedDeviceTypes.indexOf(b.type).let { if (it == -1) Int.MAX_VALUE else it }
|
||||||
|
indexOfA.compareTo(indexOfB)
|
||||||
|
}
|
||||||
|
|
||||||
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -134,7 +141,7 @@ class WebViewAudioManager(
|
||||||
if (validNewDevices.isEmpty()) return
|
if (validNewDevices.isEmpty()) return
|
||||||
|
|
||||||
// We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list
|
// We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list
|
||||||
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }
|
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator)
|
||||||
setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo))
|
setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo))
|
||||||
// This should automatically switch to a new device if it has a higher priority than the current one
|
// This should automatically switch to a new device if it has a higher priority than the current one
|
||||||
selectDefaultAudioDevice(audioDevices)
|
selectDefaultAudioDevice(audioDevices)
|
||||||
|
|
@ -294,7 +301,7 @@ class WebViewAudioManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of available audio devices.
|
* Returns the list of available audio devices, sorted by likelihood of it being used for communication.
|
||||||
*
|
*
|
||||||
* On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback.
|
* On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback.
|
||||||
*/
|
*/
|
||||||
|
|
@ -304,7 +311,7 @@ class WebViewAudioManager(
|
||||||
} else {
|
} else {
|
||||||
val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||||
rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
|
rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
|
||||||
}
|
}.sortedWith(audioDeviceComparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -323,19 +330,12 @@ class WebViewAudioManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the default audio device based on the available devices.
|
* Selects the default audio device based on the sorted available devices.
|
||||||
*
|
*
|
||||||
* @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
|
* @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
|
||||||
*/
|
*/
|
||||||
private fun selectDefaultAudioDevice(availableDevices: List<AudioDeviceInfo> = listAudioDevices()) {
|
private fun selectDefaultAudioDevice(availableDevices: List<AudioDeviceInfo> = listAudioDevices()) {
|
||||||
val selectedDevice = availableDevices
|
val selectedDevice = availableDevices.firstOrNull()
|
||||||
.minByOrNull {
|
|
||||||
wantedDeviceTypes.indexOf(it.type).let { index ->
|
|
||||||
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
|
|
||||||
if (index == -1) Int.MAX_VALUE else index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedNewCommunicationDeviceId = selectedDevice?.id
|
expectedNewCommunicationDeviceId = selectedDevice?.id
|
||||||
audioManager.selectAudioDevice(selectedDevice)
|
audioManager.selectAudioDevice(selectedDevice)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ class DefaultActiveCallManagerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
fun `hangUpCall - removes existing call if the CallType matches`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
@ -165,7 +165,7 @@ class DefaultActiveCallManagerTest {
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
|
|
||||||
|
|
@ -192,13 +192,41 @@ class DefaultActiveCallManagerTest {
|
||||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||||
manager.registerIncomingCall(notificationData)
|
manager.registerIncomingCall(notificationData)
|
||||||
|
|
||||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
room.declineCall(notificationEventId = notificationData.eventId)
|
room.declineCall(notificationEventId = notificationData.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Decline event - Hangup on a unknown call should send a decline event`() = runTest {
|
||||||
|
setupShadowPowerManager()
|
||||||
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
|
|
||||||
|
val room = mockk<JoinedRoom>(relaxed = true)
|
||||||
|
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
|
||||||
|
|
||||||
|
val manager = createActiveCallManager(
|
||||||
|
matrixClientProvider = clientProvider,
|
||||||
|
notificationManagerCompat = notificationManagerCompat
|
||||||
|
)
|
||||||
|
|
||||||
|
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||||
|
// Do not register the incoming call, so the manager doesn't know about it
|
||||||
|
manager.hangUpCall(
|
||||||
|
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId),
|
||||||
|
notificationData = notificationData,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
room.declineCall(notificationEventId = notificationData.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `Decline event - Declining from another session should stop ringing`() = runTest {
|
fun `Decline event - Declining from another session should stop ringing`() = runTest {
|
||||||
|
|
@ -269,7 +297,7 @@ class DefaultActiveCallManagerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
fun `hangUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
@ -278,11 +306,12 @@ class DefaultActiveCallManagerTest {
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
manager.hangUpCall(CallType.ExternalUrl("https://example.com"))
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
// The notification is always cancelled do not block the user
|
||||||
|
verify(exactly = 1) { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class FakeActiveCallManager(
|
class FakeActiveCallManager(
|
||||||
var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
|
var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
|
||||||
var hungUpCallResult: (CallType) -> Unit = {},
|
var hangUpCallResult: (CallType, CallNotificationData?) -> Unit = { _, _ -> },
|
||||||
var joinedCallResult: (CallType) -> Unit = {},
|
var joinedCallResult: (CallType) -> Unit = {},
|
||||||
) : ActiveCallManager {
|
) : ActiveCallManager {
|
||||||
override val activeCall = MutableStateFlow<ActiveCall?>(null)
|
override val activeCall = MutableStateFlow<ActiveCall?>(null)
|
||||||
|
|
@ -26,8 +26,8 @@ class FakeActiveCallManager(
|
||||||
registerIncomingCallResult(notificationData)
|
registerIncomingCallResult(notificationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun hungUpCall(callType: CallType) = simulateLongTask {
|
override suspend fun hangUpCall(callType: CallType, notificationData: CallNotificationData?) = simulateLongTask {
|
||||||
hungUpCallResult(callType)
|
hangUpCallResult(callType, notificationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun joinedCall(callType: CallType) = simulateLongTask {
|
override suspend fun joinedCall(callType: CallType) = simulateLongTask {
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ class ConfigureRoomPresenterTest {
|
||||||
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
||||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||||
val spaceService = FakeSpaceService(
|
val spaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) },
|
||||||
addChildToSpaceResult = addChildToSpaceResult,
|
addChildToSpaceResult = addChildToSpaceResult,
|
||||||
)
|
)
|
||||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||||
|
|
@ -261,6 +262,7 @@ class ConfigureRoomPresenterTest {
|
||||||
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
||||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||||
val spaceService = FakeSpaceService(
|
val spaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) },
|
||||||
addChildToSpaceResult = addChildToSpaceResult,
|
addChildToSpaceResult = addChildToSpaceResult,
|
||||||
)
|
)
|
||||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||||
|
|
@ -522,7 +524,9 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
private fun createMatrixClient(
|
private fun createMatrixClient(
|
||||||
isAliasAvailable: Boolean = true,
|
isAliasAvailable: Boolean = true,
|
||||||
spaceService: FakeSpaceService = FakeSpaceService(),
|
spaceService: FakeSpaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) }
|
||||||
|
),
|
||||||
) = FakeMatrixClient(
|
) = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { "matrix.org" },
|
userIdServerNameLambda = { "matrix.org" },
|
||||||
resolveRoomAliasResult = {
|
resolveRoomAliasResult = {
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ private fun SpaceFilterButton(
|
||||||
onClick = ::onClick,
|
onClick = ::onClick,
|
||||||
colors = if (isSelected) {
|
colors = if (isSelected) {
|
||||||
IconButtonDefaults.iconButtonColors(
|
IconButtonDefaults.iconButtonColors(
|
||||||
containerColor = ElementTheme.colors.bgAccentRest,
|
containerColor = ElementTheme.colors.bgActionPrimaryRest,
|
||||||
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -809,14 +809,13 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// No spaces available, so isSpaceMemberSelectable should be false
|
// Room has SpaceMember access with existing space ID, so isSpaceMemberSelectable is true
|
||||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(savedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
assertThat(savedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
||||||
assertThat(isSpaceMemberSelectable).isFalse()
|
assertThat(isSpaceMemberSelectable).isTrue()
|
||||||
// showSpaceMemberOption should still be true because savedSettings has SpaceMember
|
|
||||||
assertThat(showSpaceMemberOption).isTrue()
|
assertThat(showSpaceMemberOption).isTrue()
|
||||||
}
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,12 @@ fun SpaceView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
acceptDeclineInviteView: @Composable () -> Unit,
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler {
|
var handledBack by remember { mutableStateOf(false) }
|
||||||
|
BackHandler(enabled = !handledBack) {
|
||||||
if (state.isManageMode) {
|
if (state.isManageMode) {
|
||||||
state.eventSink(SpaceEvents.ExitManageMode)
|
state.eventSink(SpaceEvents.ExitManageMode)
|
||||||
} else {
|
} else {
|
||||||
|
handledBack = true
|
||||||
onBackClick()
|
onBackClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ datastore = "1.2.0"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
constraintlayout_compose = "1.1.1"
|
constraintlayout_compose = "1.1.1"
|
||||||
lifecycle = "2.10.0"
|
lifecycle = "2.10.0"
|
||||||
activity = "1.12.3"
|
activity = "1.12.4"
|
||||||
media3 = "1.9.2"
|
media3 = "1.9.2"
|
||||||
camera = "1.5.3"
|
camera = "1.5.3"
|
||||||
work = "2.11.1"
|
work = "2.11.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2026.01.00"
|
compose_bom = "2026.02.00"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
|
|
@ -32,7 +32,7 @@ accompanist = "0.37.3"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.7.0"
|
test_core = "1.7.0"
|
||||||
roborazzi = "1.58.0"
|
roborazzi = "1.59.0"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.7.1"
|
datetime = "0.7.1"
|
||||||
|
|
@ -46,7 +46,7 @@ appyx = "1.7.1"
|
||||||
sqldelight = "2.2.1"
|
sqldelight = "2.2.1"
|
||||||
wysiwyg = "2.41.1"
|
wysiwyg = "2.41.1"
|
||||||
telephoto = "0.18.0"
|
telephoto = "0.18.0"
|
||||||
haze = "1.7.1"
|
haze = "1.7.2"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "3.5.1"
|
dependencyAnalysis = "3.5.1"
|
||||||
|
|
@ -62,7 +62,7 @@ detekt = "1.23.8"
|
||||||
# See https://github.com/pinterest/ktlint/releases/
|
# See https://github.com/pinterest/ktlint/releases/
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
androidx-test-ext-junit = "1.3.0"
|
androidx-test-ext-junit = "1.3.0"
|
||||||
kover = "0.9.6"
|
kover = "0.9.7"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -126,7 +126,6 @@ androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
androidx_compose_ui_test_manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
androidx_compose_ui_test_manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||||
androidx_compose_ui_test_junit = { module = "androidx.compose.ui:ui-test-junit4-android" }
|
androidx_compose_ui_test_junit = { module = "androidx.compose.ui:ui-test-junit4-android" }
|
||||||
androidx_compose_material = { module = "androidx.compose.material:material" }
|
|
||||||
androidx_compose_material_icons = { module = "androidx.compose.material:material-icons-extended" }
|
androidx_compose_material_icons = { module = "androidx.compose.material:material-icons-extended" }
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ import androidx.core.text.toSpannable
|
||||||
import androidx.core.text.util.LinkifyCompat
|
import androidx.core.text.util.LinkifyCompat
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to linkify text while preserving existing URL spans.
|
* Helper class to linkify text while preserving existing URL spans.
|
||||||
|
|
@ -59,7 +57,8 @@ object LinkifyHelper {
|
||||||
|
|
||||||
// Adapt the url in the URL span to the new end index too if needed
|
// Adapt the url in the URL span to the new end index too if needed
|
||||||
if (end != newEnd) {
|
if (end != newEnd) {
|
||||||
val url = spannable.subSequence(start, newEnd).toString()
|
val diff = end - newEnd
|
||||||
|
val url = urlSpan.url.substring(0, urlSpan.url.length - diff)
|
||||||
spannable.removeSpan(urlSpan)
|
spannable.removeSpan(urlSpan)
|
||||||
spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -87,12 +86,12 @@ object LinkifyHelper {
|
||||||
var end = end
|
var end = end
|
||||||
|
|
||||||
// Trailing punctuation found, adjust the end index
|
// Trailing punctuation found, adjust the end index
|
||||||
while (spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…') && end > start) {
|
while (end > start && spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…')) {
|
||||||
end--
|
end--
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last character is a closing parenthesis, check if it's part of a pair
|
// If the last character is a closing parenthesis, check if it's part of a pair
|
||||||
if (spannable[end - 1] == ')' && end > start) {
|
if (end > start && spannable[end - 1] == ')') {
|
||||||
val linkifiedTextLastPath = spannable.substring(start, end).substringAfterLast('/')
|
val linkifiedTextLastPath = spannable.substring(start, end).substringAfterLast('/')
|
||||||
val closingParenthesisCount = linkifiedTextLastPath.count { it == ')' }
|
val closingParenthesisCount = linkifiedTextLastPath.count { it == ')' }
|
||||||
val openingParenthesisCount = linkifiedTextLastPath.count { it == '(' }
|
val openingParenthesisCount = linkifiedTextLastPath.count { it == '(' }
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ package io.element.android.libraries.androidutils.text
|
||||||
|
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -140,4 +142,27 @@ class LinkifierHelperTest {
|
||||||
assertThat(urlSpans.size).isEqualTo(1)
|
assertThat(urlSpans.size).isEqualTo(1)
|
||||||
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/READ(ME)")
|
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/READ(ME)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `linkification handles trailing question marks`() {
|
||||||
|
val text = "A url: https://github.com/element-hq/element-android?"
|
||||||
|
val result = LinkifyHelper.linkify(text)
|
||||||
|
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||||
|
assertThat(urlSpans.size).isEqualTo(1)
|
||||||
|
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `linkification doesn't modify existing URLSpan`() {
|
||||||
|
val text = buildSpannedString {
|
||||||
|
append("A url: ")
|
||||||
|
inSpans(URLSpan("https://github.com/element-hq/element-android?")) {
|
||||||
|
append("here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val result = LinkifyHelper.linkify(text)
|
||||||
|
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||||
|
assertThat(urlSpans.size).isEqualTo(1)
|
||||||
|
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android?")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ private fun ButtonInternal(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
leadingIcon != null -> {
|
leadingIcon != null -> {
|
||||||
androidx.compose.material.Icon(
|
Icon(
|
||||||
painter = leadingIcon.getPainter(),
|
painter = leadingIcon.getPainter(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = LocalContentColor.current,
|
tint = LocalContentColor.current,
|
||||||
|
|
|
||||||
|
|
@ -74,21 +74,21 @@ enum class FeatureFlags(
|
||||||
key = "feature.createSpaces",
|
key = "feature.createSpaces",
|
||||||
title = "Create spaces",
|
title = "Create spaces",
|
||||||
description = "Allow creating spaces.",
|
description = "Allow creating spaces.",
|
||||||
defaultValue = { false },
|
defaultValue = { true },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
SpaceSettings(
|
SpaceSettings(
|
||||||
key = "feature.spaceSettings",
|
key = "feature.spaceSettings",
|
||||||
title = "Space settings",
|
title = "Space settings",
|
||||||
description = "Allow managing space settings such as details, permissions and privacy.",
|
description = "Allow managing space settings such as details, permissions and privacy.",
|
||||||
defaultValue = { false },
|
defaultValue = { true },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
RoomListSpaceFilters(
|
RoomListSpaceFilters(
|
||||||
key = "feature.roomListSpaceFilters",
|
key = "feature.roomListSpaceFilters",
|
||||||
title = "Room list space filters",
|
title = "Room list space filters",
|
||||||
description = "Allow filtering the room list by space.",
|
description = "Allow filtering the room list by space.",
|
||||||
defaultValue = { false },
|
defaultValue = { true },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
PrintLogsToLogcat(
|
PrintLogsToLogcat(
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,3 @@
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
typealias SpaceId = RoomId
|
typealias SpaceId = RoomId
|
||||||
|
|
||||||
/**
|
|
||||||
* Value to use when no space is selected by the user.
|
|
||||||
*/
|
|
||||||
val MAIN_SPACE = SpaceId("!mainSpace:local")
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ interface ActiveNotificationsProvider {
|
||||||
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
|
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
|
||||||
fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
|
fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
|
||||||
fun getSummaryNotification(sessionId: SessionId): StatusBarNotification?
|
fun getSummaryNotification(sessionId: SessionId): StatusBarNotification?
|
||||||
|
fun getFallbackNotification(sessionId: SessionId): StatusBarNotification?
|
||||||
fun count(sessionId: SessionId): Int
|
fun count(sessionId: SessionId): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +77,11 @@ class DefaultActiveNotificationsProvider(
|
||||||
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? {
|
||||||
|
val fallbackId = NotificationIdProvider.getFallbackNotificationId(sessionId)
|
||||||
|
return getNotificationsForSession(sessionId).find { it.id == fallbackId }
|
||||||
|
}
|
||||||
|
|
||||||
override fun count(sessionId: SessionId): Int {
|
override fun count(sessionId: SessionId): Int {
|
||||||
return getNotificationsForSession(sessionId).size
|
return getNotificationsForSession(sessionId).size
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,20 @@ import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
|
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||||
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
|
import io.element.android.services.appnavstate.api.currentRoomId
|
||||||
import io.element.android.services.appnavstate.api.currentSessionId
|
import io.element.android.services.appnavstate.api.currentSessionId
|
||||||
|
import io.element.android.services.appnavstate.api.currentThreadId
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -46,46 +55,49 @@ class DefaultNotificationDrawerManager(
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||||
|
sessionObserver: SessionObserver,
|
||||||
) : NotificationCleaner {
|
) : NotificationCleaner {
|
||||||
// TODO EAx add a setting per user for this
|
// TODO EAx add a setting per user for this
|
||||||
private var useCompleteNotificationFormat = true
|
private var useCompleteNotificationFormat = true
|
||||||
|
|
||||||
|
private val sessionListener = object : SessionListener {
|
||||||
|
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
|
||||||
|
// User signed out, clear all notifications related to the session.
|
||||||
|
clearAllEvents(SessionId(userId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Observe application state
|
// Observe application state
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
appNavigationStateService.appNavigationState
|
appNavigationStateService.appNavigationState
|
||||||
.collect { onAppNavigationStateChange(it.navigationState) }
|
.collect { onAppNavigationStateChange(it.navigationState) }
|
||||||
}
|
}
|
||||||
|
sessionObserver.addListener(sessionListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentAppNavigationState: NavigationState? = null
|
|
||||||
|
|
||||||
private fun onAppNavigationStateChange(navigationState: NavigationState) {
|
private fun onAppNavigationStateChange(navigationState: NavigationState) {
|
||||||
when (navigationState) {
|
when (navigationState) {
|
||||||
NavigationState.Root -> {
|
NavigationState.Root -> {}
|
||||||
currentAppNavigationState?.currentSessionId()?.let { sessionId ->
|
is NavigationState.Session -> {
|
||||||
// User signed out, clear all notifications related to the session.
|
// Cleanup the fallback notification
|
||||||
clearAllEvents(sessionId)
|
clearFallbackForSession(navigationState.sessionId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is NavigationState.Session -> {}
|
|
||||||
is NavigationState.Space -> {}
|
|
||||||
is NavigationState.Room -> {
|
is NavigationState.Room -> {
|
||||||
// Cleanup notification for current room
|
// Cleanup notification for current room
|
||||||
clearMessagesForRoom(
|
clearMessagesForRoom(
|
||||||
sessionId = navigationState.parentSpace.parentSession.sessionId,
|
sessionId = navigationState.parentSession.sessionId,
|
||||||
roomId = navigationState.roomId,
|
roomId = navigationState.roomId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NavigationState.Thread -> {
|
is NavigationState.Thread -> {
|
||||||
clearMessagesForThread(
|
clearMessagesForThread(
|
||||||
sessionId = navigationState.parentRoom.parentSpace.parentSession.sessionId,
|
sessionId = navigationState.parentRoom.parentSession.sessionId,
|
||||||
roomId = navigationState.parentRoom.roomId,
|
roomId = navigationState.parentRoom.roomId,
|
||||||
threadId = navigationState.threadId,
|
threadId = navigationState.threadId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentAppNavigationState = navigationState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -93,14 +105,11 @@ class DefaultNotificationDrawerManager(
|
||||||
* Events might be grouped and there might not be one notification per event!
|
* Events might be grouped and there might not be one notification per event!
|
||||||
*/
|
*/
|
||||||
suspend fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
suspend fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||||
if (notifiableEvent.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value)) {
|
onNotifiableEventsReceived(listOf(notifiableEvent))
|
||||||
return
|
|
||||||
}
|
|
||||||
renderEvents(listOf(notifiableEvent))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
|
suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
|
||||||
val eventsToNotify = notifiableEvents.filter { !it.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value) }
|
val eventsToNotify = notifiableEvents.filter { !appNavigationStateService.appNavigationState.value.shouldIgnoreEvent(it) }
|
||||||
renderEvents(eventsToNotify)
|
renderEvents(eventsToNotify)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +129,17 @@ class DefaultNotificationDrawerManager(
|
||||||
.forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
|
.forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the fallback notification for the session.
|
||||||
|
*/
|
||||||
|
fun clearFallbackForSession(sessionId: SessionId) {
|
||||||
|
notificationDisplayer.cancelNotification(
|
||||||
|
DefaultNotificationDataFactory.FALLBACK_NOTIFICATION_TAG,
|
||||||
|
NotificationIdProvider.getFallbackNotificationId(sessionId),
|
||||||
|
)
|
||||||
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when the application is currently opened and showing timeline for the given [roomId].
|
* Should be called when the application is currently opened and showing timeline for the given [roomId].
|
||||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||||
|
|
@ -191,3 +211,30 @@ class DefaultNotificationDrawerManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to check if a notifiableEvent should be ignored based on the current application navigation state.
|
||||||
|
*/
|
||||||
|
private fun AppNavigationState.shouldIgnoreEvent(event: NotifiableEvent): Boolean {
|
||||||
|
if (!isInForeground) return false
|
||||||
|
return navigationState.currentSessionId() == event.sessionId &&
|
||||||
|
when (event) {
|
||||||
|
is NotifiableRingingCallEvent -> {
|
||||||
|
// Never ignore ringing call notifications
|
||||||
|
// Note that NotifiableRingingCallEvent are not handled by DefaultNotificationDrawerManager
|
||||||
|
false
|
||||||
|
}
|
||||||
|
is FallbackNotifiableEvent -> {
|
||||||
|
// Ignore if the room list is currently displayed
|
||||||
|
navigationState is NavigationState.Session
|
||||||
|
}
|
||||||
|
is InviteNotifiableEvent,
|
||||||
|
is SimpleNotifiableEvent -> {
|
||||||
|
event.roomId == navigationState.currentRoomId()
|
||||||
|
}
|
||||||
|
is NotifiableMessageEvent -> {
|
||||||
|
event.roomId == navigationState.currentRoomId() &&
|
||||||
|
event.threadId == navigationState.currentThreadId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,12 @@ import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.impl.R
|
|
||||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class FallbackNotificationFactory(
|
class FallbackNotificationFactory(
|
||||||
private val clock: SystemClock,
|
private val clock: SystemClock,
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
) {
|
) {
|
||||||
fun create(
|
fun create(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
|
|
@ -36,7 +33,7 @@ class FallbackNotificationFactory(
|
||||||
isRedacted = false,
|
isRedacted = false,
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
timestamp = clock.epochMillis(),
|
timestamp = clock.epochMillis(),
|
||||||
description = stringProvider.getString(R.string.notification_fallback_content),
|
description = "",
|
||||||
cause = cause,
|
cause = cause,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,18 @@
|
||||||
package io.element.android.libraries.push.impl.notifications
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import androidx.core.text.buildSpannedString
|
|
||||||
import androidx.core.text.inSpans
|
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.push.impl.R
|
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
|
||||||
|
|
||||||
interface NotificationDataFactory {
|
interface NotificationDataFactory {
|
||||||
suspend fun toNotifications(
|
suspend fun toNotifications(
|
||||||
|
|
@ -51,16 +45,15 @@ interface NotificationDataFactory {
|
||||||
|
|
||||||
@JvmName("toNotificationFallbackEvents")
|
@JvmName("toNotificationFallbackEvents")
|
||||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
fun toNotifications(
|
fun toNotification(
|
||||||
fallback: List<FallbackNotifiableEvent>,
|
fallback: List<FallbackNotifiableEvent>,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): List<OneShotNotification>
|
): OneShotNotification?
|
||||||
|
|
||||||
fun createSummaryNotification(
|
fun createSummaryNotification(
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): SummaryNotification
|
): SummaryNotification
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +64,6 @@ class DefaultNotificationDataFactory(
|
||||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator,
|
private val summaryGroupMessageCreator: SummaryGroupMessageCreator,
|
||||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
) : NotificationDataFactory {
|
) : NotificationDataFactory {
|
||||||
override suspend fun toNotifications(
|
override suspend fun toNotifications(
|
||||||
messages: List<NotifiableMessageEvent>,
|
messages: List<NotifiableMessageEvent>,
|
||||||
|
|
@ -81,10 +73,7 @@ class DefaultNotificationDataFactory(
|
||||||
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
|
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
|
||||||
.groupBy { it.roomId }
|
.groupBy { it.roomId }
|
||||||
return messagesToDisplay.flatMap { (roomId, events) ->
|
return messagesToDisplay.flatMap { (roomId, events) ->
|
||||||
val roomName = events.lastOrNull()?.roomName ?: roomId.value
|
|
||||||
val isDm = events.lastOrNull()?.roomIsDm ?: false
|
|
||||||
val eventsByThreadId = events.groupBy { it.threadId }
|
val eventsByThreadId = events.groupBy { it.threadId }
|
||||||
|
|
||||||
eventsByThreadId.map { (threadId, events) ->
|
eventsByThreadId.map { (threadId, events) ->
|
||||||
val notification = roomGroupMessageCreator.createRoomMessage(
|
val notification = roomGroupMessageCreator.createRoomMessage(
|
||||||
events = events,
|
events = events,
|
||||||
|
|
@ -98,7 +87,6 @@ class DefaultNotificationDataFactory(
|
||||||
notification = notification,
|
notification = notification,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
threadId = threadId,
|
threadId = threadId,
|
||||||
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, isDm),
|
|
||||||
messageCount = events.size,
|
messageCount = events.size,
|
||||||
latestTimestamp = events.maxOf { it.timestamp },
|
latestTimestamp = events.maxOf { it.timestamp },
|
||||||
shouldBing = events.any { it.noisy }
|
shouldBing = events.any { it.noisy }
|
||||||
|
|
@ -123,7 +111,6 @@ class DefaultNotificationDataFactory(
|
||||||
OneShotNotification(
|
OneShotNotification(
|
||||||
tag = event.roomId.value,
|
tag = event.roomId.value,
|
||||||
notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event),
|
notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event),
|
||||||
summaryLine = event.description,
|
|
||||||
isNoisy = event.noisy,
|
isNoisy = event.noisy,
|
||||||
timestamp = event.timestamp
|
timestamp = event.timestamp
|
||||||
)
|
)
|
||||||
|
|
@ -140,7 +127,6 @@ class DefaultNotificationDataFactory(
|
||||||
OneShotNotification(
|
OneShotNotification(
|
||||||
tag = event.eventId.value,
|
tag = event.eventId.value,
|
||||||
notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event),
|
notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event),
|
||||||
summaryLine = event.description,
|
|
||||||
isNoisy = event.noisy,
|
isNoisy = event.noisy,
|
||||||
timestamp = event.timestamp
|
timestamp = event.timestamp
|
||||||
)
|
)
|
||||||
|
|
@ -149,26 +135,31 @@ class DefaultNotificationDataFactory(
|
||||||
|
|
||||||
@JvmName("toNotificationFallbackEvents")
|
@JvmName("toNotificationFallbackEvents")
|
||||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
override fun toNotifications(
|
override fun toNotification(
|
||||||
fallback: List<FallbackNotifiableEvent>,
|
fallback: List<FallbackNotifiableEvent>,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): List<OneShotNotification> {
|
): OneShotNotification? {
|
||||||
return fallback.map { event ->
|
if (fallback.isEmpty()) return null
|
||||||
OneShotNotification(
|
val existingNotification = activeNotificationsProvider
|
||||||
tag = event.eventId.value,
|
.getFallbackNotification(notificationAccountParams.user.userId)
|
||||||
notification = notificationCreator.createFallbackNotification(notificationAccountParams, event),
|
?.notification
|
||||||
summaryLine = event.description.orEmpty(),
|
val notification = notificationCreator.createFallbackNotification(
|
||||||
isNoisy = false,
|
existingNotification = existingNotification,
|
||||||
timestamp = event.timestamp
|
notificationAccountParams = notificationAccountParams,
|
||||||
)
|
fallbackNotifiableEvents = fallback,
|
||||||
}
|
)
|
||||||
|
return OneShotNotification(
|
||||||
|
tag = FALLBACK_NOTIFICATION_TAG,
|
||||||
|
notification = notification,
|
||||||
|
isNoisy = false,
|
||||||
|
timestamp = fallback.first().timestamp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSummaryNotification(
|
override fun createSummaryNotification(
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): SummaryNotification {
|
): SummaryNotification {
|
||||||
return when {
|
return when {
|
||||||
|
|
@ -178,51 +169,14 @@ class DefaultNotificationDataFactory(
|
||||||
roomNotifications = roomNotifications,
|
roomNotifications = roomNotifications,
|
||||||
invitationNotifications = invitationNotifications,
|
invitationNotifications = invitationNotifications,
|
||||||
simpleNotifications = simpleNotifications,
|
simpleNotifications = simpleNotifications,
|
||||||
fallbackNotifications = fallbackNotifications,
|
|
||||||
notificationAccountParams = notificationAccountParams,
|
notificationAccountParams = notificationAccountParams,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDm: Boolean): CharSequence {
|
companion object {
|
||||||
return when (events.size) {
|
const val FALLBACK_NOTIFICATION_TAG = "FALLBACK"
|
||||||
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDm)
|
|
||||||
else -> {
|
|
||||||
stringProvider.getQuantityString(
|
|
||||||
R.plurals.notification_compat_summary_line_for_room,
|
|
||||||
events.size,
|
|
||||||
roomName,
|
|
||||||
events.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDm: Boolean): CharSequence {
|
|
||||||
return if (roomIsDm) {
|
|
||||||
buildSpannedString {
|
|
||||||
event.senderDisambiguatedDisplayName?.let {
|
|
||||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
|
||||||
append(it)
|
|
||||||
append(": ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
append(event.description)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buildSpannedString {
|
|
||||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
|
||||||
append(roomName)
|
|
||||||
append(": ")
|
|
||||||
event.senderDisambiguatedDisplayName?.let {
|
|
||||||
append(it)
|
|
||||||
append(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
append(event.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +184,6 @@ data class RoomNotification(
|
||||||
val notification: Notification,
|
val notification: Notification,
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val threadId: ThreadId?,
|
val threadId: ThreadId?,
|
||||||
val summaryLine: CharSequence,
|
|
||||||
val messageCount: Int,
|
val messageCount: Int,
|
||||||
val latestTimestamp: Long,
|
val latestTimestamp: Long,
|
||||||
val shouldBing: Boolean,
|
val shouldBing: Boolean,
|
||||||
|
|
@ -239,7 +192,6 @@ data class RoomNotification(
|
||||||
return notification == other.notification &&
|
return notification == other.notification &&
|
||||||
roomId == other.roomId &&
|
roomId == other.roomId &&
|
||||||
threadId == other.threadId &&
|
threadId == other.threadId &&
|
||||||
summaryLine.toString() == other.summaryLine.toString() &&
|
|
||||||
messageCount == other.messageCount &&
|
messageCount == other.messageCount &&
|
||||||
latestTimestamp == other.latestTimestamp &&
|
latestTimestamp == other.latestTimestamp &&
|
||||||
shouldBing == other.shouldBing
|
shouldBing == other.shouldBing
|
||||||
|
|
@ -249,7 +201,6 @@ data class RoomNotification(
|
||||||
data class OneShotNotification(
|
data class OneShotNotification(
|
||||||
val notification: Notification,
|
val notification: Notification,
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val summaryLine: CharSequence,
|
|
||||||
val isNoisy: Boolean,
|
val isNoisy: Boolean,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,11 @@ class NotificationRenderer(
|
||||||
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams)
|
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams)
|
||||||
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams)
|
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams)
|
||||||
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams)
|
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams)
|
||||||
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams)
|
val fallbackNotification = notificationDataFactory.toNotification(groupedEvents.fallbackEvents, notificationAccountParams)
|
||||||
val summaryNotification = notificationDataFactory.createSummaryNotification(
|
val summaryNotification = notificationDataFactory.createSummaryNotification(
|
||||||
roomNotifications = roomNotifications,
|
roomNotifications = roomNotifications,
|
||||||
invitationNotifications = invitationNotifications,
|
invitationNotifications = invitationNotifications,
|
||||||
simpleNotifications = simpleNotifications,
|
simpleNotifications = simpleNotifications,
|
||||||
fallbackNotifications = fallbackNotifications,
|
|
||||||
notificationAccountParams = notificationAccountParams,
|
notificationAccountParams = notificationAccountParams,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -107,13 +106,12 @@ class NotificationRenderer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show only the first fallback notification
|
if (fallbackNotification != null) {
|
||||||
if (fallbackNotifications.isNotEmpty()) {
|
Timber.tag(loggerTag.value).d("Showing or updating fallback notification")
|
||||||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
|
||||||
notificationDisplayer.showNotification(
|
notificationDisplayer.showNotification(
|
||||||
tag = "FALLBACK",
|
tag = fallbackNotification.tag,
|
||||||
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||||
notification = fallbackNotifications.first().notification
|
notification = fallbackNotification.notification,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ interface SummaryGroupMessageCreator {
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
): Notification
|
): Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +44,6 @@ class DefaultSummaryGroupMessageCreator(
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
): Notification {
|
): Notification {
|
||||||
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
|
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
|
||||||
invitationNotifications.any { it.isNoisy } ||
|
invitationNotifications.any { it.isNoisy } ||
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@ interface NotificationCreator {
|
||||||
): Notification
|
): Notification
|
||||||
|
|
||||||
fun createFallbackNotification(
|
fun createFallbackNotification(
|
||||||
|
existingNotification: Notification?,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
|
||||||
): Notification
|
): Notification
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -308,31 +309,38 @@ class DefaultNotificationCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createFallbackNotification(
|
override fun createFallbackNotification(
|
||||||
|
existingNotification: Notification?,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
|
||||||
): Notification {
|
): Notification {
|
||||||
val channelId = notificationChannels.getChannelIdForMessage(
|
val channelId = notificationChannels.getChannelIdForMessage(
|
||||||
sessionId = fallbackNotifiableEvent.sessionId,
|
sessionId = fallbackNotifiableEvent.sessionId,
|
||||||
noisy = false,
|
noisy = false,
|
||||||
)
|
)
|
||||||
|
val existingCounter = existingNotification
|
||||||
|
?.extras
|
||||||
|
?.getInt(FALLBACK_COUNTER_EXTRA)
|
||||||
|
?: 0
|
||||||
|
val counter = existingCounter + fallbackNotifiableEvents.size
|
||||||
|
val fallbackNotifiableEvent = fallbackNotifiableEvents.first()
|
||||||
return NotificationCompat.Builder(context, channelId)
|
return NotificationCompat.Builder(context, channelId)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
|
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
|
||||||
.setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8))
|
.setContentText(
|
||||||
|
stringProvider.getQuantityString(R.plurals.notification_fallback_n_content, counter, counter)
|
||||||
|
.annotateForDebug(8)
|
||||||
|
)
|
||||||
|
.setExtras(
|
||||||
|
bundleOf(
|
||||||
|
FALLBACK_COUNTER_EXTRA to counter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setNumber(counter)
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||||
.configureWith(notificationAccountParams)
|
.configureWith(notificationAccountParams)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setWhen(fallbackNotifiableEvent.timestamp)
|
.setWhen(fallbackNotifiableEvent.timestamp)
|
||||||
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
|
|
||||||
// and the user won't have access to the room yet, resulting in an error screen.
|
|
||||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))
|
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))
|
||||||
.setDeleteIntent(
|
|
||||||
pendingIntentFactory.createDismissEventPendingIntent(
|
|
||||||
fallbackNotifiableEvent.sessionId,
|
|
||||||
fallbackNotifiableEvent.roomId,
|
|
||||||
fallbackNotifiableEvent.eventId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
@ -522,6 +530,7 @@ class DefaultNotificationCreator(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MESSAGE_EVENT_ID = "message_event_id"
|
const val MESSAGE_EVENT_ID = "message_event_id"
|
||||||
|
private const val FALLBACK_COUNTER_EXTRA = "COUNTER"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
|
||||||
import io.element.android.services.appnavstate.api.currentRoomId
|
|
||||||
import io.element.android.services.appnavstate.api.currentSessionId
|
|
||||||
import io.element.android.services.appnavstate.api.currentThreadId
|
|
||||||
|
|
||||||
data class NotifiableMessageEvent(
|
data class NotifiableMessageEvent(
|
||||||
override val sessionId: SessionId,
|
override val sessionId: SessionId,
|
||||||
|
|
@ -56,24 +52,3 @@ data class NotifiableMessageEvent(
|
||||||
val imageUri: Uri?
|
val imageUri: Uri?
|
||||||
get() = imageUriString?.toUri()
|
get() = imageUriString?.toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to check if a notification should be ignored based on the current app and navigation state.
|
|
||||||
*/
|
|
||||||
fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationState): Boolean {
|
|
||||||
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
|
||||||
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
|
||||||
null -> false
|
|
||||||
else -> {
|
|
||||||
// Never ignore ringing call notifications
|
|
||||||
if (this is NotifiableRingingCallEvent) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
appNavigationState.isInForeground &&
|
|
||||||
sessionId == currentSessionId &&
|
|
||||||
roomId == currentRoomId &&
|
|
||||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notification_error_unified_push_unregistered_android">"The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor."</string>
|
<string name="notification_error_unified_push_unregistered_android">"The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor."</string>
|
||||||
<string name="notification_fallback_content">"You have new messages."</string>
|
<string name="notification_fallback_content">"You have new messages."</string>
|
||||||
|
<plurals name="notification_fallback_n_content">
|
||||||
|
<item quantity="one">"You have %d new message."</item>
|
||||||
|
<item quantity="other">"You have %d new messages."</item>
|
||||||
|
</plurals>
|
||||||
<string name="notification_incoming_call">"📹 Incoming call"</string>
|
<string name="notification_incoming_call">"📹 Incoming call"</string>
|
||||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||||
<string name="notification_invitation_action_join">"Join"</string>
|
<string name="notification_invitation_action_join">"Join"</string>
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,19 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull()
|
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getFallbackNotification returns only the fallback notification for that session id if it exists`() {
|
||||||
|
val activeNotifications = listOf(
|
||||||
|
aStatusBarNotification(id = notificationIdProvider.getFallbackNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||||
|
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||||
|
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||||
|
)
|
||||||
|
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||||
|
|
||||||
|
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID)).isNotNull()
|
||||||
|
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID_2)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk<StatusBarNotification> {
|
private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk<StatusBarNotification> {
|
||||||
every { this@mockk.id } returns id
|
every { this@mockk.id } returns id
|
||||||
every { this@mockk.tag } returns tag
|
every { this@mockk.tag } returns tag
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
|
||||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
||||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
|
||||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -663,7 +662,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
editedEventId = null,
|
editedEventId = null,
|
||||||
description = "You have new messages.",
|
description = "",
|
||||||
canBeReplaced = true,
|
canBeReplaced = true,
|
||||||
isRedacted = false,
|
isRedacted = false,
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
|
|
@ -895,7 +894,6 @@ class DefaultNotifiableEventResolverTest {
|
||||||
callNotificationEventResolver = callNotificationEventResolver,
|
callNotificationEventResolver = callNotificationEventResolver,
|
||||||
fallbackNotificationFactory = FallbackNotificationFactory(
|
fallbackNotificationFactory = FallbackNotificationFactory(
|
||||||
clock = FakeSystemClock(),
|
clock = FakeSystemClock(),
|
||||||
stringProvider = FakeStringProvider(defaultResult = "You have new messages.")
|
|
||||||
),
|
),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -29,22 +31,26 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.test.aNavigationState
|
import io.element.android.services.appnavstate.test.aNavigationState
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
import io.element.android.services.appnavstate.test.anAppNavigationState
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -92,28 +98,25 @@ class DefaultNotificationDrawerManagerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `react to applicationStateChange`() = runTest {
|
fun `react to applicationStateChange`() = runTest {
|
||||||
// For now just call all the API. Later, add more valuable tests.
|
// For now just call all the API. Later, add more valuable tests.
|
||||||
val appNavigationStateFlow: MutableStateFlow<AppNavigationState> = MutableStateFlow(
|
val appNavigationStateService = FakeAppNavigationStateService()
|
||||||
AppNavigationState(
|
|
||||||
navigationState = NavigationState.Root,
|
|
||||||
isInForeground = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow)
|
|
||||||
createDefaultNotificationDrawerManager(
|
createDefaultNotificationDrawerManager(
|
||||||
appNavigationStateService = appNavigationStateService
|
appNavigationStateService = appNavigationStateService
|
||||||
)
|
)
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
|
appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(), isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true))
|
appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true))
|
appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID), isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true))
|
appNavigationStateService.emitNavigationState(
|
||||||
runCurrent()
|
AppNavigationState(
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true))
|
aNavigationState(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID),
|
||||||
|
isInForeground = true
|
||||||
|
)
|
||||||
|
)
|
||||||
runCurrent()
|
runCurrent()
|
||||||
// Like a user sign out
|
// Like a user sign out
|
||||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
|
appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(), isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,35 +208,325 @@ class DefaultNotificationDrawerManagerTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createDefaultNotificationDrawerManager(
|
@Test
|
||||||
notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(),
|
fun `when a session is signed out, clearAllEvent is invoked`() = runTest {
|
||||||
appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(),
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(),
|
val notificationDisplayer = FakeNotificationDisplayer(
|
||||||
summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(),
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(),
|
)
|
||||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
|
||||||
sessionStore: SessionStore = InMemorySessionStore(),
|
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
getNotificationsForSessionResult = {
|
||||||
): DefaultNotificationDrawerManager {
|
listOf(
|
||||||
return DefaultNotificationDrawerManager(
|
mockk {
|
||||||
|
every { id } returns summaryId
|
||||||
|
every { tag } returns null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
countResult = { 1 },
|
||||||
|
)
|
||||||
|
val sessionObserver = FakeSessionObserver()
|
||||||
|
createDefaultNotificationDrawerManager(
|
||||||
notificationDisplayer = notificationDisplayer,
|
notificationDisplayer = notificationDisplayer,
|
||||||
notificationRenderer = NotificationRenderer(
|
|
||||||
notificationDisplayer = FakeNotificationDisplayer(),
|
|
||||||
notificationDataFactory = DefaultNotificationDataFactory(
|
|
||||||
notificationCreator = FakeNotificationCreator(),
|
|
||||||
roomGroupMessageCreator = roomGroupMessageCreator,
|
|
||||||
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
|
||||||
activeNotificationsProvider = activeNotificationsProvider,
|
|
||||||
stringProvider = FakeStringProvider(),
|
|
||||||
),
|
|
||||||
enterpriseService = enterpriseService,
|
|
||||||
sessionStore = sessionStore,
|
|
||||||
),
|
|
||||||
appNavigationStateService = appNavigationStateService,
|
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
matrixClientProvider = matrixClientProvider,
|
|
||||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
|
||||||
activeNotificationsProvider = activeNotificationsProvider,
|
activeNotificationsProvider = activeNotificationsProvider,
|
||||||
|
sessionObserver = sessionObserver,
|
||||||
|
)
|
||||||
|
// Simulate a session sign out
|
||||||
|
sessionObserver.onSessionDeleted(A_SESSION_ID.value)
|
||||||
|
// Verify we asked to cancel the notification with summaryId
|
||||||
|
cancelNotificationResult.assertions().isCalledExactly(1).withSequence(
|
||||||
|
listOf(value(null), value(summaryId)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when the application is in background, all events trigger a notification`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID),
|
||||||
|
isInForeground = false,
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aFallbackNotifiableEvent(sessionId = A_SESSION_ID),
|
||||||
|
aFallbackNotifiableEvent(sessionId = A_SESSION_ID_2),
|
||||||
|
anInviteNotifiableEvent(sessionId = A_SESSION_ID),
|
||||||
|
anInviteNotifiableEvent(sessionId = A_SESSION_ID_2),
|
||||||
|
aSimpleNotifiableEvent(sessionId = A_SESSION_ID),
|
||||||
|
aSimpleNotifiableEvent(sessionId = A_SESSION_ID_2),
|
||||||
|
aNotifiableMessageEvent(sessionId = A_SESSION_ID),
|
||||||
|
aNotifiableMessageEvent(sessionId = A_SESSION_ID_2),
|
||||||
|
aNotifiableMessageEvent(sessionId = A_SESSION_ID, threadId = A_THREAD_ID),
|
||||||
|
aNotifiableMessageEvent(sessionId = A_SESSION_ID_2, threadId = A_THREAD_ID_2),
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fallback event is ignored when the room list is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fallback event is not ignored when a room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fallback event for other session is not ignored when the room list is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invite notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(anInviteNotifiableEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invite notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
anInviteNotifiableEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invite notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
anInviteNotifiableEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `simple notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(aSimpleNotifiableEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `simple notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aSimpleNotifiableEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `simple notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aSimpleNotifiableEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(aNotifiableMessageEvent(sessionId = A_SESSION_ID)),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event for a thread emits a notification when the same room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = A_THREAD_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event for a thread does not emit a notification when the same thread is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = A_THREAD_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event for a thread emits a notification when another thread is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = A_THREAD_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event for a thread emits a notification when a thread of another room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2, threadId = A_THREAD_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = A_THREAD_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
|
||||||
|
appNavigationState = anAppNavigationState(
|
||||||
|
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
|
||||||
|
),
|
||||||
|
notifiableEvents = listOf(
|
||||||
|
aNotifiableMessageEvent(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shouldEmitNotification = true,
|
||||||
|
extraInvocationsForNotificationSummary = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun testOnNotifiableEventReceived(
|
||||||
|
appNavigationState: AppNavigationState,
|
||||||
|
notifiableEvents: List<NotifiableEvent>,
|
||||||
|
shouldEmitNotification: Boolean,
|
||||||
|
extraInvocationsForNotificationSummary: Int = 0,
|
||||||
|
) = runTest {
|
||||||
|
val showNotificationResult = lambdaRecorder<String?, Int, Notification, Boolean> { _, _, _ ->
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||||
|
appNavigationStateService = FakeAppNavigationStateService(
|
||||||
|
initialAppNavigationState = appNavigationState,
|
||||||
|
),
|
||||||
|
notificationDisplayer = FakeNotificationDisplayer(
|
||||||
|
showNotificationResult = showNotificationResult,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
defaultNotificationDrawerManager.onNotifiableEventsReceived(notifiableEvents)
|
||||||
|
showNotificationResult.assertions().isCalledExactly(
|
||||||
|
if (shouldEmitNotification) {
|
||||||
|
notifiableEvents.size + extraInvocationsForNotificationSummary
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TestScope.createDefaultNotificationDrawerManager(
|
||||||
|
notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(),
|
||||||
|
notificationRenderer: NotificationRenderer? = null,
|
||||||
|
appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(),
|
||||||
|
roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(),
|
||||||
|
summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(),
|
||||||
|
activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||||
|
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
|
sessionObserver: SessionObserver = FakeSessionObserver(),
|
||||||
|
): DefaultNotificationDrawerManager {
|
||||||
|
return DefaultNotificationDrawerManager(
|
||||||
|
notificationDisplayer = notificationDisplayer,
|
||||||
|
notificationRenderer = notificationRenderer ?: NotificationRenderer(
|
||||||
|
notificationDisplayer = notificationDisplayer,
|
||||||
|
notificationDataFactory = DefaultNotificationDataFactory(
|
||||||
|
notificationCreator = FakeNotificationCreator(),
|
||||||
|
roomGroupMessageCreator = roomGroupMessageCreator,
|
||||||
|
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
||||||
|
activeNotificationsProvider = activeNotificationsProvider,
|
||||||
|
),
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
),
|
||||||
|
appNavigationStateService = appNavigationStateService,
|
||||||
|
coroutineScope = backgroundScope,
|
||||||
|
matrixClientProvider = matrixClientProvider,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
activeNotificationsProvider = activeNotificationsProvider,
|
||||||
|
sessionObserver = sessionObserver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,9 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||||
import io.element.android.libraries.matrix.test.notification.aNotificationData
|
import io.element.android.libraries.matrix.test.notification.aNotificationData
|
||||||
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
|
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
|
@ -47,16 +43,10 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
||||||
})
|
})
|
||||||
val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
|
val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
|
||||||
matrixClientProvider = matrixClientProvider,
|
matrixClientProvider = matrixClientProvider,
|
||||||
defaultNotificationDrawerManager = DefaultNotificationDrawerManager(
|
defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||||
notificationDisplayer = FakeNotificationDisplayer(),
|
|
||||||
notificationRenderer = createNotificationRenderer(
|
notificationRenderer = createNotificationRenderer(
|
||||||
notificationDataFactory = dataFactory,
|
notificationDataFactory = dataFactory,
|
||||||
),
|
),
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
matrixClientProvider = FakeMatrixClientProvider(),
|
|
||||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
|
||||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
|
||||||
),
|
),
|
||||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ ->
|
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ ->
|
||||||
Result.success(aNotifiableMessageEvent())
|
Result.success(aNotifiableMessageEvent())
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ class DefaultSummaryGroupMessageCreatorTest {
|
||||||
RoomNotification(
|
RoomNotification(
|
||||||
notification = Notification(),
|
notification = Notification(),
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
summaryLine = "",
|
|
||||||
messageCount = 1,
|
messageCount = 1,
|
||||||
latestTimestamp = A_FAKE_TIMESTAMP + 10,
|
latestTimestamp = A_FAKE_TIMESTAMP + 10,
|
||||||
shouldBing = true,
|
shouldBing = true,
|
||||||
|
|
@ -48,7 +47,6 @@ class DefaultSummaryGroupMessageCreatorTest {
|
||||||
),
|
),
|
||||||
invitationNotifications = emptyList(),
|
invitationNotifications = emptyList(),
|
||||||
simpleNotifications = emptyList(),
|
simpleNotifications = emptyList(),
|
||||||
fallbackNotifications = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
notificationCreator.createSummaryListNotificationResult.assertions()
|
notificationCreator.createSummaryListNotificationResult.assertions()
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotif
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -34,6 +34,7 @@ private val MY_AVATAR_URL: String? = null
|
||||||
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||||
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
|
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
|
||||||
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
||||||
|
private val A_FALLBACK_EVENT = aFallbackNotifiableEvent()
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class NotificationDataFactoryTest {
|
class NotificationDataFactoryTest {
|
||||||
|
|
@ -47,7 +48,6 @@ class NotificationDataFactoryTest {
|
||||||
roomGroupMessageCreator = fakeRoomGroupMessageCreator,
|
roomGroupMessageCreator = fakeRoomGroupMessageCreator,
|
||||||
summaryGroupMessageCreator = fakeSummaryGroupMessageCreator,
|
summaryGroupMessageCreator = fakeSummaryGroupMessageCreator,
|
||||||
activeNotificationsProvider = activeNotificationsProvider,
|
activeNotificationsProvider = activeNotificationsProvider,
|
||||||
stringProvider = FakeStringProvider(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -64,7 +64,6 @@ class NotificationDataFactoryTest {
|
||||||
OneShotNotification(
|
OneShotNotification(
|
||||||
notification = expectedNotification,
|
notification = expectedNotification,
|
||||||
tag = A_ROOM_ID.value,
|
tag = A_ROOM_ID.value,
|
||||||
summaryLine = AN_INVITATION_EVENT.description,
|
|
||||||
isNoisy = AN_INVITATION_EVENT.noisy,
|
isNoisy = AN_INVITATION_EVENT.noisy,
|
||||||
timestamp = AN_INVITATION_EVENT.timestamp
|
timestamp = AN_INVITATION_EVENT.timestamp
|
||||||
)
|
)
|
||||||
|
|
@ -72,6 +71,25 @@ class NotificationDataFactoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a fallback invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) {
|
||||||
|
val fallbackEvents = listOf(A_FALLBACK_EVENT)
|
||||||
|
val expectedNotification = notificationCreator.createFallbackNotificationResult(
|
||||||
|
null,
|
||||||
|
aNotificationAccountParams(),
|
||||||
|
fallbackEvents,
|
||||||
|
)
|
||||||
|
val result = toNotification(fallbackEvents, aNotificationAccountParams())
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
OneShotNotification(
|
||||||
|
notification = expectedNotification,
|
||||||
|
tag = "FALLBACK",
|
||||||
|
isNoisy = false,
|
||||||
|
timestamp = A_FALLBACK_EVENT.timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) {
|
fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) {
|
||||||
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(
|
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(
|
||||||
|
|
@ -83,7 +101,6 @@ class NotificationDataFactoryTest {
|
||||||
OneShotNotification(
|
OneShotNotification(
|
||||||
notification = expectedNotification,
|
notification = expectedNotification,
|
||||||
tag = AN_EVENT_ID.value,
|
tag = AN_EVENT_ID.value,
|
||||||
summaryLine = A_SIMPLE_EVENT.description,
|
|
||||||
isNoisy = A_SIMPLE_EVENT.noisy,
|
isNoisy = A_SIMPLE_EVENT.noisy,
|
||||||
timestamp = AN_INVITATION_EVENT.timestamp
|
timestamp = AN_INVITATION_EVENT.timestamp
|
||||||
)
|
)
|
||||||
|
|
@ -105,7 +122,6 @@ class NotificationDataFactoryTest {
|
||||||
existingNotification = null,
|
existingNotification = null,
|
||||||
),
|
),
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
summaryLine = "A room name: Bob Hello world!",
|
|
||||||
messageCount = events.size,
|
messageCount = events.size,
|
||||||
latestTimestamp = events.maxOf { it.timestamp },
|
latestTimestamp = events.maxOf { it.timestamp },
|
||||||
shouldBing = events.any { it.noisy },
|
shouldBing = events.any { it.noisy },
|
||||||
|
|
@ -161,7 +177,6 @@ class NotificationDataFactoryTest {
|
||||||
existingNotification = null,
|
existingNotification = null,
|
||||||
),
|
),
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
summaryLine = "A room name: Bob Hello world!",
|
|
||||||
messageCount = withRedactedRemoved.size,
|
messageCount = withRedactedRemoved.size,
|
||||||
latestTimestamp = withRedactedRemoved.maxOf { it.timestamp },
|
latestTimestamp = withRedactedRemoved.maxOf { it.timestamp },
|
||||||
shouldBing = withRedactedRemoved.any { it.noisy },
|
shouldBing = withRedactedRemoved.any { it.noisy },
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNot
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -43,7 +42,7 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
|
||||||
|
|
||||||
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION)
|
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION)
|
||||||
private val ONE_SHOT_NOTIFICATION =
|
private val ONE_SHOT_NOTIFICATION =
|
||||||
OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
|
OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", isNoisy = false, timestamp = -1)
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class NotificationRendererTest {
|
class NotificationRendererTest {
|
||||||
|
|
@ -57,7 +56,6 @@ class NotificationRendererTest {
|
||||||
roomGroupMessageCreator = roomGroupMessageCreator,
|
roomGroupMessageCreator = roomGroupMessageCreator,
|
||||||
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
||||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||||
stringProvider = FakeStringProvider(),
|
|
||||||
)
|
)
|
||||||
private val notificationIdProvider = NotificationIdProvider
|
private val notificationIdProvider = NotificationIdProvider
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ import io.element.android.libraries.push.impl.notifications.factories.action.Acc
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
|
|
@ -84,19 +84,11 @@ class DefaultNotificationCreatorTest {
|
||||||
fun `test createFallbackNotification`() {
|
fun `test createFallbackNotification`() {
|
||||||
val sut = createNotificationCreator()
|
val sut = createNotificationCreator()
|
||||||
val result = sut.createFallbackNotification(
|
val result = sut.createFallbackNotification(
|
||||||
|
existingNotification = null,
|
||||||
notificationAccountParams = aNotificationAccountParams(),
|
notificationAccountParams = aNotificationAccountParams(),
|
||||||
FallbackNotifiableEvent(
|
fallbackNotifiableEvents = listOf(
|
||||||
sessionId = A_SESSION_ID,
|
aFallbackNotifiableEvent(),
|
||||||
roomId = A_ROOM_ID,
|
)
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
editedEventId = null,
|
|
||||||
description = "description",
|
|
||||||
canBeReplaced = false,
|
|
||||||
isRedacted = false,
|
|
||||||
isUpdated = false,
|
|
||||||
timestamp = A_FAKE_TIMESTAMP,
|
|
||||||
cause = null,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
result.commonAssertions(
|
result.commonAssertions(
|
||||||
expectedCategory = null,
|
expectedCategory = null,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class FakeActiveNotificationsProvider(
|
||||||
private val getMembershipNotificationForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
|
private val getMembershipNotificationForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
|
||||||
private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
|
private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
|
||||||
private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null },
|
private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null },
|
||||||
|
private val getFallbackNotificationResult: (SessionId) -> StatusBarNotification? = { null },
|
||||||
private val countResult: (SessionId) -> Int = { 0 },
|
private val countResult: (SessionId) -> Int = { 0 },
|
||||||
) : ActiveNotificationsProvider {
|
) : ActiveNotificationsProvider {
|
||||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?): List<StatusBarNotification> {
|
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?): List<StatusBarNotification> {
|
||||||
|
|
@ -47,6 +48,10 @@ class FakeActiveNotificationsProvider(
|
||||||
return getSummaryNotificationResult(sessionId)
|
return getSummaryNotificationResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? {
|
||||||
|
return getFallbackNotificationResult(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun count(sessionId: SessionId): Int {
|
override fun count(sessionId: SessionId): Int {
|
||||||
return countResult(sessionId)
|
return countResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab
|
||||||
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaAnyRecorder
|
import io.element.android.tests.testutils.lambda.lambdaAnyRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -34,8 +35,8 @@ class FakeNotificationCreator(
|
||||||
lambdaRecorder { _, _ -> A_NOTIFICATION },
|
lambdaRecorder { _, _ -> A_NOTIFICATION },
|
||||||
var createSimpleNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, SimpleNotifiableEvent, Notification> =
|
var createSimpleNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, SimpleNotifiableEvent, Notification> =
|
||||||
lambdaRecorder { _, _ -> A_NOTIFICATION },
|
lambdaRecorder { _, _ -> A_NOTIFICATION },
|
||||||
var createFallbackNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, FallbackNotifiableEvent, Notification> =
|
var createFallbackNotificationResult: LambdaThreeParamsRecorder<Notification?, NotificationAccountParams, List<FallbackNotifiableEvent>, Notification> =
|
||||||
lambdaRecorder { _, _ -> A_NOTIFICATION },
|
lambdaRecorder { _, _, _ -> A_NOTIFICATION },
|
||||||
var createSummaryListNotificationResult: LambdaFiveParamsRecorder<
|
var createSummaryListNotificationResult: LambdaFiveParamsRecorder<
|
||||||
NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification
|
NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification
|
||||||
> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
|
> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
|
||||||
|
|
@ -75,10 +76,15 @@ class FakeNotificationCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createFallbackNotification(
|
override fun createFallbackNotification(
|
||||||
|
existingNotification: Notification?,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
|
||||||
): Notification {
|
): Notification {
|
||||||
return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent)
|
return createFallbackNotificationResult(
|
||||||
|
existingNotification,
|
||||||
|
notificationAccountParams,
|
||||||
|
fallbackNotifiableEvents,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSummaryListNotification(
|
override fun createSummaryListNotification(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -28,18 +28,17 @@ class FakeNotificationDataFactory(
|
||||||
var messageEventToNotificationsResult: LambdaThreeParamsRecorder<
|
var messageEventToNotificationsResult: LambdaThreeParamsRecorder<
|
||||||
List<NotifiableMessageEvent>, ImageLoader, NotificationAccountParams, List<RoomNotification>
|
List<NotifiableMessageEvent>, ImageLoader, NotificationAccountParams, List<RoomNotification>
|
||||||
> = lambdaRecorder { _, _, _ -> emptyList() },
|
> = lambdaRecorder { _, _, _ -> emptyList() },
|
||||||
var summaryToNotificationsResult: LambdaFiveParamsRecorder<
|
var summaryToNotificationsResult: LambdaFourParamsRecorder<
|
||||||
List<RoomNotification>,
|
List<RoomNotification>,
|
||||||
List<OneShotNotification>,
|
List<OneShotNotification>,
|
||||||
List<OneShotNotification>,
|
List<OneShotNotification>,
|
||||||
List<OneShotNotification>,
|
|
||||||
NotificationAccountParams,
|
NotificationAccountParams,
|
||||||
SummaryNotification
|
SummaryNotification
|
||||||
> = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
|
> = lambdaRecorder { _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
|
||||||
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||||
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||||
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, List<OneShotNotification>> =
|
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, OneShotNotification?> =
|
||||||
lambdaRecorder { _ -> emptyList() },
|
lambdaRecorder { _ -> null },
|
||||||
) : NotificationDataFactory {
|
) : NotificationDataFactory {
|
||||||
override suspend fun toNotifications(
|
override suspend fun toNotifications(
|
||||||
messages: List<NotifiableMessageEvent>,
|
messages: List<NotifiableMessageEvent>,
|
||||||
|
|
@ -69,10 +68,10 @@ class FakeNotificationDataFactory(
|
||||||
|
|
||||||
@JvmName("toNotificationFallbackEvents")
|
@JvmName("toNotificationFallbackEvents")
|
||||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
override fun toNotifications(
|
override fun toNotification(
|
||||||
fallback: List<FallbackNotifiableEvent>,
|
fallback: List<FallbackNotifiableEvent>,
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): List<OneShotNotification> {
|
): OneShotNotification? {
|
||||||
return fallbackEventToNotificationsResult(fallback)
|
return fallbackEventToNotificationsResult(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,14 +79,12 @@ class FakeNotificationDataFactory(
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
): SummaryNotification {
|
): SummaryNotification {
|
||||||
return summaryToNotificationsResult(
|
return summaryToNotificationsResult(
|
||||||
roomNotifications,
|
roomNotifications,
|
||||||
invitationNotifications,
|
invitationNotifications,
|
||||||
simpleNotifications,
|
simpleNotifications,
|
||||||
fallbackNotifications,
|
|
||||||
notificationAccountParams,
|
notificationAccountParams,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,27 +14,25 @@ import io.element.android.libraries.push.impl.notifications.RoomNotification
|
||||||
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
|
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
|
||||||
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
||||||
class FakeSummaryGroupMessageCreator(
|
class FakeSummaryGroupMessageCreator(
|
||||||
var createSummaryNotificationResult: LambdaFiveParamsRecorder<
|
var createSummaryNotificationResult: LambdaFourParamsRecorder<
|
||||||
NotificationAccountParams, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> =
|
NotificationAccountParams, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> =
|
||||||
lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }
|
lambdaRecorder { _, _, _, _ -> A_NOTIFICATION }
|
||||||
) : SummaryGroupMessageCreator {
|
) : SummaryGroupMessageCreator {
|
||||||
override fun createSummaryNotification(
|
override fun createSummaryNotification(
|
||||||
notificationAccountParams: NotificationAccountParams,
|
notificationAccountParams: NotificationAccountParams,
|
||||||
roomNotifications: List<RoomNotification>,
|
roomNotifications: List<RoomNotification>,
|
||||||
invitationNotifications: List<OneShotNotification>,
|
invitationNotifications: List<OneShotNotification>,
|
||||||
simpleNotifications: List<OneShotNotification>,
|
simpleNotifications: List<OneShotNotification>,
|
||||||
fallbackNotifications: List<OneShotNotification>,
|
|
||||||
): Notification {
|
): Notification {
|
||||||
return createSummaryNotificationResult(
|
return createSummaryNotificationResult(
|
||||||
notificationAccountParams,
|
notificationAccountParams,
|
||||||
roomNotifications,
|
roomNotifications,
|
||||||
invitationNotifications,
|
invitationNotifications,
|
||||||
simpleNotifications,
|
simpleNotifications,
|
||||||
fallbackNotifications,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME_2
|
import io.element.android.libraries.matrix.test.A_USER_NAME_2
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
|
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||||
|
|
||||||
fun aSimpleNotifiableEvent(
|
fun aSimpleNotifiableEvent(
|
||||||
sessionId: SessionId = A_SESSION_ID,
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
|
|
@ -141,3 +143,18 @@ fun aNotifiableCallEvent(
|
||||||
senderAvatarUrl = senderAvatarUrl,
|
senderAvatarUrl = senderAvatarUrl,
|
||||||
rtcNotificationType = rtcNotificationType,
|
rtcNotificationType = rtcNotificationType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aFallbackNotifiableEvent(
|
||||||
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
|
) = FallbackNotifiableEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
editedEventId = null,
|
||||||
|
description = "A fallback notification",
|
||||||
|
canBeReplaced = false,
|
||||||
|
isRedacted = false,
|
||||||
|
isUpdated = false,
|
||||||
|
timestamp = A_FAKE_TIMESTAMP,
|
||||||
|
cause = "Unable to decrypt event",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ import io.element.android.libraries.push.impl.notifications.DefaultNotificationR
|
||||||
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
||||||
import io.element.android.libraries.push.impl.notifications.FallbackNotificationFactory
|
import io.element.android.libraries.push.impl.notifications.FallbackNotificationFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||||
|
|
@ -57,7 +57,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
@ -627,18 +626,7 @@ class DefaultPushHandlerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when receiving a fallback event, we notify the push history service about it not being resolved`() = runTest {
|
fun `when receiving a fallback event, we notify the push history service about it not being resolved`() = runTest {
|
||||||
val aNotifiableFallbackEvent = FallbackNotifiableEvent(
|
val aNotifiableFallbackEvent = aFallbackNotifiableEvent()
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
editedEventId = null,
|
|
||||||
description = "A fallback notification",
|
|
||||||
canBeReplaced = false,
|
|
||||||
isRedacted = false,
|
|
||||||
isUpdated = false,
|
|
||||||
timestamp = 0L,
|
|
||||||
cause = "Unable to decrypt event",
|
|
||||||
)
|
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||||
|
|
@ -724,7 +712,6 @@ class DefaultPushHandlerTest {
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
fallbackNotificationFactory = FallbackNotificationFactory(
|
fallbackNotificationFactory = FallbackNotificationFactory(
|
||||||
clock = FakeSystemClock(),
|
clock = FakeSystemClock(),
|
||||||
stringProvider = FakeStringProvider(),
|
|
||||||
),
|
),
|
||||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,13 @@ private const val versionYear = 26
|
||||||
* Month of the version on 2 digits. Value must be in [1,12].
|
* Month of the version on 2 digits. Value must be in [1,12].
|
||||||
* Do not update this value. it is updated by the release script.
|
* Do not update this value. it is updated by the release script.
|
||||||
*/
|
*/
|
||||||
private const val versionMonth = 1
|
private const val versionMonth = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release number in the month. Value must be in [0,99].
|
* Release number in the month. Value must be in [0,99].
|
||||||
* Do not update this value. it is updated by the release script.
|
* Do not update this value. it is updated by the release script.
|
||||||
*/
|
*/
|
||||||
private const val versionReleaseNumber = 2
|
private const val versionReleaseNumber = 0
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
|
||||||
implementation(composeBom)
|
implementation(composeBom)
|
||||||
androidTestImplementation(composeBom)
|
androidTestImplementation(composeBom)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.material)
|
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.compose.material.icons)
|
implementation(libs.androidx.compose.material.icons)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// Make sure it's warm by changing its internal state
|
// Make sure it's warm by changing its internal state
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// The transaction should be present now
|
// The transaction should be present now
|
||||||
|
|
@ -63,9 +63,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Opening the app in a cold state does nothing`() = runTest {
|
fun `Opening the app in a cold state does nothing`() = runTest {
|
||||||
val navigationStateService = FakeAppNavigationStateService().apply {
|
val navigationStateService = FakeAppNavigationStateService(
|
||||||
appNavigationState.emit(AppNavigationState(NavigationState.Root, false))
|
initialAppNavigationState = AppNavigationState(NavigationState.Root, false)
|
||||||
}
|
)
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomListService = FakeRoomListService().apply {
|
||||||
postState(RoomListService.State.Idle)
|
postState(RoomListService.State.Idle)
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +110,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// Make sure it's warm by changing its internal state
|
// Make sure it's warm by changing its internal state
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// The transaction should be present now
|
// The transaction should be present now
|
||||||
|
|
@ -145,9 +145,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// Make sure it's warm by changing its internal state
|
// Make sure it's warm by changing its internal state
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
// The transaction was never added
|
// The transaction was never added
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import io.sentry.Sentry
|
||||||
import io.sentry.SentryTracer
|
import io.sentry.SentryTracer
|
||||||
import io.sentry.protocol.SentryId
|
import io.sentry.protocol.SentryId
|
||||||
import io.sentry.protocol.SentryTransaction
|
import io.sentry.protocol.SentryTransaction
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
|
@ -149,7 +148,7 @@ class SentryAnalyticsProviderTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
appNavigationStateService = FakeAppNavigationStateService(
|
appNavigationStateService = FakeAppNavigationStateService(
|
||||||
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true))
|
initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
|
||||||
)
|
)
|
||||||
).run {
|
).run {
|
||||||
init()
|
init()
|
||||||
|
|
@ -182,7 +181,7 @@ class SentryAnalyticsProviderTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
appNavigationStateService = FakeAppNavigationStateService(
|
appNavigationStateService = FakeAppNavigationStateService(
|
||||||
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
|
initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)
|
||||||
)
|
)
|
||||||
).run {
|
).run {
|
||||||
init()
|
init()
|
||||||
|
|
@ -203,7 +202,7 @@ class SentryAnalyticsProviderTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
appNavigationStateService = FakeAppNavigationStateService(
|
appNavigationStateService = FakeAppNavigationStateService(
|
||||||
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true))
|
initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
|
||||||
)
|
)
|
||||||
).run {
|
).run {
|
||||||
init()
|
init()
|
||||||
|
|
@ -221,7 +220,7 @@ class SentryAnalyticsProviderTest {
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
getDatabaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
getDatabaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
||||||
appNavigationStateService: FakeAppNavigationStateService = FakeAppNavigationStateService(
|
appNavigationStateService: FakeAppNavigationStateService = FakeAppNavigationStateService(
|
||||||
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true))
|
initialAppNavigationState = AppNavigationState(NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
|
||||||
)
|
)
|
||||||
) = SentryAnalyticsProvider(
|
) = SentryAnalyticsProvider(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -23,9 +22,6 @@ interface AppNavigationStateService {
|
||||||
fun onNavigateToSession(owner: String, sessionId: SessionId)
|
fun onNavigateToSession(owner: String, sessionId: SessionId)
|
||||||
fun onLeavingSession(owner: String)
|
fun onLeavingSession(owner: String)
|
||||||
|
|
||||||
fun onNavigateToSpace(owner: String, spaceId: SpaceId)
|
|
||||||
fun onLeavingSpace(owner: String)
|
|
||||||
|
|
||||||
fun onNavigateToRoom(owner: String, roomId: RoomId)
|
fun onNavigateToRoom(owner: String, roomId: RoomId)
|
||||||
fun onLeavingRoom(owner: String)
|
fun onLeavingRoom(owner: String)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,17 +28,10 @@ sealed class NavigationState(open val owner: String) {
|
||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
) : NavigationState(owner)
|
) : NavigationState(owner)
|
||||||
|
|
||||||
data class Space(
|
|
||||||
override val owner: String,
|
|
||||||
// Can be fake value, if no space is selected
|
|
||||||
val spaceId: SpaceId,
|
|
||||||
val parentSession: Session,
|
|
||||||
) : NavigationState(owner)
|
|
||||||
|
|
||||||
data class Room(
|
data class Room(
|
||||||
override val owner: String,
|
override val owner: String,
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val parentSpace: Space,
|
val parentSession: Session,
|
||||||
) : NavigationState(owner)
|
) : NavigationState(owner)
|
||||||
|
|
||||||
data class Thread(
|
data class Thread(
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,14 @@ package io.element.android.services.appnavstate.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
|
||||||
fun NavigationState.currentSessionId(): SessionId? {
|
fun NavigationState.currentSessionId(): SessionId? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
NavigationState.Root -> null
|
NavigationState.Root -> null
|
||||||
is NavigationState.Session -> sessionId
|
is NavigationState.Session -> sessionId
|
||||||
is NavigationState.Space -> parentSession.sessionId
|
is NavigationState.Room -> parentSession.sessionId
|
||||||
is NavigationState.Room -> parentSpace.parentSession.sessionId
|
is NavigationState.Thread -> parentRoom.parentSession.sessionId
|
||||||
is NavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavigationState.currentSpaceId(): SpaceId? {
|
|
||||||
return when (this) {
|
|
||||||
NavigationState.Root -> null
|
|
||||||
is NavigationState.Session -> null
|
|
||||||
is NavigationState.Space -> spaceId
|
|
||||||
is NavigationState.Room -> parentSpace.spaceId
|
|
||||||
is NavigationState.Thread -> parentRoom.parentSpace.spaceId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +25,6 @@ fun NavigationState.currentRoomId(): RoomId? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
NavigationState.Root -> null
|
NavigationState.Root -> null
|
||||||
is NavigationState.Session -> null
|
is NavigationState.Session -> null
|
||||||
is NavigationState.Space -> null
|
|
||||||
is NavigationState.Room -> roomId
|
is NavigationState.Room -> roomId
|
||||||
is NavigationState.Thread -> parentRoom.roomId
|
is NavigationState.Thread -> parentRoom.roomId
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +34,6 @@ fun NavigationState.currentThreadId(): ThreadId? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
NavigationState.Root -> null
|
NavigationState.Root -> null
|
||||||
is NavigationState.Session -> null
|
is NavigationState.Session -> null
|
||||||
is NavigationState.Space -> null
|
|
||||||
is NavigationState.Room -> null
|
is NavigationState.Room -> null
|
||||||
is NavigationState.Thread -> threadId
|
is NavigationState.Thread -> threadId
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
|
|
@ -62,7 +61,6 @@ class DefaultAppNavigationStateService(
|
||||||
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
|
||||||
val newValue: NavigationState.Session = when (currentValue) {
|
val newValue: NavigationState.Session = when (currentValue) {
|
||||||
is NavigationState.Session,
|
is NavigationState.Session,
|
||||||
is NavigationState.Space,
|
|
||||||
is NavigationState.Room,
|
is NavigationState.Room,
|
||||||
is NavigationState.Thread,
|
is NavigationState.Thread,
|
||||||
is NavigationState.Root -> NavigationState.Session(owner, sessionId)
|
is NavigationState.Root -> NavigationState.Session(owner, sessionId)
|
||||||
|
|
@ -70,28 +68,14 @@ class DefaultAppNavigationStateService(
|
||||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) {
|
|
||||||
val currentValue = state.value.navigationState
|
|
||||||
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
|
|
||||||
val newValue: NavigationState.Space = when (currentValue) {
|
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
|
||||||
is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue)
|
|
||||||
is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
|
|
||||||
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
|
|
||||||
is NavigationState.Thread -> NavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession)
|
|
||||||
}
|
|
||||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigateToRoom(owner: String, roomId: RoomId) {
|
override fun onNavigateToRoom(owner: String, roomId: RoomId) {
|
||||||
val currentValue = state.value.navigationState
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
|
||||||
val newValue: NavigationState.Room = when (currentValue) {
|
val newValue: NavigationState.Room = when (currentValue) {
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
NavigationState.Root -> return logError("onNavigateToSession()")
|
||||||
is NavigationState.Session -> return logError("onNavigateToSpace()")
|
is NavigationState.Session -> NavigationState.Room(owner, roomId, currentValue)
|
||||||
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
|
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSession)
|
||||||
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
|
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSession)
|
||||||
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
|
|
||||||
}
|
}
|
||||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
@ -101,8 +85,7 @@ class DefaultAppNavigationStateService(
|
||||||
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
|
||||||
val newValue: NavigationState.Thread = when (currentValue) {
|
val newValue: NavigationState.Thread = when (currentValue) {
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
NavigationState.Root -> return logError("onNavigateToSession()")
|
||||||
is NavigationState.Session -> return logError("onNavigateToSpace()")
|
is NavigationState.Session -> return logError("onNavigateToRoom()")
|
||||||
is NavigationState.Space -> return logError("onNavigateToRoom()")
|
|
||||||
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
|
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
|
||||||
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
||||||
}
|
}
|
||||||
|
|
@ -115,8 +98,7 @@ class DefaultAppNavigationStateService(
|
||||||
if (!currentValue.assertOwner(owner)) return
|
if (!currentValue.assertOwner(owner)) return
|
||||||
val newValue: NavigationState.Room = when (currentValue) {
|
val newValue: NavigationState.Room = when (currentValue) {
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
NavigationState.Root -> return logError("onNavigateToSession()")
|
||||||
is NavigationState.Session -> return logError("onNavigateToSpace()")
|
is NavigationState.Session -> return logError("onNavigateToRoom()")
|
||||||
is NavigationState.Space -> return logError("onNavigateToRoom()")
|
|
||||||
is NavigationState.Room -> return logError("onNavigateToThread()")
|
is NavigationState.Room -> return logError("onNavigateToThread()")
|
||||||
is NavigationState.Thread -> currentValue.parentRoom
|
is NavigationState.Thread -> currentValue.parentRoom
|
||||||
}
|
}
|
||||||
|
|
@ -127,26 +109,11 @@ class DefaultAppNavigationStateService(
|
||||||
val currentValue = state.value.navigationState
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
|
||||||
if (!currentValue.assertOwner(owner)) return
|
if (!currentValue.assertOwner(owner)) return
|
||||||
val newValue: NavigationState.Space = when (currentValue) {
|
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
|
||||||
is NavigationState.Session -> return logError("onNavigateToSpace()")
|
|
||||||
is NavigationState.Space -> return logError("onNavigateToRoom()")
|
|
||||||
is NavigationState.Room -> currentValue.parentSpace
|
|
||||||
is NavigationState.Thread -> currentValue.parentRoom.parentSpace
|
|
||||||
}
|
|
||||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLeavingSpace(owner: String) {
|
|
||||||
val currentValue = state.value.navigationState
|
|
||||||
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
|
|
||||||
if (!currentValue.assertOwner(owner)) return
|
|
||||||
val newValue: NavigationState.Session = when (currentValue) {
|
val newValue: NavigationState.Session = when (currentValue) {
|
||||||
NavigationState.Root -> return logError("onNavigateToSession()")
|
NavigationState.Root -> return logError("onNavigateToSession()")
|
||||||
is NavigationState.Session -> return logError("onNavigateToSpace()")
|
is NavigationState.Session -> return logError("onNavigateToRoom()")
|
||||||
is NavigationState.Space -> currentValue.parentSession
|
is NavigationState.Room -> currentValue.parentSession
|
||||||
is NavigationState.Room -> currentValue.parentSpace.parentSession
|
is NavigationState.Thread -> currentValue.parentRoom.parentSession
|
||||||
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
|
|
||||||
}
|
}
|
||||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_SPACE_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
|
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
import io.element.android.services.appnavstate.test.A_ROOM_OWNER
|
import io.element.android.services.appnavstate.test.A_ROOM_OWNER
|
||||||
import io.element.android.services.appnavstate.test.A_SESSION_OWNER
|
import io.element.android.services.appnavstate.test.A_SESSION_OWNER
|
||||||
import io.element.android.services.appnavstate.test.A_SPACE_OWNER
|
|
||||||
import io.element.android.services.appnavstate.test.A_THREAD_OWNER
|
import io.element.android.services.appnavstate.test.A_THREAD_OWNER
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -33,22 +30,17 @@ class DefaultNavigationStateServiceTest {
|
||||||
private val navigationStateRoot = NavigationState.Root
|
private val navigationStateRoot = NavigationState.Root
|
||||||
private val navigationStateSession = NavigationState.Session(
|
private val navigationStateSession = NavigationState.Session(
|
||||||
owner = A_SESSION_OWNER,
|
owner = A_SESSION_OWNER,
|
||||||
sessionId = A_SESSION_ID
|
sessionId = A_SESSION_ID,
|
||||||
)
|
|
||||||
private val navigationStateSpace = NavigationState.Space(
|
|
||||||
owner = A_SPACE_OWNER,
|
|
||||||
spaceId = A_SPACE_ID,
|
|
||||||
parentSession = navigationStateSession
|
|
||||||
)
|
)
|
||||||
private val navigationStateRoom = NavigationState.Room(
|
private val navigationStateRoom = NavigationState.Room(
|
||||||
owner = A_ROOM_OWNER,
|
owner = A_ROOM_OWNER,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
parentSpace = navigationStateSpace
|
parentSession = navigationStateSession,
|
||||||
)
|
)
|
||||||
private val navigationStateThread = NavigationState.Thread(
|
private val navigationStateThread = NavigationState.Thread(
|
||||||
owner = A_THREAD_OWNER,
|
owner = A_THREAD_OWNER,
|
||||||
threadId = A_THREAD_ID,
|
threadId = A_THREAD_ID,
|
||||||
parentRoom = navigationStateRoom
|
parentRoom = navigationStateRoom,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -57,8 +49,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
||||||
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
||||||
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
||||||
|
|
@ -67,8 +57,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
service.onLeavingThread(A_THREAD_OWNER)
|
service.onLeavingThread(A_THREAD_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
||||||
service.onLeavingRoom(A_ROOM_OWNER)
|
service.onLeavingRoom(A_ROOM_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
service.onLeavingSession(A_SESSION_OWNER)
|
service.onLeavingSession(A_SESSION_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
||||||
|
|
@ -77,7 +65,7 @@ class DefaultNavigationStateServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun testFailure() = runTest {
|
fun testFailure() = runTest {
|
||||||
val service = createStateService()
|
val service = createStateService()
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root)
|
assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,11 +80,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
service.navigateToSession()
|
service.navigateToSession()
|
||||||
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
// From space (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From room
|
// From room
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToRoom()
|
service.navigateToRoom()
|
||||||
|
|
@ -116,15 +99,10 @@ class DefaultNavigationStateServiceTest {
|
||||||
// From root (no effect)
|
// From root (no effect)
|
||||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
||||||
// From session (no effect)
|
// From session
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToSession()
|
service.navigateToSession()
|
||||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
|
||||||
// From space
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
||||||
// From room
|
// From room
|
||||||
service.reset()
|
service.reset()
|
||||||
|
|
@ -139,35 +117,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testOnNavigateToSpace() = runTest {
|
|
||||||
val service = createStateService()
|
|
||||||
// From root (no effect)
|
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
|
||||||
// From session
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSession()
|
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From space
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
// Navigate to another space
|
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID_2)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace.copy(spaceId = A_SPACE_ID_2))
|
|
||||||
// From room (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToRoom()
|
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From thread (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToThread()
|
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnNavigateToSession() = runTest {
|
fun testOnNavigateToSession() = runTest {
|
||||||
val service = createStateService()
|
val service = createStateService()
|
||||||
|
|
@ -180,11 +129,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
// Navigate to another session
|
// Navigate to another session
|
||||||
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID_2)
|
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID_2)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession.copy(sessionId = A_SESSION_ID_2))
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession.copy(sessionId = A_SESSION_ID_2))
|
||||||
// From space
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
|
||||||
// From room
|
// From room
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToRoom()
|
service.navigateToRoom()
|
||||||
|
|
@ -208,11 +152,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
service.navigateToSession()
|
service.navigateToSession()
|
||||||
service.onLeavingThread(A_THREAD_OWNER)
|
service.onLeavingThread(A_THREAD_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
// From space (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onLeavingThread(A_THREAD_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From room (no effect)
|
// From room (no effect)
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToRoom()
|
service.navigateToRoom()
|
||||||
|
|
@ -236,16 +175,11 @@ class DefaultNavigationStateServiceTest {
|
||||||
service.navigateToSession()
|
service.navigateToSession()
|
||||||
service.onLeavingRoom(A_ROOM_OWNER)
|
service.onLeavingRoom(A_ROOM_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
// From space (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onLeavingRoom(A_ROOM_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From room
|
// From room
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToRoom()
|
service.navigateToRoom()
|
||||||
service.onLeavingRoom(A_ROOM_OWNER)
|
service.onLeavingRoom(A_ROOM_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
||||||
// From thread (no effect)
|
// From thread (no effect)
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToThread()
|
service.navigateToThread()
|
||||||
|
|
@ -253,34 +187,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testOnLeavingSpace() = runTest {
|
|
||||||
val service = createStateService()
|
|
||||||
// From root (no effect)
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
|
||||||
// From session (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSession()
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
|
||||||
// From space
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
|
|
||||||
// From room (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToRoom()
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
|
|
||||||
// From thread (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToThread()
|
|
||||||
service.onLeavingSpace(A_SPACE_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnLeavingSession() = runTest {
|
fun testOnLeavingSession() = runTest {
|
||||||
val service = createStateService()
|
val service = createStateService()
|
||||||
|
|
@ -292,11 +198,6 @@ class DefaultNavigationStateServiceTest {
|
||||||
service.navigateToSession()
|
service.navigateToSession()
|
||||||
service.onLeavingSession(A_SESSION_OWNER)
|
service.onLeavingSession(A_SESSION_OWNER)
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
|
||||||
// From space (no effect)
|
|
||||||
service.reset()
|
|
||||||
service.navigateToSpace()
|
|
||||||
service.onLeavingSession(A_SESSION_OWNER)
|
|
||||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
|
|
||||||
// From room (no effect)
|
// From room (no effect)
|
||||||
service.reset()
|
service.reset()
|
||||||
service.navigateToRoom()
|
service.navigateToRoom()
|
||||||
|
|
@ -318,13 +219,8 @@ class DefaultNavigationStateServiceTest {
|
||||||
onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AppNavigationStateService.navigateToSpace() {
|
|
||||||
navigateToSession()
|
|
||||||
onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun AppNavigationStateService.navigateToRoom() {
|
private fun AppNavigationStateService.navigateToRoom() {
|
||||||
navigateToSpace()
|
navigateToSession()
|
||||||
onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,18 @@
|
||||||
|
|
||||||
package io.element.android.services.appnavstate.test
|
package io.element.android.services.appnavstate.test
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
|
|
||||||
const val A_SESSION_OWNER = "aSessionOwner"
|
const val A_SESSION_OWNER = "aSessionOwner"
|
||||||
const val A_SPACE_OWNER = "aSpaceOwner"
|
|
||||||
const val A_ROOM_OWNER = "aRoomOwner"
|
const val A_ROOM_OWNER = "aRoomOwner"
|
||||||
const val A_THREAD_OWNER = "aThreadOwner"
|
const val A_THREAD_OWNER = "aThreadOwner"
|
||||||
|
|
||||||
fun aNavigationState(
|
fun aNavigationState(
|
||||||
sessionId: SessionId? = null,
|
sessionId: SessionId? = null,
|
||||||
spaceId: SpaceId? = MAIN_SPACE,
|
|
||||||
roomId: RoomId? = null,
|
roomId: RoomId? = null,
|
||||||
threadId: ThreadId? = null,
|
threadId: ThreadId? = null,
|
||||||
): NavigationState {
|
): NavigationState {
|
||||||
|
|
@ -30,16 +27,20 @@ fun aNavigationState(
|
||||||
return NavigationState.Root
|
return NavigationState.Root
|
||||||
}
|
}
|
||||||
val session = NavigationState.Session(A_SESSION_OWNER, sessionId)
|
val session = NavigationState.Session(A_SESSION_OWNER, sessionId)
|
||||||
if (spaceId == null) {
|
if (roomId == null) {
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session)
|
val room = NavigationState.Room(A_ROOM_OWNER, roomId, session)
|
||||||
if (roomId == null) {
|
|
||||||
return space
|
|
||||||
}
|
|
||||||
val room = NavigationState.Room(A_ROOM_OWNER, roomId, space)
|
|
||||||
if (threadId == null) {
|
if (threadId == null) {
|
||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
return NavigationState.Thread(A_THREAD_OWNER, threadId, room)
|
return NavigationState.Thread(A_THREAD_OWNER, threadId, room)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun anAppNavigationState(
|
||||||
|
navigationState: NavigationState = aNavigationState(),
|
||||||
|
isInForeground: Boolean = true,
|
||||||
|
) = AppNavigationState(
|
||||||
|
navigationState = navigationState,
|
||||||
|
isInForeground = isInForeground,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,33 +10,32 @@ package io.element.android.services.appnavstate.test
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class FakeAppNavigationStateService(
|
class FakeAppNavigationStateService(
|
||||||
override val appNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(
|
initialAppNavigationState: AppNavigationState = AppNavigationState(
|
||||||
AppNavigationState(
|
navigationState = NavigationState.Root,
|
||||||
navigationState = NavigationState.Root,
|
isInForeground = true,
|
||||||
isInForeground = true,
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
) : AppNavigationStateService {
|
) : AppNavigationStateService {
|
||||||
|
private val _appNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(initialAppNavigationState)
|
||||||
|
override val appNavigationState = _appNavigationState.asStateFlow()
|
||||||
|
|
||||||
|
fun emitNavigationState(state: AppNavigationState) {
|
||||||
|
_appNavigationState.value = state
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
|
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
|
||||||
override fun onLeavingSession(owner: String) = Unit
|
override fun onLeavingSession(owner: String) = Unit
|
||||||
|
|
||||||
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) = Unit
|
|
||||||
|
|
||||||
override fun onLeavingSpace(owner: String) = Unit
|
|
||||||
|
|
||||||
override fun onNavigateToRoom(owner: String, roomId: RoomId) = Unit
|
override fun onNavigateToRoom(owner: String, roomId: RoomId) = Unit
|
||||||
|
|
||||||
override fun onLeavingRoom(owner: String) = Unit
|
override fun onLeavingRoom(owner: String) = Unit
|
||||||
|
|
||||||
override fun onNavigateToThread(owner: String, threadId: ThreadId) = Unit
|
override fun onNavigateToThread(owner: String, threadId: ThreadId) = Unit
|
||||||
|
|
||||||
override fun onLeavingThread(owner: String) = Unit
|
override fun onLeavingThread(owner: String) = Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:913d6230ab2b470dd5393344395bb9c25973318f09ecdc52499a17e9c9e8faba
|
oid sha256:4dac0f93eb31b26fa32173fbd834c7f661e4f47c79db66fa4d1536d938a4585d
|
||||||
size 66219
|
size 66108
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f59ff395027433af611ef1aec1a1d3e5a7d670df3c77d1c5d01154199c123a71
|
oid sha256:4c3e5ef9368d68f661350a7a31b98b3ae3fbf975bc11d6f1b9e5ac908e6699dc
|
||||||
size 58586
|
size 58355
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6356cf3f4d2ec169852024b6a939a3b53bc117aa6d27179783252bc19038b0b4
|
oid sha256:f0caea3f8eb17dbebdc738a60f2f734ec26c1f94f511eab50669a1c2ec0370a1
|
||||||
size 23101
|
size 23094
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:86e1bd1197fb4affc1b14b7a2f7f24a1c89ffaaf8bdb17312c0e4059175bb878
|
oid sha256:0e41fe1823898014736c2b68a3d60f055d483626deae2407ad0145703a6fc1f6
|
||||||
size 21224
|
size 21317
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue