Merge branch 'release/25.04.1' into main
This commit is contained in:
commit
a52e8559fd
135 changed files with 2199 additions and 397 deletions
68
CHANGES.md
68
CHANGES.md
|
|
@ -1,3 +1,71 @@
|
||||||
|
Changes in Element X v25.04.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v25.04.0 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Enable Rust trace log packs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4514
|
||||||
|
* Allow using a hardware keyboard to unlock the app using a pin code by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4530
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Change (mention span) : rework and add more cases by @ganfra in https://github.com/element-hq/element-x-android/pull/4476
|
||||||
|
* Add kick (remove) confirmation and reason by @bmarty in https://github.com/element-hq/element-x-android/pull/4507
|
||||||
|
* Remove the green badge on a pending invite after a first preview by @bmarty in https://github.com/element-hq/element-x-android/pull/4532
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Improve touch indicators for the user info UI in the timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4482
|
||||||
|
* Limit the text length in the 'in reply to' preview by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4491
|
||||||
|
* Timeline header: ensure that the decoration is clickable by @bmarty in https://github.com/element-hq/element-x-android/pull/4495
|
||||||
|
* Add video autoplay to media gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4499
|
||||||
|
* Add `WakeLock` to dismiss ringing call screen when call is cancelled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4478
|
||||||
|
* Make sure the live timeline is destroyed before clearing a room's cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4515
|
||||||
|
* Fix bullet points not having leading margin on timeline items by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4536
|
||||||
|
* Fix the share location URI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4544
|
||||||
|
* Add a inderminate progress bar when loging out and in Waiting state. by @bmarty in https://github.com/element-hq/element-x-android/pull/4538
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4506
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4543
|
||||||
|
### 🧱 Build
|
||||||
|
* Element config by @bmarty in https://github.com/element-hq/element-x-android/pull/4471
|
||||||
|
* Check if Manifest.permission.REQUEST_INSTALL_PACKAGES is in the manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/4490
|
||||||
|
* Remove nightly_enterprise.yml. by @bmarty in https://github.com/element-hq/element-x-android/pull/4492
|
||||||
|
* Log the packageId which is currently built. by @bmarty in https://github.com/element-hq/element-x-android/pull/4494
|
||||||
|
* Use handy buildConfigFieldStr. by @bmarty in https://github.com/element-hq/element-x-android/pull/4501
|
||||||
|
* Fix warnings in InMemoryAppPreferencesStore by @bmarty in https://github.com/element-hq/element-x-android/pull/4523
|
||||||
|
### Dependency upgrades
|
||||||
|
* fix(deps): update camera to v1.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4483
|
||||||
|
* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.5 by @renovate in https://github.com/element-hq/element-x-android/pull/4487
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4469
|
||||||
|
* fix(deps): update dependency androidx.compose:compose-bom to v2025.03.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4484
|
||||||
|
* fix(deps): update dependencyanalysis to v2.13.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4493
|
||||||
|
* fix(deps): update media3 to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4488
|
||||||
|
* fix(deps): update dependency io.element.android:element-call-embedded to v0.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4498
|
||||||
|
* fix(deps): update dependency com.google.firebase:firebase-bom to v33.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4508
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4516
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4509
|
||||||
|
* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4444
|
||||||
|
* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4522
|
||||||
|
* fix(deps): update dependencyanalysis to v2.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4527
|
||||||
|
* fix(deps): update dependency io.element.android:compound-android to v25.4.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4537
|
||||||
|
* chore(deps): update plugin dependencycheck to v12.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4540
|
||||||
|
* fix(deps): update appyx to v1.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4547
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4548
|
||||||
|
### Others
|
||||||
|
* Update screenshots by @bmarty in https://github.com/element-hq/element-x-android/pull/4497
|
||||||
|
* Update store description. by @bmarty in https://github.com/element-hq/element-x-android/pull/4496
|
||||||
|
* Improve TextFieldDialog by @bmarty in https://github.com/element-hq/element-x-android/pull/4512
|
||||||
|
* Make `RustMatrixClient.close` asynchronous by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4513
|
||||||
|
* Replace OutlinedTextField by our TextField by @bmarty in https://github.com/element-hq/element-x-android/pull/4521
|
||||||
|
* Remove alias from room invite item by @bmarty in https://github.com/element-hq/element-x-android/pull/4531
|
||||||
|
* Remember flows by @bmarty in https://github.com/element-hq/element-x-android/pull/4533
|
||||||
|
* Use colors from compound for badges by @bmarty in https://github.com/element-hq/element-x-android/pull/4545
|
||||||
|
* Update app icon by @bmarty in https://github.com/element-hq/element-x-android/pull/4534
|
||||||
|
* Click on userId / room alias to copy value to clipboard. by @bmarty in https://github.com/element-hq/element-x-android/pull/4549
|
||||||
|
* Run the 'prevent blocked' workflow even if PR has conflicts by @robintown in https://github.com/element-hq/element-x-android/pull/4432
|
||||||
|
* Update wording for push provider support test. (#4079) by @bmarty in https://github.com/element-hq/element-x-android/pull/4553
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.4...v25.04.0
|
||||||
|
|
||||||
Changes in Element X v25.03.4
|
Changes in Element X v25.03.4
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ 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.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.SessionId
|
||||||
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.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
|
@ -378,6 +379,14 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||||
|
// We do not check the sessionId, but it will have to be done at some point (multi account)
|
||||||
|
if (sessionId != matrixClient.sessionId) {
|
||||||
|
Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId")
|
||||||
|
}
|
||||||
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||||
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202504010.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202504010.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
|
||||||
|
|
@ -12,36 +12,25 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
import io.element.android.libraries.androidutils.hash.hash
|
import io.element.android.libraries.androidutils.hash.hash
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.di.SingleIn
|
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
|
||||||
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.user.CurrentSessionIdHolder
|
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
|
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
|
||||||
|
|
||||||
@SingleIn(SessionScope::class)
|
class DefaultSeenInvitesStore(
|
||||||
@ContributesBinding(SessionScope::class)
|
context: Context,
|
||||||
class DefaultSeenInvitesStore @Inject constructor(
|
sessionId: SessionId,
|
||||||
@ApplicationContext context: Context,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
currentSessionIdHolder: CurrentSessionIdHolder,
|
|
||||||
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
|
||||||
sessionObserver: SessionObserver,
|
sessionObserver: SessionObserver,
|
||||||
) : SeenInvitesStore {
|
) : SeenInvitesStore {
|
||||||
private val sessionId: SessionId = currentSessionIdHolder.current
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sessionObserver.addListener(object : SessionListener {
|
sessionObserver.addListener(object : SessionListener {
|
||||||
override suspend fun onSessionCreated(userId: String) = Unit
|
override suspend fun onSessionCreated(userId: String) = Unit
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultSeenInvitesStoreFactory @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val sessionObserver: SessionObserver,
|
||||||
|
) : SeenInvitesStoreFactory {
|
||||||
|
// We can have only one class accessing a single data store, so keep a cache of them.
|
||||||
|
private val cache = ConcurrentHashMap<SessionId, SeenInvitesStore>()
|
||||||
|
|
||||||
|
override fun getOrCreate(
|
||||||
|
sessionId: SessionId,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SeenInvitesStore {
|
||||||
|
return cache.getOrPut(sessionId) {
|
||||||
|
DefaultSeenInvitesStore(
|
||||||
|
context = context,
|
||||||
|
sessionId = sessionId,
|
||||||
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
sessionObserver = sessionObserver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
interface SeenInvitesStoreFactory {
|
||||||
|
fun getOrCreate(
|
||||||
|
sessionId: SessionId,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SeenInvitesStore
|
||||||
|
}
|
||||||
|
|
@ -10,14 +10,31 @@ package io.element.android.features.invite.impl.di
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
@ContributesTo(SessionScope::class)
|
@ContributesTo(SessionScope::class)
|
||||||
@Module
|
@Module
|
||||||
interface InviteModule {
|
interface InviteModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter<AcceptDeclineInviteState>
|
fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter<AcceptDeclineInviteState>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
fun providesSeenInvitesStore(
|
||||||
|
factory: SeenInvitesStoreFactory,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
): SeenInvitesStore {
|
||||||
|
return factory.getOrCreate(
|
||||||
|
matrixClient.sessionId,
|
||||||
|
matrixClient.sessionCoroutineScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
implementation(projects.features.invite.api)
|
implementation(projects.features.invite.api)
|
||||||
implementation(projects.features.roomdirectory.api)
|
implementation(projects.features.roomdirectory.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
@ -46,5 +47,6 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -69,6 +70,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
private val forgetRoom: ForgetRoom,
|
private val forgetRoom: ForgetRoom,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -94,6 +96,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
var knockMessage by rememberSaveable { mutableStateOf("") }
|
var knockMessage by rememberSaveable { mutableStateOf("") }
|
||||||
var isDismissingContent by remember { mutableStateOf(false) }
|
var isDismissingContent by remember { mutableStateOf(false) }
|
||||||
|
val hideInviteAvatars by remember {
|
||||||
|
appPreferencesStore.getHideInviteAvatarsFlow()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
val contentState by produceState<ContentState>(
|
val contentState by produceState<ContentState>(
|
||||||
initialValue = ContentState.Loading,
|
initialValue = ContentState.Loading,
|
||||||
key1 = roomInfo,
|
key1 = roomInfo,
|
||||||
|
|
@ -202,6 +207,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
cancelKnockAction = cancelKnockAction.value,
|
cancelKnockAction = cancelKnockAction.value,
|
||||||
applicationName = buildMeta.applicationName,
|
applicationName = buildMeta.applicationName,
|
||||||
knockMessage = knockMessage,
|
knockMessage = knockMessage,
|
||||||
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ data class JoinRoomState(
|
||||||
val cancelKnockAction: AsyncAction<Unit>,
|
val cancelKnockAction: AsyncAction<Unit>,
|
||||||
private val applicationName: String,
|
private val applicationName: String,
|
||||||
val knockMessage: String,
|
val knockMessage: String,
|
||||||
|
val hideInviteAvatars: Boolean,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
|
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
|
||||||
|
|
@ -57,6 +58,8 @@ data class JoinRoomState(
|
||||||
}
|
}
|
||||||
else -> JoinAuthorisationStatus.None
|
else -> JoinAuthorisationStatus.None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hideAvatarsImages = hideInviteAvatars && joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ fun aJoinRoomState(
|
||||||
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
knockMessage: String = "",
|
knockMessage: String = "",
|
||||||
|
hideInviteAvatars: Boolean = false,
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
roomIdOrAlias = roomIdOrAlias,
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
|
@ -182,6 +183,7 @@ fun aJoinRoomState(
|
||||||
forgetAction = forgetAction,
|
forgetAction = forgetAction,
|
||||||
applicationName = "AppName",
|
applicationName = "AppName",
|
||||||
knockMessage = knockMessage,
|
knockMessage = knockMessage,
|
||||||
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ fun JoinRoomView(
|
||||||
roomIdOrAlias = state.roomIdOrAlias,
|
roomIdOrAlias = state.roomIdOrAlias,
|
||||||
contentState = state.contentState,
|
contentState = state.contentState,
|
||||||
knockMessage = state.knockMessage,
|
knockMessage = state.knockMessage,
|
||||||
|
hideAvatarsImages = state.hideAvatarsImages,
|
||||||
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
|
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -371,6 +372,7 @@ private fun JoinRoomContent(
|
||||||
roomIdOrAlias: RoomIdOrAlias,
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
contentState: ContentState,
|
contentState: ContentState,
|
||||||
knockMessage: String,
|
knockMessage: String,
|
||||||
|
hideAvatarsImages: Boolean,
|
||||||
onKnockMessageUpdate: (String) -> Unit,
|
onKnockMessageUpdate: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -385,13 +387,14 @@ private fun JoinRoomContent(
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||||
if (inviteSender != null) {
|
if (inviteSender != null) {
|
||||||
InviteSenderView(inviteSender = inviteSender)
|
InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
DefaultLoadedContent(
|
DefaultLoadedContent(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
knockMessage = knockMessage,
|
knockMessage = knockMessage,
|
||||||
|
hideAvatarImage = hideAvatarsImages,
|
||||||
onKnockMessageUpdate = onKnockMessageUpdate
|
onKnockMessageUpdate = onKnockMessageUpdate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -474,13 +477,14 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
|
||||||
private fun DefaultLoadedContent(
|
private fun DefaultLoadedContent(
|
||||||
contentState: ContentState.Loaded,
|
contentState: ContentState.Loaded,
|
||||||
knockMessage: String,
|
knockMessage: String,
|
||||||
|
hideAvatarImage: Boolean,
|
||||||
onKnockMessageUpdate: (String) -> Unit,
|
onKnockMessageUpdate: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
RoomPreviewOrganism(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
Avatar(contentState.avatarData(AvatarSize.RoomHeader), hideImage = hideAvatarImage)
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
if (contentState.name != null) {
|
if (contentState.name != null) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
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.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -36,6 +37,7 @@ object JoinRoomModule {
|
||||||
forgetRoom: ForgetRoom,
|
forgetRoom: ForgetRoom,
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
buildMeta: BuildMeta,
|
buildMeta: BuildMeta,
|
||||||
|
appPreferencesStore: AppPreferencesStore,
|
||||||
seenInvitesStore: SeenInvitesStore,
|
seenInvitesStore: SeenInvitesStore,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
return object : JoinRoomPresenter.Factory {
|
return object : JoinRoomPresenter.Factory {
|
||||||
|
|
@ -59,6 +61,7 @@ object JoinRoomModule {
|
||||||
cancelKnockRoom = cancelKnockRoom,
|
cancelKnockRoom = cancelKnockRoom,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
|
@ -768,6 +770,7 @@ class JoinRoomPresenterTest {
|
||||||
forgetRoom: ForgetRoom = FakeForgetRoom(),
|
forgetRoom: ForgetRoom = FakeForgetRoom(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||||
): JoinRoomPresenter {
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
|
|
@ -783,6 +786,7 @@ class JoinRoomPresenterTest {
|
||||||
forgetRoom = forgetRoom,
|
forgetRoom = forgetRoom,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,25 +16,32 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
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.media.MediaPreviewValue
|
||||||
|
import io.element.android.libraries.matrix.api.media.isPreviewEnabled
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineProtectionPresenter @Inject constructor(
|
class TimelineProtectionPresenter @Inject constructor(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val room: MatrixRoom,
|
||||||
) : Presenter<TimelineProtectionState> {
|
) : Presenter<TimelineProtectionState> {
|
||||||
|
private val allowedEvents = mutableStateOf<Set<EventId>>(setOf())
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineProtectionState {
|
override fun present(): TimelineProtectionState {
|
||||||
val hideMediaContent by remember {
|
val mediaPreviewValue = remember {
|
||||||
appPreferencesStore.doesHideImagesAndVideosFlow()
|
appPreferencesStore.getTimelineMediaPreviewValueFlow()
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = MediaPreviewValue.On)
|
||||||
var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
|
val roomInfo = room.roomInfoFlow.collectAsState()
|
||||||
val protectionState by remember(hideMediaContent) {
|
val protectionState by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (hideMediaContent) {
|
val isPreviewEnabled = mediaPreviewValue.value.isPreviewEnabled(roomInfo.value.joinRule)
|
||||||
ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
|
if (isPreviewEnabled) {
|
||||||
} else {
|
|
||||||
ProtectionState.RenderAll
|
ProtectionState.RenderAll
|
||||||
|
} else {
|
||||||
|
ProtectionState.RenderOnly(eventIds = allowedEvents.value.toImmutableSet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +49,7 @@ class TimelineProtectionPresenter @Inject constructor(
|
||||||
fun handleEvent(event: TimelineProtectionEvent) {
|
fun handleEvent(event: TimelineProtectionEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is TimelineProtectionEvent.ShowContent -> {
|
is TimelineProtectionEvent.ShowContent -> {
|
||||||
allowedEvents = allowedEvents + setOfNotNull(event.eventId)
|
allowedEvents.value = allowedEvents.value + setOfNotNull(event.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,12 @@
|
||||||
package io.element.android.features.messages.impl.timeline.protection
|
package io.element.android.features.messages.impl.timeline.protection
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
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.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -32,8 +37,8 @@ class TimelineProtectionPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - protected`() = runTest {
|
fun `present - media preview value off`() = runTest {
|
||||||
val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true)
|
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Off)
|
||||||
val presenter = createPresenter(appPreferencesStore)
|
val presenter = createPresenter(appPreferencesStore)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -47,9 +52,42 @@ class TimelineProtectionPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - media preview value private in public room`() = runTest {
|
||||||
|
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
|
||||||
|
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Public))
|
||||||
|
val presenter = createPresenter(appPreferencesStore, room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
|
||||||
|
// ShowContent with null should have no effect.
|
||||||
|
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
|
||||||
|
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - media preview value private in non public room`() = runTest {
|
||||||
|
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
|
||||||
|
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite))
|
||||||
|
val presenter = createPresenter(appPreferencesStore, room)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll)
|
||||||
|
// ShowContent with null should have no effect.
|
||||||
|
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
|
||||||
|
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
) = TimelineProtectionPresenter(
|
) = TimelineProtectionPresenter(
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
room = room,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
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 kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
interface PreferencesEntryPoint : FeatureEntryPoint {
|
interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||||
|
|
@ -29,6 +31,7 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||||
|
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
|
|
@ -41,5 +44,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
fun onSecureBackupClick()
|
fun onSecureBackupClick()
|
||||||
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
||||||
|
fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,12 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.appyx.canPop
|
import io.element.android.libraries.architecture.appyx.canPop
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
|
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
|
||||||
|
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -50,6 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
||||||
|
private val pushHistoryEntryPoint: PushHistoryEntryPoint,
|
||||||
private val logoutEntryPoint: LogoutEntryPoint,
|
private val logoutEntryPoint: LogoutEntryPoint,
|
||||||
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
||||||
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
|
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
|
||||||
|
|
@ -83,6 +87,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object TroubleshootNotifications : NavTarget
|
data object TroubleshootNotifications : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object PushHistory : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LockScreenSettings : NavTarget
|
data object LockScreenSettings : NavTarget
|
||||||
|
|
||||||
|
|
@ -182,6 +189,10 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
override fun onTroubleshootNotificationsClick() {
|
override fun onTroubleshootNotificationsClick() {
|
||||||
backstack.push(NavTarget.TroubleshootNotifications)
|
backstack.push(NavTarget.TroubleshootNotifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPushHistoryClick() {
|
||||||
|
backstack.push(NavTarget.PushHistory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
|
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +209,23 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
NavTarget.PushHistory -> {
|
||||||
|
pushHistoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(object : PushHistoryEntryPoint.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
if (backstack.canPop()) {
|
||||||
|
backstack.pop()
|
||||||
|
} else {
|
||||||
|
navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||||
|
plugins<PreferencesEntryPoint.Callback>().forEach { it.navigateTo(sessionId, roomId, eventId) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
is NavTarget.EditDefaultNotificationSetting -> {
|
is NavTarget.EditDefaultNotificationSetting -> {
|
||||||
val callback = object : EditDefaultNotificationSettingNode.Callback {
|
val callback = object : EditDefaultNotificationSettingNode.Callback {
|
||||||
override fun openRoomNotificationSettings(roomId: RoomId) {
|
override fun openRoomNotificationSettings(roomId: RoomId) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.preferences.impl.advanced
|
package io.element.android.features.preferences.impl.advanced
|
||||||
|
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
|
|
||||||
sealed interface AdvancedSettingsEvents {
|
sealed interface AdvancedSettingsEvents {
|
||||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
|
|
@ -16,4 +17,6 @@ sealed interface AdvancedSettingsEvents {
|
||||||
data object ChangeTheme : AdvancedSettingsEvents
|
data object ChangeTheme : AdvancedSettingsEvents
|
||||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||||
|
data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents
|
||||||
|
data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.runtime.setValue
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.compound.theme.mapToTheme
|
import io.element.android.compound.theme.mapToTheme
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -43,6 +44,14 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
}.collectAsState(initial = Theme.System)
|
}.collectAsState(initial = Theme.System)
|
||||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val hideInviteAvatars by remember {
|
||||||
|
appPreferencesStore.getHideInviteAvatarsFlow()
|
||||||
|
}.collectAsState(false)
|
||||||
|
|
||||||
|
val timelineMediaPreviewValue by remember {
|
||||||
|
appPreferencesStore.getTimelineMediaPreviewValueFlow()
|
||||||
|
}.collectAsState(initial = MediaPreviewValue.On)
|
||||||
|
|
||||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
||||||
|
|
@ -60,6 +69,12 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
appPreferencesStore.setTheme(event.theme.name)
|
appPreferencesStore.setTheme(event.theme.name)
|
||||||
showChangeThemeDialog = false
|
showChangeThemeDialog = false
|
||||||
}
|
}
|
||||||
|
is AdvancedSettingsEvents.SetHideInviteAvatars -> localCoroutineScope.launch {
|
||||||
|
appPreferencesStore.setHideInviteAvatars(event.value)
|
||||||
|
}
|
||||||
|
is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> localCoroutineScope.launch {
|
||||||
|
appPreferencesStore.setTimelineMediaPreviewValue(event.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +84,8 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
doesCompressMedia = doesCompressMedia,
|
doesCompressMedia = doesCompressMedia,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
|
timelineMediaPreviewValue = timelineMediaPreviewValue,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.preferences.impl.advanced
|
package io.element.android.features.preferences.impl.advanced
|
||||||
|
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
|
|
||||||
data class AdvancedSettingsState(
|
data class AdvancedSettingsState(
|
||||||
val isDeveloperModeEnabled: Boolean,
|
val isDeveloperModeEnabled: Boolean,
|
||||||
|
|
@ -15,5 +16,7 @@ data class AdvancedSettingsState(
|
||||||
val doesCompressMedia: Boolean,
|
val doesCompressMedia: Boolean,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
val showChangeThemeDialog: Boolean,
|
val showChangeThemeDialog: Boolean,
|
||||||
|
val hideInviteAvatars: Boolean,
|
||||||
|
val timelineMediaPreviewValue: MediaPreviewValue,
|
||||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.advanced
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
|
|
||||||
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
|
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
|
||||||
override val values: Sequence<AdvancedSettingsState>
|
override val values: Sequence<AdvancedSettingsState>
|
||||||
|
|
@ -18,6 +19,8 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
||||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||||
aAdvancedSettingsState(isSharePresenceEnabled = true),
|
aAdvancedSettingsState(isSharePresenceEnabled = true),
|
||||||
aAdvancedSettingsState(doesCompressMedia = true),
|
aAdvancedSettingsState(doesCompressMedia = true),
|
||||||
|
aAdvancedSettingsState(hideInviteAvatars = true),
|
||||||
|
aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +29,8 @@ fun aAdvancedSettingsState(
|
||||||
isSharePresenceEnabled: Boolean = false,
|
isSharePresenceEnabled: Boolean = false,
|
||||||
doesCompressMedia: Boolean = false,
|
doesCompressMedia: Boolean = false,
|
||||||
showChangeThemeDialog: Boolean = false,
|
showChangeThemeDialog: Boolean = false,
|
||||||
|
hideInviteAvatars: Boolean = false,
|
||||||
|
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
|
||||||
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
||||||
) = AdvancedSettingsState(
|
) = AdvancedSettingsState(
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
|
@ -33,5 +38,7 @@ fun aAdvancedSettingsState(
|
||||||
doesCompressMedia = doesCompressMedia,
|
doesCompressMedia = doesCompressMedia,
|
||||||
theme = Theme.System,
|
theme = Theme.System,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
|
timelineMediaPreviewValue = timelineMediaPreviewValue,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,22 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.compound.theme.themes
|
import io.element.android.compound.theme.themes
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ListOption
|
import io.element.android.libraries.designsystem.components.dialogs.ListOption
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
|
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingTextDefaults
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
|
|
@ -98,6 +106,7 @@ fun AdvancedSettingsView(
|
||||||
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
|
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ModerationAndSafety(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showChangeThemeDialog) {
|
if (state.showChangeThemeDialog) {
|
||||||
|
|
@ -116,6 +125,57 @@ fun AdvancedSettingsView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModerationAndSafety(
|
||||||
|
state: AdvancedSettingsState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
PreferenceCategory(
|
||||||
|
modifier = modifier,
|
||||||
|
title = stringResource(R.string.screen_advanced_settings_moderation_and_safety_section_title),
|
||||||
|
showTopDivider = true
|
||||||
|
) {
|
||||||
|
PreferenceSwitch(
|
||||||
|
title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title),
|
||||||
|
isChecked = state.hideInviteAvatars,
|
||||||
|
onCheckedChange = {
|
||||||
|
state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ListSectionHeader(
|
||||||
|
title = stringResource(R.string.screen_advanced_settings_show_media_timeline_title),
|
||||||
|
hasDivider = false,
|
||||||
|
description = {
|
||||||
|
ListSupportingText(
|
||||||
|
text = stringResource(R.string.screen_advanced_settings_show_media_timeline_subtitle),
|
||||||
|
contentPadding = ListSupportingTextDefaults.Padding.None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_hide)) },
|
||||||
|
leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.Off, compact = true),
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_private_rooms)) },
|
||||||
|
leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.Private, compact = true),
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_show)) },
|
||||||
|
leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.On, compact = true),
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getOptions(): ImmutableList<ListOption> {
|
private fun getOptions(): ImmutableList<ListOption> {
|
||||||
return themes.map {
|
return themes.map {
|
||||||
|
|
@ -134,9 +194,21 @@ private fun Theme.toHumanReadable(): String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
|
internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
|
||||||
ElementPreview {
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
AdvancedSettingsView(state = state, onBackClick = { })
|
|
||||||
}
|
@PreviewWithLargeHeight
|
||||||
|
@Composable
|
||||||
|
internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@ExcludeFromCoverage
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: AdvancedSettingsState) {
|
||||||
|
AdvancedSettingsView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
||||||
sealed interface DeveloperSettingsEvents {
|
sealed interface DeveloperSettingsEvents {
|
||||||
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
|
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
|
||||||
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
|
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
|
||||||
data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents
|
|
||||||
data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents
|
data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents
|
||||||
data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents
|
data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents
|
||||||
data object ClearCache : DeveloperSettingsEvents
|
data object ClearCache : DeveloperSettingsEvents
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,6 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
appPreferencesStore
|
appPreferencesStore
|
||||||
.getCustomElementCallBaseUrlFlow()
|
.getCustomElementCallBaseUrlFlow()
|
||||||
}.collectAsState(initial = null)
|
}.collectAsState(initial = null)
|
||||||
val hideImagesAndVideos by remember {
|
|
||||||
appPreferencesStore
|
|
||||||
.doesHideImagesAndVideosFlow()
|
|
||||||
}.collectAsState(initial = false)
|
|
||||||
|
|
||||||
val tracingLogLevelFlow = remember {
|
val tracingLogLevelFlow = remember {
|
||||||
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
|
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
|
||||||
|
|
@ -128,9 +124,6 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||||
}
|
}
|
||||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||||
is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch {
|
|
||||||
appPreferencesStore.setHideImagesAndVideos(event.value)
|
|
||||||
}
|
|
||||||
is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch {
|
is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch {
|
||||||
appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel())
|
appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel())
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +148,6 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
baseUrl = customElementCallBaseUrl,
|
baseUrl = customElementCallBaseUrl,
|
||||||
validator = ::customElementCallUrlValidator,
|
validator = ::customElementCallUrlValidator,
|
||||||
),
|
),
|
||||||
hideImagesAndVideos = hideImagesAndVideos,
|
|
||||||
tracingLogLevel = tracingLogLevel,
|
tracingLogLevel = tracingLogLevel,
|
||||||
tracingLogPacks = tracingLogPacks,
|
tracingLogPacks = tracingLogPacks,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ data class DeveloperSettingsState(
|
||||||
val rageshakeState: RageshakePreferencesState,
|
val rageshakeState: RageshakePreferencesState,
|
||||||
val clearCacheAction: AsyncAction<Unit>,
|
val clearCacheAction: AsyncAction<Unit>,
|
||||||
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
|
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
|
||||||
val hideImagesAndVideos: Boolean,
|
|
||||||
val tracingLogLevel: AsyncData<LogLevelItem>,
|
val tracingLogLevel: AsyncData<LogLevelItem>,
|
||||||
val tracingLogPacks: ImmutableList<TraceLogPack>,
|
val tracingLogPacks: ImmutableList<TraceLogPack>,
|
||||||
val eventSink: (DeveloperSettingsEvents) -> Unit
|
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
||||||
fun aDeveloperSettingsState(
|
fun aDeveloperSettingsState(
|
||||||
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
||||||
hideImagesAndVideos: Boolean = false,
|
|
||||||
traceLogPacks: List<TraceLogPack> = emptyList(),
|
traceLogPacks: List<TraceLogPack> = emptyList(),
|
||||||
eventSink: (DeveloperSettingsEvents) -> Unit = {},
|
eventSink: (DeveloperSettingsEvents) -> Unit = {},
|
||||||
) = DeveloperSettingsState(
|
) = DeveloperSettingsState(
|
||||||
|
|
@ -43,7 +42,6 @@ fun aDeveloperSettingsState(
|
||||||
cacheSize = AsyncData.Success("1.2 MB"),
|
cacheSize = AsyncData.Success("1.2 MB"),
|
||||||
clearCacheAction = clearCacheAction,
|
clearCacheAction = clearCacheAction,
|
||||||
customElementCallBaseUrlState = customElementCallBaseUrlState,
|
customElementCallBaseUrlState = customElementCallBaseUrlState,
|
||||||
hideImagesAndVideos = hideImagesAndVideos,
|
|
||||||
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
|
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
|
||||||
tracingLogPacks = traceLogPacks.toPersistentList(),
|
tracingLogPacks = traceLogPacks.toPersistentList(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ fun DeveloperSettingsView(
|
||||||
title = stringResource(id = CommonStrings.common_developer_options)
|
title = stringResource(id = CommonStrings.common_developer_options)
|
||||||
) {
|
) {
|
||||||
// Note: this is OK to hardcode strings in this debug screen.
|
// Note: this is OK to hardcode strings in this debug screen.
|
||||||
SettingsCategory(state)
|
|
||||||
PreferenceCategory(
|
PreferenceCategory(
|
||||||
title = "Feature flags",
|
title = "Feature flags",
|
||||||
showTopDivider = true,
|
showTopDivider = true,
|
||||||
|
|
@ -134,22 +133,6 @@ fun DeveloperSettingsView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsCategory(
|
|
||||||
state: DeveloperSettingsState,
|
|
||||||
) {
|
|
||||||
PreferenceCategory(title = "Preferences", showTopDivider = false) {
|
|
||||||
PreferenceSwitch(
|
|
||||||
title = "Hide image & video previews",
|
|
||||||
subtitle = "When toggled image & video will not render in the timeline by default.",
|
|
||||||
isChecked = state.hideImagesAndVideos,
|
|
||||||
onCheckedChange = {
|
|
||||||
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(it))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ElementCallCategory(
|
private fun ElementCallCategory(
|
||||||
state: DeveloperSettingsState,
|
state: DeveloperSettingsState,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class NotificationSettingsNode @AssistedInject constructor(
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun editDefaultNotificationMode(isOneToOne: Boolean)
|
fun editDefaultNotificationMode(isOneToOne: Boolean)
|
||||||
fun onTroubleshootNotificationsClick()
|
fun onTroubleshootNotificationsClick()
|
||||||
|
fun onPushHistoryClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
@ -39,6 +40,10 @@ class NotificationSettingsNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.onTroubleshootNotificationsClick() }
|
callbacks.forEach { it.onTroubleshootNotificationsClick() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onPushHistoryClick() {
|
||||||
|
callbacks.forEach { it.onPushHistoryClick() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -47,6 +52,7 @@ class NotificationSettingsNode @AssistedInject constructor(
|
||||||
onOpenEditDefault = { openEditDefault(isOneToOne = it) },
|
onOpenEditDefault = { openEditDefault(isOneToOne = it) },
|
||||||
onBackClick = ::navigateUp,
|
onBackClick = ::navigateUp,
|
||||||
onTroubleshootNotificationsClick = ::onTroubleshootNotificationsClick,
|
onTroubleshootNotificationsClick = ::onTroubleshootNotificationsClick,
|
||||||
|
onPushHistoryClick = ::onPushHistoryClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ fun NotificationSettingsView(
|
||||||
state: NotificationSettingsState,
|
state: NotificationSettingsState,
|
||||||
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
|
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
|
||||||
onTroubleshootNotificationsClick: () -> Unit,
|
onTroubleshootNotificationsClick: () -> Unit,
|
||||||
|
onPushHistoryClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -82,6 +83,7 @@ fun NotificationSettingsView(
|
||||||
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
|
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
|
||||||
onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
|
onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
|
||||||
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
||||||
|
onPushHistoryClick = onPushHistoryClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
|
|
@ -105,6 +107,7 @@ private fun NotificationSettingsContentView(
|
||||||
// onCallsNotificationsChanged: (Boolean) -> Unit,
|
// onCallsNotificationsChanged: (Boolean) -> Unit,
|
||||||
onInviteForMeNotificationsChange: (Boolean) -> Unit,
|
onInviteForMeNotificationsChange: (Boolean) -> Unit,
|
||||||
onTroubleshootNotificationsClick: () -> Unit,
|
onTroubleshootNotificationsClick: () -> Unit,
|
||||||
|
onPushHistoryClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val systemSettings: NotificationSettingsState.AppSettings = state.appSettings
|
val systemSettings: NotificationSettingsState.AppSettings = state.appSettings
|
||||||
|
|
@ -203,6 +206,12 @@ private fun NotificationSettingsContentView(
|
||||||
},
|
},
|
||||||
onClick = onTroubleshootNotificationsClick
|
onClick = onTroubleshootNotificationsClick
|
||||||
)
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(R.string.troubleshoot_notifications_entry_point_push_history_title))
|
||||||
|
},
|
||||||
|
onClick = onPushHistoryClick
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (state.showAdvancedSettings) {
|
if (state.showAdvancedSettings) {
|
||||||
PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) {
|
PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) {
|
||||||
|
|
@ -303,5 +312,6 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onOpenEditDefault = {},
|
onOpenEditDefault = {},
|
||||||
onTroubleshootNotificationsClick = {},
|
onTroubleshootNotificationsClick = {},
|
||||||
|
onPushHistoryClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@
|
||||||
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
|
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
|
||||||
<string name="screen_advanced_settings_share_presence">"Share presence"</string>
|
<string name="screen_advanced_settings_share_presence">"Share presence"</string>
|
||||||
<string name="screen_advanced_settings_share_presence_description">"If turned off, you won’t be able to send or receive read receipts or typing notifications."</string>
|
<string name="screen_advanced_settings_share_presence_description">"If turned off, you won’t be able to send or receive read receipts or typing notifications."</string>
|
||||||
|
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Always hide"</string>
|
||||||
|
<string name="screen_advanced_settings_show_media_timeline_always_show">"Always show"</string>
|
||||||
|
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"In private rooms"</string>
|
||||||
|
<string name="screen_advanced_settings_show_media_timeline_subtitle">"A hidden media can always be shown by tapping on it"</string>
|
||||||
|
<string name="screen_advanced_settings_show_media_timeline_title">"Show media in timeline"</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
||||||
<string name="screen_blocked_users_empty">"You have no blocked users"</string>
|
<string name="screen_blocked_users_empty">"You have no blocked users"</string>
|
||||||
<string name="screen_blocked_users_unblock_alert_action">"Unblock"</string>
|
<string name="screen_blocked_users_unblock_alert_action">"Unblock"</string>
|
||||||
|
|
@ -58,6 +63,7 @@ If you proceed, some of your settings may change."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
||||||
<string name="screen_notification_settings_title">"Notifications"</string>
|
<string name="screen_notification_settings_title">"Notifications"</string>
|
||||||
|
<string name="troubleshoot_notifications_entry_point_push_history_title">"Push history"</string>
|
||||||
<string name="troubleshoot_notifications_entry_point_section">"Troubleshoot"</string>
|
<string name="troubleshoot_notifications_entry_point_section">"Troubleshoot"</string>
|
||||||
<string name="troubleshoot_notifications_entry_point_title">"Troubleshoot notifications"</string>
|
<string name="troubleshoot_notifications_entry_point_title">"Troubleshoot notifications"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ class DeveloperSettingsPresenterTest {
|
||||||
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
||||||
assertThat(state.customElementCallBaseUrlState).isNotNull()
|
assertThat(state.customElementCallBaseUrlState).isNotNull()
|
||||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||||
assertThat(state.hideImagesAndVideos).isFalse()
|
|
||||||
assertThat(state.rageshakeState.isEnabled).isFalse()
|
assertThat(state.rageshakeState.isEnabled).isFalse()
|
||||||
assertThat(state.rageshakeState.isSupported).isTrue()
|
assertThat(state.rageshakeState.isSupported).isTrue()
|
||||||
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
|
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
|
||||||
|
|
@ -147,28 +146,6 @@ class DeveloperSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - toggling hide image and video`() = runTest {
|
|
||||||
val preferences = InMemoryAppPreferencesStore()
|
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(2)
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.hideImagesAndVideos).isFalse()
|
|
||||||
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.hideImagesAndVideos).isTrue()
|
|
||||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
|
|
||||||
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.hideImagesAndVideos).isFalse()
|
|
||||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - changing tracing log level`() = runTest {
|
fun `present - changing tracing log level`() = runTest {
|
||||||
val preferences = InMemoryAppPreferencesStore()
|
val preferences = InMemoryAppPreferencesStore()
|
||||||
|
|
|
||||||
|
|
@ -109,18 +109,6 @@ class DeveloperSettingsViewTest {
|
||||||
rule.onNodeWithText("Clear cache").performClick()
|
rule.onNodeWithText("Clear cache").performClick()
|
||||||
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
|
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on the hide images and videos switch emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
|
|
||||||
rule.setDeveloperSettingsView(
|
|
||||||
state = aDeveloperSettingsState(
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
),
|
|
||||||
)
|
|
||||||
rule.onNodeWithText("Hide image & video previews").performClick()
|
|
||||||
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,22 @@ class NotificationSettingsViewTest {
|
||||||
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
|
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on push history notification invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setNotificationSettingsView(
|
||||||
|
state = aValidNotificationSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onPushHistoryClick = it
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title)
|
||||||
|
}
|
||||||
|
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on group chats invokes the expected callback`() {
|
fun `clicking on group chats invokes the expected callback`() {
|
||||||
|
|
@ -284,6 +300,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotif
|
||||||
state: NotificationSettingsState,
|
state: NotificationSettingsState,
|
||||||
onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(),
|
onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onPushHistoryClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
|
|
@ -291,6 +308,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotif
|
||||||
state = state,
|
state = state,
|
||||||
onOpenEditDefault = onOpenEditDefault,
|
onOpenEditDefault = onOpenEditDefault,
|
||||||
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
||||||
|
onPushHistoryClick = onPushHistoryClick,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,9 @@ class RoomListPresenter @Inject constructor(
|
||||||
|
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
|
val hideInvitesAvatar by remember {
|
||||||
|
appPreferencesStore.getHideInviteAvatarsFlow()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||||
|
|
||||||
|
|
@ -173,6 +176,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
hideInvitesAvatars = hideInvitesAvatar,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ data class RoomListState(
|
||||||
val contentState: RoomListContentState,
|
val contentState: RoomListContentState,
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
|
val hideInvitesAvatars: Boolean,
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = contentState is RoomListContentState.Rooms
|
val displayFilters = contentState is RoomListContentState.Rooms
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ internal fun aRoomListState(
|
||||||
contentState: RoomListContentState = aRoomsContentState(),
|
contentState: RoomListContentState = aRoomsContentState(),
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
|
hideInvitesAvatars: Boolean = false,
|
||||||
eventSink: (RoomListEvents) -> Unit = {}
|
eventSink: (RoomListEvents) -> Unit = {}
|
||||||
) = RoomListState(
|
) = RoomListState(
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
|
|
@ -75,6 +76,7 @@ internal fun aRoomListState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ fun RoomListView(
|
||||||
RoomListSearchView(
|
RoomListSearchView(
|
||||||
state = state.searchState,
|
state = state.searchState,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
|
hideInvitesAvatars = state.hideInvitesAvatars,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = onRoomClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
|
|
@ -134,6 +135,7 @@ private fun RoomListScaffold(
|
||||||
RoomListContentView(
|
RoomListContentView(
|
||||||
contentState = state.contentState,
|
contentState = state.contentState,
|
||||||
filtersState = state.filtersState,
|
filtersState = state.filtersState,
|
||||||
|
hideInvitesAvatars = state.hideInvitesAvatars,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
fun RoomListContentView(
|
fun RoomListContentView(
|
||||||
contentState: RoomListContentState,
|
contentState: RoomListContentState,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
|
|
@ -86,6 +87,7 @@ fun RoomListContentView(
|
||||||
is RoomListContentState.Rooms -> {
|
is RoomListContentState.Rooms -> {
|
||||||
RoomsView(
|
RoomsView(
|
||||||
state = contentState,
|
state = contentState,
|
||||||
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
|
|
@ -156,6 +158,7 @@ private fun EmptyView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomsView(
|
private fun RoomsView(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
|
hideInvitesAvatars: Boolean,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
|
|
@ -171,6 +174,7 @@ private fun RoomsView(
|
||||||
} else {
|
} else {
|
||||||
RoomsViewList(
|
RoomsViewList(
|
||||||
state = state,
|
state = state,
|
||||||
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
|
|
@ -183,6 +187,7 @@ private fun RoomsView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomsViewList(
|
private fun RoomsViewList(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
|
|
@ -240,6 +245,7 @@ private fun RoomsViewList(
|
||||||
) { index, room ->
|
) { index, room ->
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = room,
|
room = room,
|
||||||
|
hideInviteAvatars = hideInvitesAvatars,
|
||||||
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
||||||
state.seenRoomInvites.contains(room.roomId),
|
state.seenRoomInvites.contains(room.roomId),
|
||||||
onClick = onRoomClick,
|
onClick = onRoomClick,
|
||||||
|
|
@ -303,6 +309,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||||
filtersState = aRoomListFiltersState(
|
filtersState = aRoomListFiltersState(
|
||||||
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||||
),
|
),
|
||||||
|
hideInvitesAvatars = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
onSetUpRecoveryClick = {},
|
onSetUpRecoveryClick = {},
|
||||||
onConfirmRecoveryKeyClick = {},
|
onConfirmRecoveryKeyClick = {},
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ internal val minHeight = 84.dp
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomSummaryRow(
|
internal fun RoomSummaryRow(
|
||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
|
hideInviteAvatars: Boolean,
|
||||||
isInviteSeen: Boolean,
|
isInviteSeen: Boolean,
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
|
|
@ -81,6 +82,7 @@ internal fun RoomSummaryRow(
|
||||||
RoomSummaryDisplayType.INVITE -> {
|
RoomSummaryDisplayType.INVITE -> {
|
||||||
RoomSummaryScaffoldRow(
|
RoomSummaryScaffoldRow(
|
||||||
room = room,
|
room = room,
|
||||||
|
hideAvatarImage = hideInviteAvatars,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
Timber.d("Long click on invite room")
|
Timber.d("Long click on invite room")
|
||||||
|
|
@ -93,6 +95,7 @@ internal fun RoomSummaryRow(
|
||||||
InviteSenderView(
|
InviteSenderView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
inviteSender = room.inviteSender,
|
inviteSender = room.inviteSender,
|
||||||
|
hideAvatarImage = hideInviteAvatars
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
@ -165,6 +168,7 @@ private fun RoomSummaryScaffoldRow(
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
hideAvatarImage: Boolean = false,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val clickModifier = Modifier.combinedClickable(
|
val clickModifier = Modifier.combinedClickable(
|
||||||
|
|
@ -185,6 +189,7 @@ private fun RoomSummaryScaffoldRow(
|
||||||
CompositeAvatar(
|
CompositeAvatar(
|
||||||
avatarData = room.avatarData,
|
avatarData = room.avatarData,
|
||||||
heroes = room.heroes,
|
heroes = room.heroes,
|
||||||
|
hideAvatarImages = hideAvatarImage,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -388,6 +393,7 @@ private fun MentionIndicatorAtom() {
|
||||||
internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
|
internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = data,
|
room = data,
|
||||||
|
hideInviteAvatars = false,
|
||||||
// Set isInviteSeen to true for the preview when the room has name "Bob"
|
// Set isInviteSeen to true for the preview when the room has name "Bob"
|
||||||
isInviteSeen = data.name == "Bob",
|
isInviteSeen = data.name == "Bob",
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchView(
|
internal fun RoomListSearchView(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onRoomClick: (RoomId) -> Unit,
|
onRoomClick: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -80,6 +81,7 @@ internal fun RoomListSearchView(
|
||||||
if (state.isSearchActive) {
|
if (state.isSearchActive) {
|
||||||
RoomListSearchContent(
|
RoomListSearchContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = onRoomClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
@ -92,6 +94,7 @@ internal fun RoomListSearchView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListSearchContent(
|
private fun RoomListSearchContent(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onRoomClick: (RoomId) -> Unit,
|
onRoomClick: (RoomId) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -173,6 +176,7 @@ private fun RoomListSearchContent(
|
||||||
) { room ->
|
) { room ->
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = room,
|
room = room,
|
||||||
|
hideInviteAvatars = hideInvitesAvatars,
|
||||||
// TODO
|
// TODO
|
||||||
isInviteSeen = false,
|
isInviteSeen = false,
|
||||||
onClick = ::onRoomClick,
|
onClick = ::onRoomClick,
|
||||||
|
|
@ -189,6 +193,7 @@ private fun RoomListSearchContent(
|
||||||
internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
|
internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
|
||||||
RoomListSearchContent(
|
RoomListSearchContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
hideInvitesAvatars = false,
|
||||||
onRoomClick = {},
|
onRoomClick = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
android_gradle_plugin = "8.9.1"
|
android_gradle_plugin = "8.9.1"
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.20"
|
||||||
kotlinpoet = "2.1.0"
|
kotlinpoet = "2.1.0"
|
||||||
ksp = "2.1.20-1.0.32"
|
ksp = "2.1.20-2.0.0"
|
||||||
firebaseAppDistribution = "5.1.1"
|
firebaseAppDistribution = "5.1.1"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
|
|
@ -29,7 +29,7 @@ compose_bom = "2025.03.01"
|
||||||
composecompiler = "1.5.15"
|
composecompiler = "1.5.15"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.10.1"
|
coroutines = "1.10.2"
|
||||||
|
|
||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist = "0.37.2"
|
accompanist = "0.37.2"
|
||||||
|
|
@ -50,7 +50,7 @@ wysiwyg = "2.38.3"
|
||||||
telephoto = "0.15.1"
|
telephoto = "0.15.1"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "2.14.0"
|
dependencyAnalysis = "2.16.0"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.56.1"
|
dagger = "2.56.1"
|
||||||
|
|
@ -149,7 +149,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
|
||||||
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||||
test_junit = "junit:junit:4.13.2"
|
test_junit = "junit:junit:4.13.2"
|
||||||
test_runner = "androidx.test:runner:1.6.2"
|
test_runner = "androidx.test:runner:1.6.2"
|
||||||
test_mockk = "io.mockk:mockk:1.13.17"
|
test_mockk = "io.mockk:mockk:1.14.0"
|
||||||
test_konsist = "com.lemonappdev:konsist:0.17.3"
|
test_konsist = "com.lemonappdev:konsist:0.17.3"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
||||||
test_truth = "com.google.truth:truth:1.4.4"
|
test_truth = "com.google.truth:truth:1.4.4"
|
||||||
|
|
@ -188,7 +188,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:11.8.5"
|
maplibre = "org.maplibre.gl:android-sdk:11.8.6"
|
||||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,13 @@ fun Avatar(
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
// If not null, will be used instead of the size from avatarData
|
// If not null, will be used instead of the size from avatarData
|
||||||
forcedAvatarSize: Dp? = null,
|
forcedAvatarSize: Dp? = null,
|
||||||
|
// If true, will show initials even if avatarData.url is not null
|
||||||
|
hideImage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val commonModifier = modifier
|
val commonModifier = modifier
|
||||||
.size(forcedAvatarSize ?: avatarData.size.dp)
|
.size(forcedAvatarSize ?: avatarData.size.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
if (avatarData.url.isNullOrBlank()) {
|
if (avatarData.url.isNullOrBlank() || hideImage) {
|
||||||
InitialsAvatar(
|
InitialsAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,16 @@ fun CompositeAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
hideAvatarImages: Boolean = false,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
if (avatarData.url != null || heroes.isEmpty()) {
|
if (avatarData.url != null || heroes.isEmpty()) {
|
||||||
Avatar(avatarData, modifier, contentDescription)
|
Avatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
modifier = modifier,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
hideImage = hideAvatarImages
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val limitedHeroes = heroes.take(4)
|
val limitedHeroes = heroes.take(4)
|
||||||
val numberOfHeroes = limitedHeroes.size
|
val numberOfHeroes = limitedHeroes.size
|
||||||
|
|
@ -49,7 +55,12 @@ fun CompositeAvatar(
|
||||||
error("Unsupported number of heroes: 0")
|
error("Unsupported number of heroes: 0")
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
Avatar(heroes[0], modifier, contentDescription)
|
Avatar(
|
||||||
|
avatarData = heroes[0],
|
||||||
|
modifier = modifier,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
hideImage = hideAvatarImages
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val angle = 2 * Math.PI / numberOfHeroes
|
val angle = 2 * Math.PI / numberOfHeroes
|
||||||
|
|
@ -91,8 +102,9 @@ fun CompositeAvatar(
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
heroAvatar,
|
avatarData = heroAvatar,
|
||||||
forcedAvatarSize = heroAvatarSize,
|
forcedAvatarSize = heroAvatarSize,
|
||||||
|
hideImage = hideAvatarImages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.media
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Off
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.On
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Private
|
||||||
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the values for media preview settings.
|
||||||
|
* - [On] means that media preview are enabled
|
||||||
|
* - [Off] means that media preview are disabled
|
||||||
|
* - [Private] means that media preview are enabled only for private chats.
|
||||||
|
*/
|
||||||
|
enum class MediaPreviewValue {
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Private
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaPreviewValue.isPreviewEnabled(joinRule: JoinRule?): Boolean {
|
||||||
|
return when (this) {
|
||||||
|
On -> true
|
||||||
|
Off -> false
|
||||||
|
Private -> when (joinRule) {
|
||||||
|
is JoinRule.Knock,
|
||||||
|
is JoinRule.Invite,
|
||||||
|
is JoinRule.Restricted,
|
||||||
|
is JoinRule.KnockRestricted -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,3 +87,4 @@ const val A_RECOVERY_KEY = "1234 5678"
|
||||||
val A_SERVER_LIST = listOf("server1", "server2")
|
val A_SERVER_LIST = listOf("server1", "server2")
|
||||||
|
|
||||||
const val A_TIMESTAMP = 567L
|
const val A_TIMESTAMP = 567L
|
||||||
|
const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,15 @@ import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
@Composable
|
@Composable
|
||||||
fun InviteSenderView(
|
fun InviteSenderView(
|
||||||
inviteSender: InviteSender,
|
inviteSender: InviteSender,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
hideAvatarImage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.padding(vertical = 2.dp)) {
|
Box(modifier = Modifier.padding(vertical = 2.dp)) {
|
||||||
Avatar(avatarData = inviteSender.avatarData)
|
Avatar(avatarData = inviteSender.avatarData, hideImage = hideAvatarImage)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = inviteSender.annotatedString(),
|
text = inviteSender.annotatedString(),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.preferences.api.store
|
package io.element.android.libraries.preferences.api.store
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||||
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -21,8 +22,11 @@ interface AppPreferencesStore {
|
||||||
suspend fun setTheme(theme: String)
|
suspend fun setTheme(theme: String)
|
||||||
fun getThemeFlow(): Flow<String?>
|
fun getThemeFlow(): Flow<String?>
|
||||||
|
|
||||||
suspend fun setHideImagesAndVideos(value: Boolean)
|
suspend fun setHideInviteAvatars(value: Boolean)
|
||||||
fun doesHideImagesAndVideosFlow(): Flow<Boolean>
|
fun getHideInviteAvatarsFlow(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue)
|
||||||
|
fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue>
|
||||||
|
|
||||||
suspend fun setTracingLogLevel(logLevel: LogLevel)
|
suspend fun setTracingLogLevel(logLevel: LogLevel)
|
||||||
fun getTracingLogLevelFlow(): Flow<LogLevel>
|
fun getTracingLogLevelFlow(): Flow<LogLevel>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||||
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
|
@ -31,7 +32,8 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
||||||
private val developerModeKey = booleanPreferencesKey("developerMode")
|
private val developerModeKey = booleanPreferencesKey("developerMode")
|
||||||
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
||||||
private val themeKey = stringPreferencesKey("theme")
|
private val themeKey = stringPreferencesKey("theme")
|
||||||
private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos")
|
private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
|
||||||
|
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
|
||||||
private val logLevelKey = stringPreferencesKey("logLevel")
|
private val logLevelKey = stringPreferencesKey("logLevel")
|
||||||
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
|
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
|
||||||
|
|
||||||
|
|
@ -83,15 +85,27 @@ class DefaultAppPreferencesStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setHideImagesAndVideos(value: Boolean) {
|
override suspend fun setHideInviteAvatars(value: Boolean) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
prefs[hideImagesAndVideosKey] = value
|
prefs[hideInviteAvatarsKey] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doesHideImagesAndVideosFlow(): Flow<Boolean> {
|
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
|
||||||
return store.data.map { prefs ->
|
return store.data.map { prefs ->
|
||||||
prefs[hideImagesAndVideosKey] ?: false
|
prefs[hideInviteAvatarsKey] == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[timelineMediaPreviewValueKey] = value.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue> {
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) } ?: MediaPreviewValue.On
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.preferences.test
|
package io.element.android.libraries.preferences.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||||
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
|
@ -15,18 +16,20 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class InMemoryAppPreferencesStore(
|
class InMemoryAppPreferencesStore(
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
hideImagesAndVideos: Boolean = false,
|
|
||||||
customElementCallBaseUrl: String? = null,
|
customElementCallBaseUrl: String? = null,
|
||||||
|
hideInviteAvatars: Boolean = false,
|
||||||
|
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
|
||||||
theme: String? = null,
|
theme: String? = null,
|
||||||
logLevel: LogLevel = LogLevel.INFO,
|
logLevel: LogLevel = LogLevel.INFO,
|
||||||
traceLockPacks: Set<TraceLogPack> = emptySet(),
|
traceLockPacks: Set<TraceLogPack> = emptySet(),
|
||||||
) : AppPreferencesStore {
|
) : AppPreferencesStore {
|
||||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||||
private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos)
|
|
||||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||||
private val theme = MutableStateFlow(theme)
|
private val theme = MutableStateFlow(theme)
|
||||||
private val logLevel = MutableStateFlow(logLevel)
|
private val logLevel = MutableStateFlow(logLevel)
|
||||||
private val tracingLogPacks = MutableStateFlow(traceLockPacks)
|
private val tracingLogPacks = MutableStateFlow(traceLockPacks)
|
||||||
|
private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars)
|
||||||
|
private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue)
|
||||||
|
|
||||||
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
|
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
|
||||||
isDeveloperModeEnabled.value = enabled
|
isDeveloperModeEnabled.value = enabled
|
||||||
|
|
@ -52,12 +55,20 @@ class InMemoryAppPreferencesStore(
|
||||||
return theme
|
return theme
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setHideImagesAndVideos(value: Boolean) {
|
override suspend fun setHideInviteAvatars(value: Boolean) {
|
||||||
hideImagesAndVideos.value = value
|
hideInviteAvatars.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doesHideImagesAndVideosFlow(): Flow<Boolean> {
|
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
|
||||||
return hideImagesAndVideos
|
return hideInviteAvatars
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
|
||||||
|
timelineMediaPreviewValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue> {
|
||||||
|
return timelineMediaPreviewValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setTracingLogLevel(logLevel: LogLevel) {
|
override suspend fun setTracingLogLevel(logLevel: LogLevel) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.push.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
import io.element.android.libraries.pushproviders.api.Distributor
|
import io.element.android.libraries.pushproviders.api.Distributor
|
||||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -51,4 +52,19 @@ interface PushService {
|
||||||
* Return false in case of early error.
|
* Return false in case of early error.
|
||||||
*/
|
*/
|
||||||
suspend fun testPush(): Boolean
|
suspend fun testPush(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a flow of total number of received Push.
|
||||||
|
*/
|
||||||
|
val pushCounter: Flow<Int>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a flow of list of [PushHistoryItem].
|
||||||
|
*/
|
||||||
|
fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the push history, including the push counter.
|
||||||
|
*/
|
||||||
|
suspend fun resetPushHistory()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.api.history
|
||||||
|
|
||||||
|
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.SessionId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class representing a push history item.
|
||||||
|
* @property pushDate Date (timestamp).
|
||||||
|
* @property formattedDate Formatted date.
|
||||||
|
* @property providerInfo Push provider name / info
|
||||||
|
* @property eventId EventId from the push, can be null if the received data are not correct.
|
||||||
|
* @property roomId RoomId from the push, can be null if the received data are not correct.
|
||||||
|
* @property sessionId The session Id, can be null if the session cannot be retrieved
|
||||||
|
* @property hasBeenResolved Result of resolving the event
|
||||||
|
* @property comment Comment. Can contains an error message if the event could not be resolved, or other any information.
|
||||||
|
*/
|
||||||
|
data class PushHistoryItem(
|
||||||
|
val pushDate: Long,
|
||||||
|
val formattedDate: String,
|
||||||
|
val providerInfo: String,
|
||||||
|
val eventId: EventId?,
|
||||||
|
val roomId: RoomId?,
|
||||||
|
val sessionId: SessionId?,
|
||||||
|
val hasBeenResolved: Boolean,
|
||||||
|
val comment: String?,
|
||||||
|
)
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.api.store
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface PushDataStore {
|
|
||||||
val pushCounterFlow: Flow<Int>
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,7 @@ import extension.setupAnvil
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.sqldelight)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -32,9 +33,16 @@ dependencies {
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
|
||||||
|
implementation(libs.sqldelight.driver.android)
|
||||||
|
implementation(libs.sqlcipher)
|
||||||
|
implementation(libs.sqlite)
|
||||||
|
implementation(libs.sqldelight.coroutines)
|
||||||
|
implementation(projects.libraries.encryptedDb)
|
||||||
|
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.dateformatter.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
|
|
@ -76,3 +84,11 @@ dependencies {
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(libs.kotlinx.collections.immutable)
|
testImplementation(libs.kotlinx.collections.immutable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqldelight {
|
||||||
|
databases {
|
||||||
|
create("PushDatabase") {
|
||||||
|
schemaOutputDirectory = File("src/main/sqldelight/databases")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||||
import io.element.android.libraries.push.impl.test.TestPush
|
import io.element.android.libraries.push.impl.test.TestPush
|
||||||
import io.element.android.libraries.pushproviders.api.Distributor
|
import io.element.android.libraries.pushproviders.api.Distributor
|
||||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||||
|
|
@ -34,6 +36,7 @@ class DefaultPushService @Inject constructor(
|
||||||
private val getCurrentPushProvider: GetCurrentPushProvider,
|
private val getCurrentPushProvider: GetCurrentPushProvider,
|
||||||
private val sessionObserver: SessionObserver,
|
private val sessionObserver: SessionObserver,
|
||||||
private val pushClientSecretStore: PushClientSecretStore,
|
private val pushClientSecretStore: PushClientSecretStore,
|
||||||
|
private val pushDataStore: PushDataStore,
|
||||||
) : PushService, SessionListener {
|
) : PushService, SessionListener {
|
||||||
init {
|
init {
|
||||||
observeSessions()
|
observeSessions()
|
||||||
|
|
@ -125,4 +128,14 @@ class DefaultPushService @Inject constructor(
|
||||||
pushClientSecretStore.resetSecret(sessionId)
|
pushClientSecretStore.resetSecret(sessionId)
|
||||||
userPushStore.reset()
|
userPushStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val pushCounter: Flow<Int> = pushDataStore.pushCounterFlow
|
||||||
|
|
||||||
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
|
return pushDataStore.getPushHistoryItemsFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resetPushHistory() {
|
||||||
|
pushDataStore.reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.history
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.impl.PushDatabase
|
||||||
|
import io.element.android.libraries.push.impl.db.PushHistory
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPushHistoryService @Inject constructor(
|
||||||
|
private val pushDatabase: PushDatabase,
|
||||||
|
private val systemClock: SystemClock,
|
||||||
|
) : PushHistoryService {
|
||||||
|
override fun onPushReceived(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId?,
|
||||||
|
roomId: RoomId?,
|
||||||
|
sessionId: SessionId?,
|
||||||
|
hasBeenResolved: Boolean,
|
||||||
|
comment: String?,
|
||||||
|
) {
|
||||||
|
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||||
|
PushHistory(
|
||||||
|
pushDate = systemClock.epochMillis(),
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = eventId?.value,
|
||||||
|
roomId = roomId?.value,
|
||||||
|
sessionId = sessionId?.value,
|
||||||
|
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||||
|
comment = comment,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep only the last 100 events
|
||||||
|
pushDatabase.pushHistoryQueries.removeOldest(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.history
|
||||||
|
|
||||||
|
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.SessionId
|
||||||
|
|
||||||
|
interface PushHistoryService {
|
||||||
|
/**
|
||||||
|
* Create a new push history entry.
|
||||||
|
* Do not use directly, prefer using the extension functions.
|
||||||
|
*/
|
||||||
|
fun onPushReceived(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId?,
|
||||||
|
roomId: RoomId?,
|
||||||
|
sessionId: SessionId?,
|
||||||
|
hasBeenResolved: Boolean,
|
||||||
|
comment: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PushHistoryService.onInvalidPushReceived(
|
||||||
|
providerInfo: String,
|
||||||
|
) = onPushReceived(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = null,
|
||||||
|
roomId = null,
|
||||||
|
sessionId = null,
|
||||||
|
hasBeenResolved = false,
|
||||||
|
comment = "Invalid push data",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PushHistoryService.onUnableToRetrieveSession(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId,
|
||||||
|
roomId: RoomId,
|
||||||
|
reason: String,
|
||||||
|
) = onPushReceived(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = eventId,
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = null,
|
||||||
|
hasBeenResolved = false,
|
||||||
|
comment = "Unable to retrieve session: $reason",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PushHistoryService.onUnableToResolveEvent(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId,
|
||||||
|
roomId: RoomId,
|
||||||
|
sessionId: SessionId,
|
||||||
|
reason: String,
|
||||||
|
) = onPushReceived(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = eventId,
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
hasBeenResolved = false,
|
||||||
|
comment = "Unable to resolve event: $reason",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PushHistoryService.onSuccess(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId,
|
||||||
|
roomId: RoomId,
|
||||||
|
sessionId: SessionId,
|
||||||
|
comment: String?,
|
||||||
|
) = onPushReceived(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = eventId,
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
hasBeenResolved = true,
|
||||||
|
comment = buildString {
|
||||||
|
append("Success")
|
||||||
|
if (comment.isNullOrBlank().not()) {
|
||||||
|
append(" - $comment")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PushHistoryService.onDiagnosticPush(
|
||||||
|
providerInfo: String,
|
||||||
|
) = onPushReceived(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = null,
|
||||||
|
roomId = null,
|
||||||
|
sessionId = null,
|
||||||
|
hasBeenResolved = true,
|
||||||
|
comment = "Diagnostic push",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.history.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.push.impl.PushDatabase
|
||||||
|
import io.element.encrypteddb.SqlCipherDriverFactory
|
||||||
|
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
object PushHistoryModule {
|
||||||
|
@Provides
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
fun providePushDatabase(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): PushDatabase {
|
||||||
|
val name = "push_database"
|
||||||
|
val secretFile = context.getDatabasePath("$name.key")
|
||||||
|
|
||||||
|
// Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions
|
||||||
|
val parentDir = secretFile.parentFile
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
parentDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||||
|
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||||
|
.create(PushDatabase.Schema, "$name.db", context)
|
||||||
|
return PushDatabase(driver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,16 +30,26 @@ interface CallNotificationEventResolver {
|
||||||
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
|
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
|
||||||
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
||||||
*/
|
*/
|
||||||
fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent?
|
fun resolveEvent(
|
||||||
|
sessionId: SessionId,
|
||||||
|
notificationData: NotificationData,
|
||||||
|
forceNotify: Boolean = false,
|
||||||
|
): Result<NotifiableEvent>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
) : CallNotificationEventResolver {
|
) : CallNotificationEventResolver {
|
||||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
override fun resolveEvent(
|
||||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null
|
sessionId: SessionId,
|
||||||
return notificationData.run {
|
notificationData: NotificationData,
|
||||||
|
forceNotify: Boolean
|
||||||
|
): Result<NotifiableEvent> = runCatching {
|
||||||
|
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||||
|
?: throw ResolvingException("content is not a call notify")
|
||||||
|
|
||||||
|
notificationData.run {
|
||||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||||
NotifiableRingingCallEvent(
|
NotifiableRingingCallEvent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
|
@ -21,6 +22,7 @@ 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.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
|
@ -59,7 +61,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
|
||||||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||||
*/
|
*/
|
||||||
interface NotifiableEventResolver {
|
interface NotifiableEventResolver {
|
||||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
|
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
|
|
@ -73,31 +75,39 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : NotifiableEventResolver {
|
) : NotifiableEventResolver {
|
||||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||||
// Restore session
|
// Restore session
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure(
|
||||||
|
ResolvingException("Unable to restore session for $sessionId")
|
||||||
|
)
|
||||||
val notificationService = client.notificationService()
|
val notificationService = client.notificationService()
|
||||||
val notificationData = notificationService.getNotification(
|
val notificationData = notificationService.getNotification(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
).onFailure {
|
).onFailure {
|
||||||
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
|
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
|
||||||
}.getOrNull()
|
}
|
||||||
|
|
||||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||||
return notificationData?.asNotifiableEvent(client, sessionId)
|
return notificationData.flatMap {
|
||||||
|
if (it == null) {
|
||||||
|
Timber.tag(loggerTag.value).d("No notification data found for event $eventId")
|
||||||
|
return@flatMap Result.failure(ResolvingException("Unable to resolve event"))
|
||||||
|
} else {
|
||||||
|
it.asNotifiableEvent(client, sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun NotificationData.asNotifiableEvent(
|
private suspend fun NotificationData.asNotifiableEvent(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
userId: SessionId,
|
userId: SessionId,
|
||||||
): ResolvedPushEvent? {
|
): Result<ResolvedPushEvent> = runCatching {
|
||||||
val content = this.content
|
when (val content = this.content) {
|
||||||
val notifiableEvent = when (content) {
|
|
||||||
is NotificationContent.MessageLike.RoomMessage -> {
|
is NotificationContent.MessageLike.RoomMessage -> {
|
||||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||||
buildNotifiableMessageEvent(
|
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||||
sessionId = userId,
|
sessionId = userId,
|
||||||
senderId = content.senderId,
|
senderId = content.senderId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
|
@ -115,10 +125,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
senderAvatarPath = senderAvatarUrl,
|
senderAvatarPath = senderAvatarUrl,
|
||||||
hasMentionOrReply = hasMention,
|
hasMentionOrReply = hasMention,
|
||||||
)
|
)
|
||||||
|
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||||
}
|
}
|
||||||
is NotificationContent.Invite -> {
|
is NotificationContent.Invite -> {
|
||||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||||
InviteNotifiableEvent(
|
val inviteNotifiableEvent = InviteNotifiableEvent(
|
||||||
sessionId = userId,
|
sessionId = userId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
|
|
@ -136,15 +147,16 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
// TODO check if title is needed anymore
|
// TODO check if title is needed anymore
|
||||||
title = null,
|
title = null,
|
||||||
)
|
)
|
||||||
|
ResolvedPushEvent.Event(inviteNotifiableEvent)
|
||||||
}
|
}
|
||||||
NotificationContent.MessageLike.CallAnswer,
|
NotificationContent.MessageLike.CallAnswer,
|
||||||
NotificationContent.MessageLike.CallCandidates,
|
NotificationContent.MessageLike.CallCandidates,
|
||||||
NotificationContent.MessageLike.CallHangup -> {
|
NotificationContent.MessageLike.CallHangup -> {
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||||
null
|
throw ResolvingException("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
is NotificationContent.MessageLike.CallInvite -> {
|
is NotificationContent.MessageLike.CallInvite -> {
|
||||||
buildNotifiableMessageEvent(
|
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||||
sessionId = userId,
|
sessionId = userId,
|
||||||
senderId = content.senderId,
|
senderId = content.senderId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
|
@ -158,9 +170,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
roomAvatarPath = roomAvatarUrl,
|
roomAvatarPath = roomAvatarUrl,
|
||||||
senderAvatarPath = senderAvatarUrl,
|
senderAvatarPath = senderAvatarUrl,
|
||||||
)
|
)
|
||||||
|
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||||
}
|
}
|
||||||
is NotificationContent.MessageLike.CallNotify -> {
|
is NotificationContent.MessageLike.CallNotify -> {
|
||||||
callNotificationEventResolver.resolveEvent(userId, this)
|
val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow()
|
||||||
|
ResolvedPushEvent.Event(notifiableEvent)
|
||||||
}
|
}
|
||||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||||
|
|
@ -168,11 +182,12 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
NotificationContent.MessageLike.KeyVerificationKey,
|
NotificationContent.MessageLike.KeyVerificationKey,
|
||||||
NotificationContent.MessageLike.KeyVerificationMac,
|
NotificationContent.MessageLike.KeyVerificationMac,
|
||||||
NotificationContent.MessageLike.KeyVerificationReady,
|
NotificationContent.MessageLike.KeyVerificationReady,
|
||||||
NotificationContent.MessageLike.KeyVerificationStart -> null.also {
|
NotificationContent.MessageLike.KeyVerificationStart -> {
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
|
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||||
|
throw ResolvingException("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
is NotificationContent.MessageLike.Poll -> {
|
is NotificationContent.MessageLike.Poll -> {
|
||||||
buildNotifiableMessageEvent(
|
val notifiableEventMessage = buildNotifiableMessageEvent(
|
||||||
sessionId = userId,
|
sessionId = userId,
|
||||||
senderId = content.senderId,
|
senderId = content.senderId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
|
@ -187,19 +202,35 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
roomAvatarPath = roomAvatarUrl,
|
roomAvatarPath = roomAvatarUrl,
|
||||||
senderAvatarPath = senderAvatarUrl,
|
senderAvatarPath = senderAvatarUrl,
|
||||||
)
|
)
|
||||||
|
ResolvedPushEvent.Event(notifiableEventMessage)
|
||||||
}
|
}
|
||||||
is NotificationContent.MessageLike.ReactionContent -> null.also {
|
is NotificationContent.MessageLike.ReactionContent -> {
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
|
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
|
||||||
|
throw ResolvingException("Ignoring notification for reaction")
|
||||||
}
|
}
|
||||||
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
|
NotificationContent.MessageLike.RoomEncrypted -> {
|
||||||
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
||||||
|
val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId)
|
||||||
|
ResolvedPushEvent.Event(fallbackNotifiableEvent)
|
||||||
}
|
}
|
||||||
is NotificationContent.MessageLike.RoomRedaction -> {
|
is NotificationContent.MessageLike.RoomRedaction -> {
|
||||||
// Note: this case will be handled below
|
// Note: this case will be handled below
|
||||||
null
|
val redactedEventId = content.redactedEventId
|
||||||
|
if (redactedEventId == null) {
|
||||||
|
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||||
|
throw ResolvingException("redactedEventId is null")
|
||||||
|
} else {
|
||||||
|
ResolvedPushEvent.Redaction(
|
||||||
|
sessionId = userId,
|
||||||
|
roomId = roomId,
|
||||||
|
redactedEventId = redactedEventId,
|
||||||
|
reason = content.reason,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NotificationContent.MessageLike.Sticker -> null.also {
|
NotificationContent.MessageLike.Sticker -> {
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
||||||
|
throw ResolvingException("Ignoring notification for reaction")
|
||||||
}
|
}
|
||||||
is NotificationContent.StateEvent.RoomMemberContent,
|
is NotificationContent.StateEvent.RoomMemberContent,
|
||||||
NotificationContent.StateEvent.PolicyRuleRoom,
|
NotificationContent.StateEvent.PolicyRuleRoom,
|
||||||
|
|
@ -221,29 +252,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
NotificationContent.StateEvent.RoomTombstone,
|
NotificationContent.StateEvent.RoomTombstone,
|
||||||
NotificationContent.StateEvent.RoomTopic,
|
NotificationContent.StateEvent.RoomTopic,
|
||||||
NotificationContent.StateEvent.SpaceChild,
|
NotificationContent.StateEvent.SpaceChild,
|
||||||
NotificationContent.StateEvent.SpaceParent -> null.also {
|
NotificationContent.StateEvent.SpaceParent -> {
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
|
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||||
|
throw ResolvingException("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (notifiableEvent != null) {
|
|
||||||
ResolvedPushEvent.Event(notifiableEvent)
|
|
||||||
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
|
|
||||||
val redactedEventId = content.redactedEventId
|
|
||||||
if (redactedEventId == null) {
|
|
||||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ResolvedPushEvent.Redaction(
|
|
||||||
sessionId = userId,
|
|
||||||
roomId = roomId,
|
|
||||||
redactedEventId = redactedEventId,
|
|
||||||
reason = content.reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fallbackNotifiableEvent(
|
private fun fallbackNotifiableEvent(
|
||||||
|
|
@ -293,7 +306,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? {
|
private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? {
|
||||||
if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) {
|
if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val fileResult = when (val messageType = messageType) {
|
val fileResult = when (val messageType = messageType) {
|
||||||
|
|
@ -320,7 +333,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? {
|
private suspend fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? {
|
||||||
if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) {
|
if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return when (val messageType = messageType) {
|
return when (val messageType = messageType) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
||||||
notificationData = notificationData,
|
notificationData = notificationData,
|
||||||
// Make sure the notifiable event is not a ringing one
|
// Make sure the notifiable event is not a ringing one
|
||||||
forceNotify = true,
|
forceNotify = true,
|
||||||
)
|
).getOrNull()
|
||||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
class ResolvingException(message: String) : Exception(message)
|
||||||
|
|
@ -14,6 +14,12 @@ import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||||
|
import io.element.android.libraries.push.impl.history.onDiagnosticPush
|
||||||
|
import io.element.android.libraries.push.impl.history.onInvalidPushReceived
|
||||||
|
import io.element.android.libraries.push.impl.history.onSuccess
|
||||||
|
import io.element.android.libraries.push.impl.history.onUnableToResolveEvent
|
||||||
|
import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession
|
||||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||||
|
|
@ -43,13 +49,15 @@ class DefaultPushHandler @Inject constructor(
|
||||||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val notificationChannels: NotificationChannels,
|
private val notificationChannels: NotificationChannels,
|
||||||
|
private val pushHistoryService: PushHistoryService,
|
||||||
) : PushHandler {
|
) : PushHandler {
|
||||||
/**
|
/**
|
||||||
* Called when message is received.
|
* Called when message is received.
|
||||||
*
|
*
|
||||||
* @param pushData the data received in the push.
|
* @param pushData the data received in the push.
|
||||||
|
* @param providerInfo the provider info.
|
||||||
*/
|
*/
|
||||||
override suspend fun handle(pushData: PushData) {
|
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||||
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
|
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
|
||||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||||
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
||||||
|
|
@ -57,18 +65,25 @@ class DefaultPushHandler @Inject constructor(
|
||||||
incrementPushDataStore.incrementPushCounter()
|
incrementPushDataStore.incrementPushCounter()
|
||||||
// Diagnostic Push
|
// Diagnostic Push
|
||||||
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
||||||
|
pushHistoryService.onDiagnosticPush(providerInfo)
|
||||||
diagnosticPushHandler.handlePush()
|
diagnosticPushHandler.handlePush()
|
||||||
} else {
|
} else {
|
||||||
handleInternal(pushData)
|
handleInternal(pushData, providerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun handleInvalid(providerInfo: String) {
|
||||||
|
incrementPushDataStore.incrementPushCounter()
|
||||||
|
pushHistoryService.onInvalidPushReceived(providerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal receive method.
|
* Internal receive method.
|
||||||
*
|
*
|
||||||
* @param pushData Object containing message data.
|
* @param pushData Object containing message data.
|
||||||
|
* @param providerInfo the provider info.
|
||||||
*/
|
*/
|
||||||
private suspend fun handleInternal(pushData: PushData) {
|
private suspend fun handleInternal(pushData: PushData, providerInfo: String) {
|
||||||
try {
|
try {
|
||||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||||
Timber.tag(loggerTag.value).d("## handleInternal() : $pushData")
|
Timber.tag(loggerTag.value).d("## handleInternal() : $pushData")
|
||||||
|
|
@ -77,42 +92,77 @@ class DefaultPushHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
val clientSecret = pushData.clientSecret
|
val clientSecret = pushData.clientSecret
|
||||||
// clientSecret should not be null. If this happens, restore default session
|
// clientSecret should not be null. If this happens, restore default session
|
||||||
val userId = clientSecret
|
var reason = if (clientSecret == null) "No client secret" else ""
|
||||||
?.let {
|
val userId = clientSecret?.let {
|
||||||
// Get userId from client secret
|
// Get userId from client secret
|
||||||
pushClientSecret.getUserIdFromSecret(clientSecret)
|
pushClientSecret.getUserIdFromSecret(clientSecret).also {
|
||||||
|
if (it == null) {
|
||||||
|
reason = "Unable to get userId from client secret"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?: run {
|
|
||||||
matrixAuthenticationService.getLatestSessionId()
|
|
||||||
}
|
|
||||||
if (userId == null) {
|
|
||||||
Timber.w("Unable to get a session")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
?: run {
|
||||||
when (resolvedPushEvent) {
|
matrixAuthenticationService.getLatestSessionId().also {
|
||||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
if (it == null) {
|
||||||
is ResolvedPushEvent.Event -> {
|
if (reason.isNotEmpty()) reason += " - "
|
||||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
reason += "Unable to get latest sessionId"
|
||||||
is NotifiableRingingCallEvent -> {
|
|
||||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
|
||||||
handleRingingCallEvent(notifiableEvent)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
|
||||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
|
||||||
if (areNotificationsEnabled) {
|
|
||||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ResolvedPushEvent.Redaction -> {
|
if (userId == null) {
|
||||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
Timber.w("Unable to get a session")
|
||||||
}
|
pushHistoryService.onUnableToRetrieveSession(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = pushData.eventId,
|
||||||
|
roomId = pushData.roomId,
|
||||||
|
reason = reason,
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId).fold(
|
||||||
|
onSuccess = { resolvedPushEvent ->
|
||||||
|
pushHistoryService.onSuccess(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = pushData.eventId,
|
||||||
|
roomId = pushData.roomId,
|
||||||
|
sessionId = userId,
|
||||||
|
comment = resolvedPushEvent.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
|
||||||
|
when (resolvedPushEvent) {
|
||||||
|
is ResolvedPushEvent.Event -> {
|
||||||
|
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||||
|
is NotifiableRingingCallEvent -> {
|
||||||
|
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||||
|
handleRingingCallEvent(notifiableEvent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||||
|
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||||
|
if (areNotificationsEnabled) {
|
||||||
|
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ResolvedPushEvent.Redaction -> {
|
||||||
|
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { failure ->
|
||||||
|
Timber.tag(loggerTag.value).w(failure, "Unable to get a notification data")
|
||||||
|
pushHistoryService.onUnableToResolveEvent(
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = pushData.eventId,
|
||||||
|
roomId = pushData.roomId,
|
||||||
|
sessionId = userId,
|
||||||
|
reason = failure.message ?: failure.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,20 @@ import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import app.cash.sqldelight.coroutines.asFlow
|
||||||
|
import app.cash.sqldelight.coroutines.mapToList
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import io.element.android.libraries.push.impl.PushDatabase
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -28,6 +37,9 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPushDataStore @Inject constructor(
|
class DefaultPushDataStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
|
private val pushDatabase: PushDatabase,
|
||||||
|
private val dateFormatter: DateFormatter,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : PushDataStore {
|
) : PushDataStore {
|
||||||
private val pushCounter = intPreferencesKey("push_counter")
|
private val pushCounter = intPreferencesKey("push_counter")
|
||||||
|
|
||||||
|
|
@ -41,4 +53,35 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
settings[pushCounter] = currentCounterValue + 1
|
settings[pushCounter] = currentCounterValue + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
|
return pushDatabase.pushHistoryQueries.selectAll()
|
||||||
|
.asFlow()
|
||||||
|
.mapToList(dispatchers.io)
|
||||||
|
.map { items ->
|
||||||
|
items.map { pushHistory ->
|
||||||
|
PushHistoryItem(
|
||||||
|
pushDate = pushHistory.pushDate,
|
||||||
|
formattedDate = dateFormatter.format(
|
||||||
|
timestamp = pushHistory.pushDate,
|
||||||
|
mode = DateFormatterMode.Full,
|
||||||
|
useRelative = false,
|
||||||
|
),
|
||||||
|
providerInfo = pushHistory.providerInfo,
|
||||||
|
eventId = pushHistory.eventId?.let { EventId(it) },
|
||||||
|
roomId = pushHistory.roomId?.let { RoomId(it) },
|
||||||
|
sessionId = pushHistory.sessionId?.let { SessionId(it) },
|
||||||
|
hasBeenResolved = pushHistory.hasBeenResolved == 1L,
|
||||||
|
comment = pushHistory.comment,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
pushDatabase.pushHistoryQueries.removeAll()
|
||||||
|
context.dataStore.edit {
|
||||||
|
it.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.store
|
||||||
|
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface PushDataStore {
|
||||||
|
val pushCounterFlow: Flow<Int>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a flow of list of [PushHistoryItem].
|
||||||
|
*/
|
||||||
|
fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the push counter to 0, and clear the database.
|
||||||
|
*/
|
||||||
|
suspend fun reset()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
CREATE TABLE PushHistory (
|
||||||
|
pushDate INTEGER NOT NULL,
|
||||||
|
providerInfo TEXT NOT NULL,
|
||||||
|
eventId TEXT,
|
||||||
|
roomId TEXT,
|
||||||
|
sessionId TEXT,
|
||||||
|
hasBeenResolved INTEGER NOT NULL,
|
||||||
|
comment TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
selectAll:
|
||||||
|
SELECT * FROM PushHistory ORDER BY pushDate DESC;
|
||||||
|
|
||||||
|
insertPushHistory:
|
||||||
|
INSERT INTO PushHistory VALUES ?;
|
||||||
|
|
||||||
|
removeAll:
|
||||||
|
DELETE FROM PushHistory;
|
||||||
|
|
||||||
|
-- add query to keep only the last x entries
|
||||||
|
removeOldest:
|
||||||
|
DELETE FROM PushHistory WHERE rowid NOT IN (SELECT rowid FROM PushHistory ORDER BY pushDate DESC LIMIT ?);
|
||||||
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
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.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||||
|
import io.element.android.libraries.push.impl.store.InMemoryPushDataStore
|
||||||
|
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||||
import io.element.android.libraries.push.impl.test.FakeTestPush
|
import io.element.android.libraries.push.impl.test.FakeTestPush
|
||||||
import io.element.android.libraries.push.impl.test.TestPush
|
import io.element.android.libraries.push.impl.test.TestPush
|
||||||
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
|
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
|
||||||
|
|
@ -288,6 +290,7 @@ class DefaultPushServiceTest {
|
||||||
getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null),
|
getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null),
|
||||||
sessionObserver: SessionObserver = NoOpSessionObserver(),
|
sessionObserver: SessionObserver = NoOpSessionObserver(),
|
||||||
pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(),
|
pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(),
|
||||||
|
pushDataStore: PushDataStore = InMemoryPushDataStore(),
|
||||||
): DefaultPushService {
|
): DefaultPushService {
|
||||||
return DefaultPushService(
|
return DefaultPushService(
|
||||||
testPush = testPush,
|
testPush = testPush,
|
||||||
|
|
@ -296,6 +299,7 @@ class DefaultPushServiceTest {
|
||||||
getCurrentPushProvider = getCurrentPushProvider,
|
getCurrentPushProvider = getCurrentPushProvider,
|
||||||
sessionObserver = sessionObserver,
|
sessionObserver = sessionObserver,
|
||||||
pushClientSecretStore = pushClientSecretStore,
|
pushClientSecretStore = pushClientSecretStore,
|
||||||
|
pushDataStore = pushDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.history
|
||||||
|
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakePushHistoryService(
|
||||||
|
private val onPushReceivedResult: (
|
||||||
|
String,
|
||||||
|
EventId?,
|
||||||
|
RoomId?,
|
||||||
|
SessionId?,
|
||||||
|
Boolean,
|
||||||
|
String?
|
||||||
|
) -> Unit = { _, _, _, _, _, _ -> lambdaError() }
|
||||||
|
) : PushHistoryService {
|
||||||
|
override fun onPushReceived(
|
||||||
|
providerInfo: String,
|
||||||
|
eventId: EventId?,
|
||||||
|
roomId: RoomId?,
|
||||||
|
sessionId: SessionId?,
|
||||||
|
hasBeenResolved: Boolean,
|
||||||
|
comment: String?,
|
||||||
|
) {
|
||||||
|
onPushReceivedResult(
|
||||||
|
providerInfo,
|
||||||
|
eventId,
|
||||||
|
roomId,
|
||||||
|
sessionId,
|
||||||
|
hasBeenResolved,
|
||||||
|
comment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
fun `resolve event no session`() = runTest {
|
fun `resolve event no session`() = runTest {
|
||||||
val sut = createDefaultNotifiableEventResolver(notificationService = null)
|
val sut = createDefaultNotifiableEventResolver(notificationService = null)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -78,7 +78,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
notificationResult = Result.failure(AN_EXCEPTION)
|
notificationResult = Result.failure(AN_EXCEPTION)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -87,7 +87,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
notificationResult = Result.success(null)
|
notificationResult = Result.success(null)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -106,7 +106,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Hello world")
|
aNotifiableMessageEvent(body = "Hello world")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -127,7 +127,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -152,7 +152,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Hello world")
|
aNotifiableMessageEvent(body = "Hello world")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -177,7 +177,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Hello world")
|
aNotifiableMessageEvent(body = "Hello world")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -196,7 +196,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Audio")
|
aNotifiableMessageEvent(body = "Audio")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -215,7 +215,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Video")
|
aNotifiableMessageEvent(body = "Video")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -234,7 +234,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Voice message")
|
aNotifiableMessageEvent(body = "Voice message")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -253,7 +253,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Image")
|
aNotifiableMessageEvent(body = "Image")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -272,7 +272,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Sticker")
|
aNotifiableMessageEvent(body = "Sticker")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -291,7 +291,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "File")
|
aNotifiableMessageEvent(body = "File")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -310,7 +310,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Location")
|
aNotifiableMessageEvent(body = "Location")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -329,7 +329,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Notice")
|
aNotifiableMessageEvent(body = "Notice")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -348,7 +348,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "* Bob is happy")
|
aNotifiableMessageEvent(body = "* Bob is happy")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -367,7 +367,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
val expectedResult = ResolvedPushEvent.Event(
|
val expectedResult = ResolvedPushEvent.Event(
|
||||||
aNotifiableMessageEvent(body = "Poll: A question")
|
aNotifiableMessageEvent(body = "Poll: A question")
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -384,7 +384,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.getOrNull()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -418,7 +418,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -452,7 +452,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -487,7 +487,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -522,7 +522,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isUpdated = false,
|
isUpdated = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -538,7 +538,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.getOrNull()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -564,7 +564,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
timestamp = A_FAKE_TIMESTAMP,
|
timestamp = A_FAKE_TIMESTAMP,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -602,7 +602,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isUpdated = false
|
isUpdated = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -638,7 +638,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -675,7 +675,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -711,7 +711,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -733,7 +733,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
reason = A_REDACTION_REASON,
|
reason = A_REDACTION_REASON,
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -749,46 +749,46 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `resolve null cases`() {
|
fun `resolve null cases`() {
|
||||||
testNull(NotificationContent.MessageLike.CallAnswer)
|
testFailure(NotificationContent.MessageLike.CallAnswer)
|
||||||
testNull(NotificationContent.MessageLike.CallHangup)
|
testFailure(NotificationContent.MessageLike.CallHangup)
|
||||||
testNull(NotificationContent.MessageLike.CallCandidates)
|
testFailure(NotificationContent.MessageLike.CallCandidates)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationReady)
|
testFailure(NotificationContent.MessageLike.KeyVerificationReady)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationStart)
|
testFailure(NotificationContent.MessageLike.KeyVerificationStart)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationCancel)
|
testFailure(NotificationContent.MessageLike.KeyVerificationCancel)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationAccept)
|
testFailure(NotificationContent.MessageLike.KeyVerificationAccept)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationKey)
|
testFailure(NotificationContent.MessageLike.KeyVerificationKey)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationMac)
|
testFailure(NotificationContent.MessageLike.KeyVerificationMac)
|
||||||
testNull(NotificationContent.MessageLike.KeyVerificationDone)
|
testFailure(NotificationContent.MessageLike.KeyVerificationDone)
|
||||||
testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
testFailure(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||||
testNull(NotificationContent.MessageLike.Sticker)
|
testFailure(NotificationContent.MessageLike.Sticker)
|
||||||
testNull(NotificationContent.StateEvent.PolicyRuleRoom)
|
testFailure(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||||
testNull(NotificationContent.StateEvent.PolicyRuleServer)
|
testFailure(NotificationContent.StateEvent.PolicyRuleServer)
|
||||||
testNull(NotificationContent.StateEvent.PolicyRuleUser)
|
testFailure(NotificationContent.StateEvent.PolicyRuleUser)
|
||||||
testNull(NotificationContent.StateEvent.RoomAliases)
|
testFailure(NotificationContent.StateEvent.RoomAliases)
|
||||||
testNull(NotificationContent.StateEvent.RoomAvatar)
|
testFailure(NotificationContent.StateEvent.RoomAvatar)
|
||||||
testNull(NotificationContent.StateEvent.RoomCanonicalAlias)
|
testFailure(NotificationContent.StateEvent.RoomCanonicalAlias)
|
||||||
testNull(NotificationContent.StateEvent.RoomCreate)
|
testFailure(NotificationContent.StateEvent.RoomCreate)
|
||||||
testNull(NotificationContent.StateEvent.RoomEncryption)
|
testFailure(NotificationContent.StateEvent.RoomEncryption)
|
||||||
testNull(NotificationContent.StateEvent.RoomGuestAccess)
|
testFailure(NotificationContent.StateEvent.RoomGuestAccess)
|
||||||
testNull(NotificationContent.StateEvent.RoomHistoryVisibility)
|
testFailure(NotificationContent.StateEvent.RoomHistoryVisibility)
|
||||||
testNull(NotificationContent.StateEvent.RoomJoinRules)
|
testFailure(NotificationContent.StateEvent.RoomJoinRules)
|
||||||
testNull(NotificationContent.StateEvent.RoomName)
|
testFailure(NotificationContent.StateEvent.RoomName)
|
||||||
testNull(NotificationContent.StateEvent.RoomPinnedEvents)
|
testFailure(NotificationContent.StateEvent.RoomPinnedEvents)
|
||||||
testNull(NotificationContent.StateEvent.RoomPowerLevels)
|
testFailure(NotificationContent.StateEvent.RoomPowerLevels)
|
||||||
testNull(NotificationContent.StateEvent.RoomServerAcl)
|
testFailure(NotificationContent.StateEvent.RoomServerAcl)
|
||||||
testNull(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
testFailure(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
||||||
testNull(NotificationContent.StateEvent.RoomTombstone)
|
testFailure(NotificationContent.StateEvent.RoomTombstone)
|
||||||
testNull(NotificationContent.StateEvent.RoomTopic)
|
testFailure(NotificationContent.StateEvent.RoomTopic)
|
||||||
testNull(NotificationContent.StateEvent.SpaceChild)
|
testFailure(NotificationContent.StateEvent.SpaceChild)
|
||||||
testNull(NotificationContent.StateEvent.SpaceParent)
|
testFailure(NotificationContent.StateEvent.SpaceParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testNull(content: NotificationContent) = runTest {
|
private fun testFailure(content: NotificationContent) = runTest {
|
||||||
val sut = createDefaultNotifiableEventResolver(
|
val sut = createDefaultNotifiableEventResolver(
|
||||||
notificationResult = Result.success(
|
notificationResult = Result.success(
|
||||||
aNotificationData(
|
aNotificationData(
|
||||||
|
|
@ -797,7 +797,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||||
assertThat(result).isNull()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDefaultNotifiableEventResolver(
|
private fun createDefaultNotifiableEventResolver(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
||||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||||
),
|
),
|
||||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> aNotifiableMessageEvent() }),
|
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ ->
|
||||||
|
Result.success(aNotifiableMessageEvent())
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
defaultOnMissedCallNotificationHandler.addMissedCallNotification(
|
defaultOnMissedCallNotificationHandler.addMissedCallNotification(
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeNotifiableEventResolver(
|
class FakeNotifiableEventResolver(
|
||||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() }
|
private val notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() }
|
||||||
) : NotifiableEventResolver {
|
) : NotifiableEventResolver {
|
||||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||||
return notifiableEventResult(sessionId, roomId, eventId)
|
return notifiableEventResult(sessionId, roomId, eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.libraries.push.impl.history.FakePushHistoryService
|
||||||
|
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||||
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.ResolvingException
|
||||||
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.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
|
||||||
|
|
@ -42,6 +45,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
|
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
|
||||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
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
|
||||||
|
|
@ -50,16 +54,41 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
private const val A_PUSHER_INFO = "info"
|
||||||
|
|
||||||
class DefaultPushHandlerTest {
|
class DefaultPushHandlerTest {
|
||||||
|
@Test
|
||||||
|
fun `check handleInvalid behavior`() = runTest {
|
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
|
)
|
||||||
|
defaultPushHandler.handleInvalid(A_PUSHER_INFO)
|
||||||
|
incrementPushCounterResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_PUSHER_INFO), value(null), value(null), value(null), value(false), value("Invalid push data"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent> { _, _, _ ->
|
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent>> { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||||
}
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val aPushData = PushData(
|
val aPushData = PushData(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -72,9 +101,10 @@ class DefaultPushHandlerTest {
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
incrementPushCounterResult = incrementPushCounterResult
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
notifiableEventResult.assertions()
|
notifiableEventResult.assertions()
|
||||||
|
|
@ -83,6 +113,8 @@ class DefaultPushHandlerTest {
|
||||||
onNotifiableEventReceived.assertions()
|
onNotifiableEventReceived.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(aNotifiableMessageEvent))
|
.with(value(aNotifiableMessageEvent))
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -90,8 +122,8 @@ class DefaultPushHandlerTest {
|
||||||
runTest {
|
runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||||
}
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
|
@ -101,6 +133,10 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = notifiableEventResult,
|
notifiableEventResult = notifiableEventResult,
|
||||||
|
|
@ -110,15 +146,18 @@ class DefaultPushHandlerTest {
|
||||||
userPushStore = FakeUserPushStore().apply {
|
userPushStore = FakeUserPushStore().apply {
|
||||||
setNotificationEnabledForDevice(false)
|
setNotificationEnabledForDevice(false)
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = incrementPushCounterResult
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
notifiableEventResult.assertions()
|
notifiableEventResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
onNotifiableEventReceived.assertions()
|
onNotifiableEventReceived.assertions()
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -126,8 +165,8 @@ class DefaultPushHandlerTest {
|
||||||
runTest {
|
runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||||
}
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
|
@ -137,6 +176,10 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = notifiableEventResult,
|
notifiableEventResult = notifiableEventResult,
|
||||||
|
|
@ -146,9 +189,10 @@ class DefaultPushHandlerTest {
|
||||||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||||
getLatestSessionIdLambda = { A_USER_ID }
|
getLatestSessionIdLambda = { A_USER_ID }
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = incrementPushCounterResult
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
notifiableEventResult.assertions()
|
notifiableEventResult.assertions()
|
||||||
|
|
@ -157,6 +201,8 @@ class DefaultPushHandlerTest {
|
||||||
onNotifiableEventReceived.assertions()
|
onNotifiableEventReceived.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(aNotifiableMessageEvent))
|
.with(value(aNotifiableMessageEvent))
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -164,8 +210,8 @@ class DefaultPushHandlerTest {
|
||||||
runTest {
|
runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||||
}
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
|
@ -175,6 +221,10 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = notifiableEventResult,
|
notifiableEventResult = notifiableEventResult,
|
||||||
|
|
@ -184,22 +234,27 @@ class DefaultPushHandlerTest {
|
||||||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||||
getLatestSessionIdLambda = { null }
|
getLatestSessionIdLambda = { null }
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = incrementPushCounterResult
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
notifiableEventResult.assertions()
|
notifiableEventResult.assertions()
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
onNotifiableEventReceived.assertions()
|
onNotifiableEventReceived.assertions()
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
||||||
runTest {
|
runTest {
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event?> { _, _, _ -> null }
|
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||||
|
Result.failure(ResolvingException("Unable to resolve"))
|
||||||
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
val aPushData = PushData(
|
val aPushData = PushData(
|
||||||
|
|
@ -208,6 +263,10 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = notifiableEventResult,
|
notifiableEventResult = notifiableEventResult,
|
||||||
|
|
@ -218,9 +277,10 @@ class DefaultPushHandlerTest {
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
incrementPushCounterResult = incrementPushCounterResult
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
notifiableEventResult.assertions()
|
notifiableEventResult.assertions()
|
||||||
|
|
@ -228,6 +288,9 @@ class DefaultPushHandlerTest {
|
||||||
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||||
onNotifiableEventReceived.assertions()
|
onNotifiableEventReceived.assertions()
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -251,20 +314,28 @@ class DefaultPushHandlerTest {
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
elementCallEntryPoint = elementCallEntryPoint,
|
elementCallEntryPoint = elementCallEntryPoint,
|
||||||
notifiableEventResult = { _, _, _ ->
|
notifiableEventResult = { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
Result.success(
|
||||||
|
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
||||||
|
)
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = {},
|
incrementPushCounterResult = {},
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||||
|
onPushReceivedResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -288,21 +359,27 @@ class DefaultPushHandlerTest {
|
||||||
Unit,
|
Unit,
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
elementCallEntryPoint = elementCallEntryPoint,
|
elementCallEntryPoint = elementCallEntryPoint,
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = { _, _, _ ->
|
notifiableEventResult = { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))
|
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = {},
|
incrementPushCounterResult = {},
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
|
|
||||||
handleIncomingCallLambda.assertions().isNeverCalled()
|
handleIncomingCallLambda.assertions().isNeverCalled()
|
||||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||||
|
onPushReceivedResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -326,11 +403,15 @@ class DefaultPushHandlerTest {
|
||||||
Unit,
|
Unit,
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
elementCallEntryPoint = elementCallEntryPoint,
|
elementCallEntryPoint = elementCallEntryPoint,
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
notifiableEventResult = { _, _, _ ->
|
notifiableEventResult = { _, _, _ ->
|
||||||
ResolvedPushEvent.Event(aNotifiableCallEvent())
|
Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent()))
|
||||||
},
|
},
|
||||||
incrementPushCounterResult = {},
|
incrementPushCounterResult = {},
|
||||||
userPushStore = FakeUserPushStore().apply {
|
userPushStore = FakeUserPushStore().apply {
|
||||||
|
|
@ -339,10 +420,12 @@ class DefaultPushHandlerTest {
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||||
|
onPushReceivedResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -361,19 +444,26 @@ class DefaultPushHandlerTest {
|
||||||
)
|
)
|
||||||
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onRedactedEventReceived = onRedactedEventReceived,
|
onRedactedEventReceived = onRedactedEventReceived,
|
||||||
incrementPushCounterResult = incrementPushCounterResult,
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
notifiableEventResult = { _, _, _ -> aRedaction },
|
notifiableEventResult = { _, _, _ -> Result.success(aRedaction) },
|
||||||
pushClientSecret = FakePushClientSecret(
|
pushClientSecret = FakePushClientSecret(
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
onRedactedEventReceived.assertions().isCalledOnce()
|
onRedactedEventReceived.assertions().isCalledOnce()
|
||||||
.with(value(aRedaction))
|
.with(value(aRedaction))
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -386,20 +476,27 @@ class DefaultPushHandlerTest {
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val diagnosticPushHandler = DiagnosticPushHandler()
|
val diagnosticPushHandler = DiagnosticPushHandler()
|
||||||
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||||
|
val pushHistoryService = FakePushHistoryService(
|
||||||
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
|
)
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
diagnosticPushHandler = diagnosticPushHandler,
|
diagnosticPushHandler = diagnosticPushHandler,
|
||||||
incrementPushCounterResult = { }
|
incrementPushCounterResult = { },
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
diagnosticPushHandler.state.test {
|
diagnosticPushHandler.state.test {
|
||||||
defaultPushHandler.handle(aPushData)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
awaitItem()
|
awaitItem()
|
||||||
}
|
}
|
||||||
|
onPushReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDefaultPushHandler(
|
private fun createDefaultPushHandler(
|
||||||
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
|
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||||
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||||
notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() },
|
notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() },
|
||||||
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
||||||
userPushStore: UserPushStore = FakeUserPushStore(),
|
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||||
|
|
@ -408,6 +505,7 @@ class DefaultPushHandlerTest {
|
||||||
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
|
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
|
||||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||||
|
pushHistoryService: PushHistoryService = FakePushHistoryService(),
|
||||||
): DefaultPushHandler {
|
): DefaultPushHandler {
|
||||||
return DefaultPushHandler(
|
return DefaultPushHandler(
|
||||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
||||||
|
|
@ -425,6 +523,7 @@ class DefaultPushHandlerTest {
|
||||||
diagnosticPushHandler = diagnosticPushHandler,
|
diagnosticPushHandler = diagnosticPushHandler,
|
||||||
elementCallEntryPoint = elementCallEntryPoint,
|
elementCallEntryPoint = elementCallEntryPoint,
|
||||||
notificationChannels = notificationChannels,
|
notificationChannels = notificationChannels,
|
||||||
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.store
|
||||||
|
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class InMemoryPushDataStore(
|
||||||
|
initialPushCounter: Int = 0,
|
||||||
|
initialPushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||||
|
private val resetResult: () -> Unit = { lambdaError() }
|
||||||
|
) : PushDataStore {
|
||||||
|
private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter)
|
||||||
|
override val pushCounterFlow: Flow<Int> = mutablePushCounterFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems)
|
||||||
|
|
||||||
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
|
return mutablePushHistoryItemsFlow.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
resetResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,10 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.libraries.push.api)
|
api(projects.libraries.push.api)
|
||||||
|
api(projects.libraries.pushproviders.api)
|
||||||
implementation(projects.libraries.push.impl)
|
implementation(projects.libraries.push.impl)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.push.test
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
import io.element.android.libraries.pushproviders.api.Distributor
|
import io.element.android.libraries.pushproviders.api.Distributor
|
||||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
@ -26,6 +27,7 @@ class FakePushService(
|
||||||
private val currentPushProvider: () -> PushProvider? = { availablePushProviders.firstOrNull() },
|
private val currentPushProvider: () -> PushProvider? = { availablePushProviders.firstOrNull() },
|
||||||
private val selectPushProviderLambda: suspend (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
private val selectPushProviderLambda: suspend (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||||
private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val resetPushHistoryResult: () -> Unit = { lambdaError() },
|
||||||
) : PushService {
|
) : PushService {
|
||||||
override suspend fun getCurrentPushProvider(): PushProvider? {
|
override suspend fun getCurrentPushProvider(): PushProvider? {
|
||||||
return registeredPushProvider ?: currentPushProvider()
|
return registeredPushProvider ?: currentPushProvider()
|
||||||
|
|
@ -68,4 +70,26 @@ class FakePushService(
|
||||||
override suspend fun testPush(): Boolean = simulateLongTask {
|
override suspend fun testPush(): Boolean = simulateLongTask {
|
||||||
testPushBlock()
|
testPushBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val pushHistoryItemsFlow = MutableStateFlow<List<PushHistoryItem>>(emptyList())
|
||||||
|
|
||||||
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
|
return pushHistoryItemsFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitPushHistoryItems(items: List<PushHistoryItem>) {
|
||||||
|
pushHistoryItemsFlow.value = items
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pushCounterFlow = MutableStateFlow(0)
|
||||||
|
|
||||||
|
override val pushCounter: Flow<Int> = pushCounterFlow
|
||||||
|
|
||||||
|
fun emitPushCounter(counter: Int) {
|
||||||
|
pushCounterFlow.value = counter
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resetPushHistory() {
|
||||||
|
resetPushHistoryResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,14 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||||
import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver
|
import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeCallNotificationEventResolver(
|
class FakeCallNotificationEventResolver(
|
||||||
var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> NotifiableEvent? = { _, _, _ -> null },
|
var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> Result<NotifiableEvent> = { _, _, _ ->
|
||||||
|
lambdaError()
|
||||||
|
},
|
||||||
) : CallNotificationEventResolver {
|
) : CallNotificationEventResolver {
|
||||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): Result<NotifiableEvent> {
|
||||||
return resolveEventLambda(sessionId, notificationData, forceNotify)
|
return resolveEventLambda(sessionId, notificationData, forceNotify)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,14 @@ import io.element.android.libraries.pushproviders.api.PushHandler
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakePushHandler(
|
class FakePushHandler(
|
||||||
private val handleResult: (PushData) -> Unit = { lambdaError() }
|
private val handleResult: (PushData, String) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val handleInvalidResult: (String) -> Unit = { lambdaError() },
|
||||||
) : PushHandler {
|
) : PushHandler {
|
||||||
override suspend fun handle(pushData: PushData) {
|
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||||
handleResult(pushData)
|
handleResult(pushData, providerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun handleInvalid(providerInfo: String) {
|
||||||
|
handleInvalidResult(providerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,12 @@
|
||||||
package io.element.android.libraries.pushproviders.api
|
package io.element.android.libraries.pushproviders.api
|
||||||
|
|
||||||
interface PushHandler {
|
interface PushHandler {
|
||||||
suspend fun handle(pushData: PushData)
|
suspend fun handle(
|
||||||
|
pushData: PushData,
|
||||||
|
providerInfo: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun handleInvalid(
|
||||||
|
providerInfo: String,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
val pushData = pushParser.parse(message.data)
|
val pushData = pushParser.parse(message.data)
|
||||||
if (pushData == null) {
|
if (pushData == null) {
|
||||||
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
||||||
|
pushHandler.handleInvalid(
|
||||||
|
providerInfo = FirebaseConfig.NAME,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
pushHandler.handle(pushData)
|
pushHandler.handle(
|
||||||
|
pushData = pushData,
|
||||||
|
providerInfo = FirebaseConfig.NAME,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.tests.testutils.lambda.value
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
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
|
||||||
|
|
@ -31,16 +32,18 @@ import org.robolectric.RobolectricTestRunner
|
||||||
class VectorFirebaseMessagingServiceTest {
|
class VectorFirebaseMessagingServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test receiving invalid data`() = runTest {
|
fun `test receiving invalid data`() = runTest {
|
||||||
val lambda = lambdaRecorder<PushData, Unit>(ensureNeverCalled = true) { }
|
val lambda = lambdaRecorder<String, Unit> {}
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleResult = lambda)
|
pushHandler = FakePushHandler(handleInvalidResult = lambda)
|
||||||
)
|
)
|
||||||
vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle()))
|
vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle()))
|
||||||
|
runCurrent()
|
||||||
|
lambda.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test receiving valid data`() = runTest {
|
fun `test receiving valid data`() = runTest {
|
||||||
val lambda = lambdaRecorder<PushData, Unit> { }
|
val lambda = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleResult = lambda)
|
pushHandler = FakePushHandler(handleResult = lambda)
|
||||||
)
|
)
|
||||||
|
|
@ -56,7 +59,10 @@ class VectorFirebaseMessagingServiceTest {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
lambda.assertions()
|
lambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)))
|
.with(
|
||||||
|
value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)),
|
||||||
|
value(FirebaseConfig.NAME)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,14 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
val pushData = pushParser.parse(message, instance)
|
val pushData = pushParser.parse(message, instance)
|
||||||
if (pushData == null) {
|
if (pushData == null) {
|
||||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||||
|
pushHandler.handleInvalid(
|
||||||
|
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
pushHandler.handle(pushData)
|
pushHandler.handle(
|
||||||
|
pushData = pushData,
|
||||||
|
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onMessage valid invoke the push handler`() = runTest {
|
fun `onMessage valid invokes the push handler`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val pushHandlerResult = lambdaRecorder<PushData, Unit> {}
|
val pushHandlerResult = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||||
pushHandler = FakePushHandler(
|
pushHandler = FakePushHandler(
|
||||||
handleResult = pushHandlerResult
|
handleResult = pushHandlerResult
|
||||||
|
|
@ -80,23 +80,25 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
unread = 1,
|
unread = 1,
|
||||||
clientSecret = A_SECRET
|
clientSecret = A_SECRET
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
value(
|
||||||
|
UnifiedPushConfig.NAME + " - " + A_SECRET
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onMessage invalid does not invoke the push handler`() = runTest {
|
fun `onMessage invalid invokes the push handler invalid method`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val pushHandlerResult = lambdaRecorder<PushData, Unit> {}
|
val handleInvalidResult = lambdaRecorder<String, Unit> { }
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||||
pushHandler = FakePushHandler(
|
pushHandler = FakePushHandler(
|
||||||
handleResult = pushHandlerResult
|
handleInvalidResult = handleInvalidResult,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET)
|
vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
pushHandlerResult.assertions()
|
handleInvalidResult.assertions().isCalledOnce()
|
||||||
.isNeverCalled()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import extension.setupAnvil
|
import extension.setupAnvil
|
||||||
import org.gradle.internal.extensions.stdlib.capitalized
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -52,15 +50,3 @@ sqldelight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for KSP not picking up the generated files from SqlDelight
|
|
||||||
androidComponents {
|
|
||||||
onVariants(selector().all()) { variant ->
|
|
||||||
afterEvaluate {
|
|
||||||
val variantName = variant.name.capitalized()
|
|
||||||
tasks.getByName<KotlinCompile>("ksp${variantName}Kotlin") {
|
|
||||||
setSource(tasks.getByName("generate${variantName}SessionDatabaseInterface").outputs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.api
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
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.SessionId
|
||||||
|
|
||||||
|
interface PushHistoryEntryPoint : FeatureEntryPoint {
|
||||||
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
|
interface NodeBuilder {
|
||||||
|
fun callback(callback: Callback): NodeBuilder
|
||||||
|
fun build(): Node
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onDone()
|
||||||
|
fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,8 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.libraries.troubleshoot.api)
|
api(projects.libraries.troubleshoot.api)
|
||||||
api(projects.libraries.push.api)
|
api(projects.libraries.push.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
@ -40,6 +42,7 @@ dependencies {
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPushHistoryEntryPoint @Inject constructor() : PushHistoryEntryPoint {
|
||||||
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PushHistoryEntryPoint.NodeBuilder {
|
||||||
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
|
return object : PushHistoryEntryPoint.NodeBuilder {
|
||||||
|
override fun callback(callback: PushHistoryEntryPoint.Callback): PushHistoryEntryPoint.NodeBuilder {
|
||||||
|
plugins += callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
return parentNode.createNode<PushHistoryNode>(buildContext, plugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
sealed interface PushHistoryEvents {
|
||||||
|
data object Reset : PushHistoryEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||||
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class PushHistoryNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: PushHistoryPresenter,
|
||||||
|
private val screenTracker: ScreenTracker,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
private fun onDone() {
|
||||||
|
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||||
|
it.onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||||
|
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||||
|
it.onItemClick(sessionId, roomId, eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot)
|
||||||
|
val state = presenter.present()
|
||||||
|
PushHistoryView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = ::onDone,
|
||||||
|
onItemClick = ::onItemClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PushHistoryPresenter @Inject constructor(
|
||||||
|
private val pushService: PushService,
|
||||||
|
) : Presenter<PushHistoryState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): PushHistoryState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val pushCounter by pushService.pushCounter.collectAsState(0)
|
||||||
|
val pushHistory by remember {
|
||||||
|
pushService.getPushHistoryItemsFlow()
|
||||||
|
}.collectAsState(emptyList())
|
||||||
|
|
||||||
|
fun handleEvents(event: PushHistoryEvents) {
|
||||||
|
when (event) {
|
||||||
|
PushHistoryEvents.Reset -> coroutineScope.launch {
|
||||||
|
pushService.resetPushHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PushHistoryState(
|
||||||
|
pushCounter = pushCounter,
|
||||||
|
pushHistoryItems = pushHistory.toImmutableList(),
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class PushHistoryState(
|
||||||
|
val pushCounter: Int,
|
||||||
|
val pushHistoryItems: ImmutableList<PushHistoryItem>,
|
||||||
|
val eventSink: (PushHistoryEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
open class PushHistoryStateProvider : PreviewParameterProvider<PushHistoryState> {
|
||||||
|
override val values: Sequence<PushHistoryState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aPushHistoryState(),
|
||||||
|
aPushHistoryState(
|
||||||
|
pushCounter = 123,
|
||||||
|
pushHistoryItems = listOf(
|
||||||
|
aPushHistoryItem(
|
||||||
|
hasBeenResolved = false,
|
||||||
|
comment = "An error description"
|
||||||
|
),
|
||||||
|
aPushHistoryItem(
|
||||||
|
pushDate = 1,
|
||||||
|
providerInfo = "providerInfo2",
|
||||||
|
eventId = EventId("\$anEventId"),
|
||||||
|
roomId = RoomId("!roomId:domain"),
|
||||||
|
sessionId = SessionId("@alice:server.org"),
|
||||||
|
hasBeenResolved = true,
|
||||||
|
comment = "A comment"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aPushHistoryState(
|
||||||
|
pushCounter: Int = 0,
|
||||||
|
pushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||||
|
eventSink: (PushHistoryEvents) -> Unit = {},
|
||||||
|
) = PushHistoryState(
|
||||||
|
pushCounter = pushCounter,
|
||||||
|
pushHistoryItems = pushHistoryItems.toImmutableList(),
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aPushHistoryItem(
|
||||||
|
pushDate: Long = 0,
|
||||||
|
formattedDate: String = "formattedDate",
|
||||||
|
providerInfo: String = "providerInfo",
|
||||||
|
eventId: EventId? = null,
|
||||||
|
roomId: RoomId? = null,
|
||||||
|
sessionId: SessionId? = null,
|
||||||
|
hasBeenResolved: Boolean = false,
|
||||||
|
comment: String? = null,
|
||||||
|
): PushHistoryItem {
|
||||||
|
return PushHistoryItem(
|
||||||
|
pushDate = pushDate,
|
||||||
|
formattedDate = formattedDate,
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
eventId = eventId,
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
hasBeenResolved = hasBeenResolved,
|
||||||
|
comment = comment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
|
import io.element.android.libraries.troubleshoot.impl.R
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PushHistoryView(
|
||||||
|
state: PushHistoryState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.systemBarsPadding()
|
||||||
|
.imePadding(),
|
||||||
|
contentWindowInsets = WindowInsets.statusBars,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackClick)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_push_history_title),
|
||||||
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_reset),
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(PushHistoryEvents.Reset)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
PushHistoryContent(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
|
state = state,
|
||||||
|
onItemClick = onItemClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PushHistoryContent(
|
||||||
|
state: PushHistoryState,
|
||||||
|
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Total number of received push") },
|
||||||
|
trailingContent = ListItemContent.Text(state.pushCounter.toString()),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.pushHistoryItems,
|
||||||
|
key = {
|
||||||
|
it.pushDate.toString() + it.sessionId + it.roomId + it.eventId
|
||||||
|
},
|
||||||
|
) { pushHistory ->
|
||||||
|
PushHistoryItem(
|
||||||
|
pushHistory,
|
||||||
|
onClick = {
|
||||||
|
val sessionId = pushHistory.sessionId
|
||||||
|
val roomId = pushHistory.roomId
|
||||||
|
val eventId = pushHistory.eventId
|
||||||
|
if (sessionId != null && roomId != null && eventId != null) {
|
||||||
|
onItemClick(sessionId, roomId, eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PushHistoryItem(
|
||||||
|
pushHistoryItem: PushHistoryItem,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = pushHistoryItem.formattedDate,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = pushHistoryItem.providerInfo,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
|
||||||
|
text = pushHistoryItem.sessionId?.value ?: "No sessionId",
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = pushHistoryItem.roomId?.value ?: "No roomId",
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = pushHistoryItem.eventId?.value ?: "No eventId",
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
|
pushHistoryItem.comment?.let {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
text = it,
|
||||||
|
color = if (pushHistoryItem.hasBeenResolved) {
|
||||||
|
ElementTheme.colors.textSecondary
|
||||||
|
} else {
|
||||||
|
ElementTheme.colors.textCriticalPrimary
|
||||||
|
},
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pushHistoryItem.hasBeenResolved) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.CheckCircleSolid(),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Error(),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun PushHistoryViewPreview(
|
||||||
|
@PreviewParameter(PushHistoryStateProvider::class) state: PushHistoryState,
|
||||||
|
) = ElementPreview {
|
||||||
|
PushHistoryView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
onItemClick = { _, _, _ -> },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_push_history_title">"Push history"</string>
|
||||||
<string name="troubleshoot_notifications_screen_action">"Run tests"</string>
|
<string name="troubleshoot_notifications_screen_action">"Run tests"</string>
|
||||||
<string name="troubleshoot_notifications_screen_action_again">"Run tests again"</string>
|
<string name="troubleshoot_notifications_screen_action_again">"Run tests again"</string>
|
||||||
<string name="troubleshoot_notifications_screen_failure">"Some tests failed. Please check the details."</string>
|
<string name="troubleshoot_notifications_screen_failure">"Some tests failed. Please check the details."</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PushHistoryPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createPushHistoryPresenter()
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.pushCounter).isEqualTo(0)
|
||||||
|
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - updating state`() = runTest {
|
||||||
|
val pushService = FakePushService()
|
||||||
|
val presenter = createPushHistoryPresenter(
|
||||||
|
pushService = pushService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.pushCounter).isEqualTo(0)
|
||||||
|
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||||
|
pushService.emitPushCounter(1)
|
||||||
|
assertThat(awaitItem().pushCounter).isEqualTo(1)
|
||||||
|
val item = aPushHistoryItem()
|
||||||
|
pushService.emitPushHistoryItems(listOf(item))
|
||||||
|
assertThat(awaitItem().pushHistoryItems).containsExactly(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - reset`() = runTest {
|
||||||
|
val resetPushHistoryResult = lambdaRecorder<Unit> { }
|
||||||
|
val pushService = FakePushService(
|
||||||
|
resetPushHistoryResult = resetPushHistoryResult,
|
||||||
|
)
|
||||||
|
val presenter = createPushHistoryPresenter(
|
||||||
|
pushService = pushService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(PushHistoryEvents.Reset)
|
||||||
|
runCurrent()
|
||||||
|
resetPushHistoryResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPushHistoryPresenter(
|
||||||
|
pushService: PushService = FakePushService(),
|
||||||
|
): PushHistoryPresenter {
|
||||||
|
return PushHistoryPresenter(
|
||||||
|
pushService = pushService,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
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.SessionId
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_FORMATTED_DATE
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithThreeParams
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PushHistoryViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Reset sends a PushHistoryEvents`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>()
|
||||||
|
rule.setPushHistoryView(
|
||||||
|
aPushHistoryState(
|
||||||
|
pushCounter = 123,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_reset)
|
||||||
|
eventsRecorder.assertSingle(PushHistoryEvents.Reset)
|
||||||
|
// Also check that the push counter is rendered
|
||||||
|
rule.onNodeWithText("123").assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on an invalid event has no effect`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||||
|
rule.setPushHistoryView(
|
||||||
|
aPushHistoryState(
|
||||||
|
pushHistoryItems = listOf(
|
||||||
|
aPushHistoryItem(
|
||||||
|
formattedDate = A_FORMATTED_DATE,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(A_FORMATTED_DATE).performClick()
|
||||||
|
// No callback invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on a valid event invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||||
|
val onItemClick = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
|
rule.setPushHistoryView(
|
||||||
|
aPushHistoryState(
|
||||||
|
pushHistoryItems = listOf(
|
||||||
|
aPushHistoryItem(
|
||||||
|
formattedDate = A_FORMATTED_DATE,
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onItemClick = onItemClick,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(A_FORMATTED_DATE).performClick()
|
||||||
|
onItemClick.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPushHistoryView(
|
||||||
|
state: PushHistoryState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onItemClick: (SessionId, RoomId, EventId) -> Unit = EnsureNeverCalledWithThreeParams(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
PushHistoryView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onItemClick = onItemClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ private const val versionYear = 25
|
||||||
private const val versionMonth = 4
|
private const val versionMonth = 4
|
||||||
|
|
||||||
// Note: must be in [0,99]
|
// Note: must be in [0,99]
|
||||||
private const val versionReleaseNumber = 0
|
private const val versionReleaseNumber = 1
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber
|
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ private fun Project.kover(action: Action<KoverProjectExtension>) {
|
||||||
|
|
||||||
fun Project.setupKover() {
|
fun Project.setupKover() {
|
||||||
// Create verify all task joining all existing verification tasks
|
// Create verify all task joining all existing verification tasks
|
||||||
task("koverVerifyAll") {
|
tasks.register("koverVerifyAll") {
|
||||||
group = "verification"
|
group = "verification"
|
||||||
description = "Verifies the code coverage of all subprojects."
|
description = "Verifies the code coverage of all subprojects."
|
||||||
val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
|
val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:27587909106650e33d7bc7854c0f2dd7ca6e2dad0aaf6487bf266753288ec6f6
|
oid sha256:6408d329ea127961b08cb92fcfbbdd93fc2c700191e7c97ce4ebad147647c1c4
|
||||||
size 24898
|
size 24899
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9e65635400bfda5d242d07ebe31113e3ef2d039f21208421023f099997d5e4f9
|
oid sha256:c1764cc1d1fc5234ef88a1f1cf9317a234cd7ee7195a190a0e827442c0d75db2
|
||||||
size 29603
|
size 29601
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue